94 lines
6.3 KiB
Markdown
94 lines
6.3 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.
|
|
|
|
> **Grill-refined (implemented).** This sub-plan was hardened in a follow-up grill and built. Deltas
|
|
> from the text below, all in the shipped code:
|
|
> - WS `type` `health` → **`probe_gateway`** (in-game command stays `sandbox health`).
|
|
> - Validation uses **zod** (`protocol.ts`); `parse()` throws on malformed / unknown `event` / failed
|
|
> schema (handler catches → debug-logs → drops). `payload` (`[]`/missing/non-object → `{}`) via zod.
|
|
> - `resolveAccount(secret, password)` owns the password gate (signature gained `password`).
|
|
> - Dispatch state machine extracted to **`connection.ts`** (pure, FakeSocket-testable); the module
|
|
> `index.ts` is the thin `ws` adapter. Registry depends on a minimal `GatewaySocket { send, close }`.
|
|
> - `registry.request()` rejects a typed **`GatewayError { code }`** on not_connected/timeout/ok:false,
|
|
> resolves the payload on success. Newest-wins eviction rejects the old socket's pendings + closes it;
|
|
> `unregister`/close are reference-identity-checked.
|
|
> - Logging via Fastify's **pino**; env `LOG_LEVEL` (default `info`, `silent` in tests), `LOG_PRETTY`;
|
|
> silent drops log at `debug` with `{ event, type, reason }`. **No `serve` script** — `dev` is the
|
|
> pretty entry point (`LOG_PRETTY=1 tsx watch …`); `pino-pretty` devDep; `zod` dep.
|
|
> - Every `computer_request` (incl. `hello`) must carry non-empty `traposSenderId` (zod-enforced).
|
|
> - Open `GET /health` is liveness-only (`{ok:true}`, no auth, no probe). Opencode probe: client built
|
|
> once, fresh uncached 5s-bounded `config.get()` per call, no-URL short-circuit.
|
|
|
|
## 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.
|