cc-libs/.plans/archived/trap-sandbox/02-lua-client.md
2026-06-15 19:59:26 +02:00

61 lines
3.9 KiB
Markdown

# 02 — Lua client (`trapos-sandbox`)
> Sub-plan of [trap-sandbox-gateway-spec](./trap-sandbox-gateway-spec.md). Implements the
> **Protocol**, **Handshake**, and **os-event contract** from the spec — reference them, don't
> redefine. M2; depends on the protocol contract (not on M1 code existing).
## Scope
The Lua side: `apis/libsandbox.lua`, `servers/sandbox.lua`, `programs/sandbox.lua`, and Lua **unit**
tests. Package/meta changes are [03-packages](./03-packages.md); integration/e2e is
[04-integration-tooling](./04-integration-tooling.md).
## Task checklist
- [ ] `apis/libsandbox.lua` (template: `apis/libmcpcomputer.lua`).
- [ ] `servers/sandbox.lua` (template: `servers/mcp-computer-server.lua`).
- [ ] `programs/sandbox.lua` (template: `programs/ai.lua`).
- [ ] Lua unit tests (`tests/sandbox.lua`).
- [ ] Gate: `just _craftos-test` runs `tests/sandbox.lua` green.
## `apis/libsandbox.lua` — template: `apis/libmcpcomputer.lua`
`createSandbox()` returns:
- `traposId(osLike)``id .. ":" .. (label or "")`.
- Envelope builders for the 4 events + hello. **Every `computer_request` (including `hello`) must
carry a non-empty `traposSenderId` = the computer's `traposId`** — the TS gateway's zod schema
requires it (decided in the 01-ts-gateway grill). The `hello` identity used for registration is
still `payload.traposId`, but the envelope `traposSenderId` must also be present.
- `onMessage(content, …)`: decode (pcall); `gateway_request`→ built-in `ping` (else
`ok:false unknown_type`) replied as `computer_response` via pcall-send; `gateway_response`→ if
`type=="hello"` drive the handshake state, else `queueEvent('trapos_sandbox_reply', messageId, ok,
payload, error)`.
- `startSession(opts)` (daemon; injectable `http`/`eventloop`/`encode`/`decode`/`os`): builds the
connect URL from base `opts.url`, computes `traposId`; `websocketAsync`; on `websocket_success`
send hello; on hello `ok:true` set state=ready + `queueEvent('trapos_sandbox_connected', traposId)`;
on hello `ok:false` set state=unauthorized (**terminal**, store `lastError`, close, no reconnect);
on `websocket_failure`/`_closed` reconnect via `el.setTimeout(connect, 5)` (unless unauthorized);
hello with no ack in ~10s → close + reconnect; `trapos_sandbox_send` handler builds
`computer_request`, **pcall-send** only when ready, else fast `trapos_sandbox_reply(messageId,
false, nil, {code:"not_connected"})`. Returns `{ isReady, lastError }`.
- `request(type, payload, opts)` (injectable `queueEvent`/`pullEvent`/`startTimer`): mint messageId,
queue `trapos_sandbox_send`, wait for matching `trapos_sandbox_reply` vs a timer (**default 10s**,
overridable). Returns `(ok, payloadOrError)`.
## `servers/sandbox.lua` — template: `servers/mcp-computer-server.lua`
Read `sandbox.url` (unset/`http.websocket` missing → "daemon inactive", return) and `sandbox.password`;
`startSession{ eventloop=_G.bootEventLoop, url, password(secret), os }`; print version.
## `programs/sandbox.lua` — template: `programs/ai.lua`
- Reads `sandbox.url` first; unset → immediate `set sandbox.url first` (no 10s wait).
- `health` command → `request('probe_gateway')` (wire `type` renamed from `health` during the
01-ts-gateway grill; the in-game command name stays `health`); print `sandboxOk`/`opencodeOk`
(+ `opencode: <detail>` when false); on `not ok` print `sandbox health failed: <code> <message>`
(e.g. `unauthorized`, `not_connected`, `timeout`).
- `--version` / `--help`.
## Lua unit tests (`tests/sandbox.lua`, libtest + fakes)
Envelope builders, `traposId`, `onMessage` dispatch (ping ok / unknown_type / hello), empty-payload
coercion, `startSession` wiring (fake eventloop+http: hello sent on open, ready on ack, terminal on
unauthorized, reconnect on drop), `request()` resolve + timeout via injected event primitives.
## Verification gate
`just _craftos-test` runs the new `tests/sandbox.lua` green.