cc-libs/.plans/trap-sandbox/01-ts-gateway.md
2026-06-14 02:31:39 +02:00

76 lines
4.8 KiB
Markdown

# 01 — TS gateway (`tools/trap-sandbox/`)
> Sub-plan of [trap-sandbox-gateway-spec](./trap-sandbox-gateway-spec.md). Implements the
> **Protocol** and **Handshake** contracts defined there — do not redefine frame shapes or error
> codes here; reference the spec. **Build first** (M1): no Lua dependency.
## Scope
The TypeScript service: Fastify app on port 4444, the `trapos-cloud-gateway` module
(protocol/registry/WS), the opencode probe, the auth seam, env/config, package wiring, and TS
**unit** tests. Integration/e2e tests live in [04-integration-tooling](./04-integration-tooling.md).
## Task checklist
- [ ] Scaffold `tools/trap-sandbox/` (ESM, `tsc` build, `tsx` dev/test, `node:test`, typescript-eslint
flat config — copy `mcp-bridge`'s `tsconfig.json`/`eslint.config.js`).
- [ ] `package.json`: deps + devDeps + scripts (see below).
- [ ] `src/auth.ts``resolveAccount(secret)`.
- [ ] `src/opencode.ts``createOpencodeProbe(config)`.
- [ ] `src/modules/trapos-cloud-gateway/protocol.ts` — envelope/error types + parse/validate.
- [ ] `src/modules/trapos-cloud-gateway/registry.ts``GatewayRegistry`.
- [ ] `src/modules/trapos-cloud-gateway/index.ts` — Fastify plugin: `GET /health` (open) + `GET /gateway` (ws).
- [ ] `src/app.ts``createApp(config)` returning `{ app, registry }`.
- [ ] `src/index.ts` — env config, listen, `EADDRINUSE` clear message + exit.
- [ ] TS unit tests (`test/`): protocol, registry, auth/hello, probe.
- [ ] Gate: `npm test`, `npm run lint`/`check` clean.
## Layout
ESM, `tsc` build, `tsx` dev/test, `node:test`, typescript-eslint flat config (copy mcp-bridge's
`tsconfig.json`/`eslint.config.js`). Fastify `^5` + `@fastify/websocket` `^11`; `engines.node>=20`.
```
src/
├── index.ts # env config; build app; listen on SANDBOX_PORT; clear EADDRINUSE message + exit
├── app.ts # createApp(config): Fastify + GatewayRegistry; register @fastify/websocket + module; returns { app, registry }
├── auth.ts # resolveAccount(secret): "local" | null (constant seam; null = unauthorized)
├── opencode.ts # createOpencodeProbe(config): probe(): { ok, detail } via @opencode-ai/sdk
└── modules/trapos-cloud-gateway/
├── index.ts # Fastify plugin: GET /health (open) + GET /gateway (ws)
├── protocol.ts # envelope/error types + parse/validate (4 events, hello, empty-payload→{})
└── registry.ts # GatewayRegistry keyed by (accountId,traposId); newest-wins; request()+timeout; pending cleanup on close
test/ # TS unit (protocol, registry, auth/hello, probe) with FakeSocket + fake fetch
```
## Behavior
- **WS `/gateway`** (`{ websocket: true }`): no upgrade auth (auth is the hello frame). `maxPayload`
~1MB. `socket.on("message")` parses a frame inside try/catch → dispatch: pre-hello only `hello` is
honored; post-hello handle `computer_request` (`health`, else `unknown_type`) and
`computer_response` (resolve pending `gateway_request` by messageId). `close`/`error` → unregister
+ reject that connection's pendings. (Handshake steps & error codes: see spec **Handshake**.)
- **GatewayRegistry**: `register`/`unregister`/`has`/`count`; `request(accountId, traposId, type,
payload, timeoutMs=600000)` sends `gateway_request`, races matching `computer_response` vs timeout.
Keyed by `(accountId, traposId)`, newest-wins.
- **opencode probe**: `createOpencodeClient({ baseUrl: OPENCODE_URL, headers:{ Authorization: basic
("opencode", OPENCODE_SERVER_PASSWORD) }, throwOnError:false })` then `config.get()` with
`AbortSignal.timeout(5000)`, wrapped in try/catch. `ok = response 2xx`; `detail` = url + status or
error. No `OPENCODE_URL``{ ok:false, detail:"OPENCODE_URL not set" }` without a call.
- **auth**: `resolveAccount(secret)``"local" | null` (constant seam; `null` = unauthorized).
## Env (`index.ts`)
`SANDBOX_HOST`(`0.0.0.0`), `SANDBOX_PORT`(`4444`), `SANDBOX_PASSWORD`(unset⇒ auth off),
`SANDBOX_REQUEST_TIMEOUT_MS`(`600000`), `OPENCODE_URL`(`http://127.0.0.1:4242`),
`OPENCODE_SERVER_PASSWORD`, `OPENCODE_USERNAME`(`opencode`).
## package.json
- deps: `fastify`, `@fastify/websocket`, `@opencode-ai/sdk` (move from devDeps).
- devDeps: copy mcp-bridge (`typescript`, `tsx`, `@eslint/js`, `typescript-eslint`, `@types/node`,
`globals`, `eslint`, `@types/ws`).
- scripts: copy mcp-bridge **plus `"lint":"npm run eslint"`**.
## TS unit tests (`test/`)
Protocol parse/validate (4 events, hello, `ok`/`error`, empty-payload→`{}`), registry (duplicate
newest-wins per account, messageId correlation, timeout, pending cleanup on close), `resolveAccount`,
opencode probe (fake fetch/server: ok / 401 / timeout). Use `FakeSocket` + fake fetch.
## Verification gate
`npm test` + `npm run lint`/`check` clean in `tools/trap-sandbox`; service starts and listens on 4444.