4.8 KiB
01 — TS gateway (tools/trap-sandbox/)
Sub-plan of trap-sandbox-gateway-spec. 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.
Task checklist
- Scaffold
tools/trap-sandbox/(ESM,tscbuild,tsxdev/test,node:test, typescript-eslint flat config — copymcp-bridge'stsconfig.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,EADDRINUSEclear message + exit.- TS unit tests (
test/): protocol, registry, auth/hello, probe. - Gate:
npm test,npm run lint/checkclean.
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 onlyhellois honored; post-hello handlecomputer_request(health, elseunknown_type) andcomputer_response(resolve pendinggateway_requestby 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)sendsgateway_request, races matchingcomputer_responsevs timeout. Keyed by(accountId, traposId), newest-wins. - opencode probe:
createOpencodeClient({ baseUrl: OPENCODE_URL, headers:{ Authorization: basic ("opencode", OPENCODE_SERVER_PASSWORD) }, throwOnError:false })thenconfig.get()withAbortSignal.timeout(5000), wrapped in try/catch.ok = response 2xx;detail= url + status or error. NoOPENCODE_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.