{"_id":"@brainst0rm/endpoint-stub","name":"@brainst0rm/endpoint-stub","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@brainst0rm/endpoint-stub","version":"0.1.0","repository":{"type":"git","url":"git+https://github.com/justinjilg/brainstorm.git","directory":"packages/endpoint-stub"},"type":"module","description":"Reference implementation of the endpoint side of the Brainstorm dispatch protocol. Connects to a relay over WS, receives CommandEnvelopes, executes tools via a pluggable executor, emits CommandAck/ProgressEvent/CommandResult back. Used as a test fixture f","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"bin":{"brainstorm-endpoint-stub":"dist/bin.js"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","start":"node dist/bin.js"},"dependencies":{"@brainst0rm/relay":"0.1.0","@brainst0rm/sandbox":"0.1.0","@noble/ed25519":"^2.1.0","@noble/hashes":"^1.5.0","ws":"^8.18.0"},"devDependencies":{"@types/ws":"^8.5.12","@types/node":"^22.0.0","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"_id":"@brainst0rm/endpoint-stub@0.1.0","gitHead":"10e9a5392bcf59b26446ef84a942d5b89cecc491","bugs":{"url":"https://github.com/justinjilg/brainstorm/issues"},"homepage":"https://github.com/justinjilg/brainstorm#readme","_nodeVersion":"22.22.2","_npmVersion":"10.9.7","dist":{"integrity":"sha512-ceIoap9NPouWfLRrDi9VKRVjaAGsSFT1YJ/3bX2jTYXh4SA/OkHt6euq7487le3f1i4D0PW4L6XN4LkvT4BI9g==","shasum":"69420280396b4edaa89871efd436b85addc7d883","tarball":"https://registry.npmjs.org/@brainst0rm/endpoint-stub/-/endpoint-stub-0.1.0.tgz","fileCount":18,"unpackedSize":176804,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@brainst0rm%2fendpoint-stub@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGdaI36x9V7kRMJZJkq22e6HhgYe0hXNp0DpP2jlKASoAiAMXrBXNriAatlhngt5bTmg4bmiGv2DWmHLPEhMYMOiiQ=="}]},"_npmUser":{"name":"justinjilg","email":"justin.jilg@gmail.com"},"directories":{},"maintainers":[{"name":"justinjilg","email":"justin.jilg@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/endpoint-stub_0.1.0_1778931963020_0.39276517750122575"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-16T11:46:02.886Z","0.1.0":"2026-05-16T11:46:03.168Z","modified":"2026-05-16T11:46:03.547Z"},"maintainers":[{"name":"justinjilg","email":"justin.jilg@gmail.com"}],"description":"Reference implementation of the endpoint side of the Brainstorm dispatch protocol. Connects to a relay over WS, receives CommandEnvelopes, executes tools via a pluggable executor, emits CommandAck/ProgressEvent/CommandResult back. Used as a test fixture f","homepage":"https://github.com/justinjilg/brainstorm#readme","repository":{"type":"git","url":"git+https://github.com/justinjilg/brainstorm.git","directory":"packages/endpoint-stub"},"bugs":{"url":"https://github.com/justinjilg/brainstorm/issues"},"readme":"# @brainst0rm/endpoint-stub\n\nReference implementation of the **endpoint** side of the Brainstorm dispatch\nprotocol. Connects to a relay over WebSocket, receives `CommandEnvelope`\nframes, executes tools via a pluggable executor, and emits `CommandAck` /\n`ProgressEvent` / `CommandResult` back to the relay.\n\n## What this is for\n\n1. **Test fixture** for distributed dispatch flows (Stage 1.1+ in\n   `docs/endpoint-agent-plan.md`). Stand it up alongside `@brainst0rm/relay`\n   to exercise the full operator → relay → endpoint loop without needing\n   a real sandboxed agent.\n2. **Reference** for `crd4sdom`'s production `brainstorm-agent` (Go).\n   The TypeScript here pins the protocol semantics — `CommandAck` timing,\n   signature verification order, lifecycle transitions — that the Go\n   implementation must also satisfy.\n3. **Self-contained dev endpoint** for local `brainstorm dispatch` smoke\n   tests on a developer laptop.\n\n## What this is NOT\n\nThe stub is honest about being a stub:\n\n- No microVM sandbox isolation (P3 work in the production agent)\n- No real evidence-chain hashing of execution\n- No reset machinery between commands\n- No `GuestQuery` / `GuestResponse` integrity-monitor handling\n\nEvery result the stub produces includes `{ stub: true }` in its stdout JSON\nso consumers can immediately see they're not running against a real\nisolated endpoint.\n\n## Quick start\n\n```bash\n# 1. Start a relay (separate terminal)\nbrainstorm-relay\n\n# 2. Have an admin issue a bootstrap token via the relay's HTTP API\ncurl -X POST http://127.0.0.1:8444/v1/admin/endpoint/enroll \\\n  -H \"Authorization: Bearer $ADMIN_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"tenant_id\":\"tenant-dev\"}'\n# → { \"bootstrap_token\": \"...\", \"endpoint_id\": \"uuid-...\" }\n\n# 3. Run the stub\nexport BRAINSTORM_RELAY_URL_WS=ws://127.0.0.1:8443\nexport BRAINSTORM_RELAY_URL_HTTP=http://127.0.0.1:8444\nexport BRAINSTORM_ENDPOINT_BOOTSTRAP=...        # from step 2\nexport BRAINSTORM_ENDPOINT_TENANT_ID=tenant-dev\nexport BRAINSTORM_ENDPOINT_ID=...               # from step 2\nexport BRAINSTORM_ENDPOINT_TENANT_PUBKEY_HEX=... # tenant's signing pubkey\nbrainstorm-endpoint-stub\n```\n\nThe stub generates an Ed25519 keypair on first run and persists it to\n`~/.brainstorm/endpoint-stub/identity.json` (mode 0600). Subsequent runs\nreuse the keypair, so the relay continues to recognize it.\n\n## Programmatic usage\n\n```typescript\nimport { EndpointStub, type ToolExecutor } from \"@brainst0rm/endpoint-stub\";\n\nconst myExecutor: ToolExecutor = async (ctx) => {\n  return { exit_code: 0, stdout: `ran ${ctx.tool}`, stderr: \"\" };\n};\n\nconst stub = new EndpointStub({\n  relayUrl: \"ws://127.0.0.1:8443\",\n  tenantId: \"tenant-dev\",\n  identityPath: \"/tmp/my-endpoint.json\",\n  endpointId: \"uuid-...\",\n  tenantPublicKey: tenantPubKeyBytes,\n  executor: myExecutor,\n});\n\nawait stub.connect(); // EndpointHello + await EndpointHelloAck\nawait stub.run(); // Loop until close\n```\n\n`connect()` resolves once the session is established, so it's safe for an\noperator to immediately dispatch. `run()` resolves when the connection\ncloses.\n\n## Pluggable executor\n\nThe default `stubExecutor` echoes each command's params back as JSON. To\nexercise more interesting code paths, supply your own:\n\n```typescript\nconst echoExecutor: ToolExecutor = async (ctx) => {\n  // ctx: { command_id, tool, params, deadline_ms }\n  return { exit_code, stdout, stderr };\n};\n```\n\nReturning `exit_code !== 0` produces a `failed` `CommandResult` with code\n`SANDBOX_TOOL_ERROR`. Throwing an exception does the same with the error\nmessage in `error.message`.\n\n## Real CHV sandbox executor (`BSM_USE_CHV_EXECUTOR=1`)\n\nThe stub ships with a built-in `ChvSandboxExecutor` that wires the\npluggable executor seam to a real `ChvSandbox` from\n[`@brainst0rm/sandbox`](../sandbox). When you set\n`BSM_USE_CHV_EXECUTOR=1`, the bin constructs a `ChvSandboxExecutor`\nfrom the same env contract `first-light.sh` uses and hands it to the\n`EndpointStub` instead of the default echo-style `stubExecutor`.\n\n```bash\nexport BSM_USE_CHV_EXECUTOR=1\nexport BSM_KERNEL=/srv/bsm/sandbox/bsm-sandbox-kernel\nexport BSM_INITRAMFS=/srv/bsm/sandbox/bsm-sandbox-initramfs   # if modular kernel\nexport BSM_ROOTFS=/srv/bsm/sandbox/bsm-sandbox-rootfs.img\nexport BSM_VSOCK_SOCKET=/tmp/bsm-endpoint-stub.sock           # default\nexport BSM_API_SOCKET=/tmp/bsm-endpoint-stub-api.sock         # default\nexport BSM_GUEST_PORT=52000                                   # default; matches image-builder vsock-init\n# optional: BSM_CH_BIN, BSM_CHREMOTE_BIN to override PATH lookup\n\n# everything below is the standard stub config — unchanged\nexport BRAINSTORM_RELAY_URL_WS=ws://127.0.0.1:8443\nexport BRAINSTORM_RELAY_URL_HTTP=http://127.0.0.1:8444\nexport BRAINSTORM_ENDPOINT_BOOTSTRAP=...\nexport BRAINSTORM_ENDPOINT_TENANT_ID=tenant-dev\nexport BRAINSTORM_ENDPOINT_ID=...\nexport BRAINSTORM_ENDPOINT_TENANT_PUBKEY_HEX=...\n\nbrainstorm-endpoint-stub\n```\n\nWhen the env var is unset (or any value other than `\"1\"`), the stub\nfalls back to `stubExecutor` — the existing echo-back behaviour. So\nturning the real sandbox on and off is a single env flip; nothing else\nchanges about the stub's wiring.\n\n### Honest cost: cold-boot-per-dispatch (~600ms latency floor)\n\nThe MVP picks the simpler of the two patterns from the design space:\n\n- **Cold-boot-per-dispatch** (what's shipped): boot a fresh `ChvSandbox`\n  per command, `executeTool`, `shutdown`. ~600ms latency floor on\n  Hetzner node-2 per PR #277. Zero steady-state RAM. No\n  shared-state-between-tools concerns. Failure modes are local — a\n  boot failure on one dispatch does not poison subsequent dispatches.\n- **Pool of N pre-booted sandboxes** (deferred): take from pool →\n  `executeTool` → `reset` → return to pool. ~2-30ms per dispatch\n  (matches the steady-state numbers in PR #277). Higher steady-state\n  RAM. Adds reset machinery on the critical path. We're holding off\n  until we have real dispatch-rate data to size the pool.\n\nOperators dispatching many commands in tight succession will feel the\n600ms floor. If your workload is sub-100ms-sensitive, do not enable\n`BSM_USE_CHV_EXECUTOR=1` until the pool variant lands.\n\n### Error mapping (executor → operator)\n\n| Sandbox event                  | `ToolExecutorResult.exit_code` | `stderr`                                   | EndpointStub maps to                     |\n| ------------------------------ | ------------------------------ | ------------------------------------------ | ---------------------------------------- |\n| `boot()` throws                | `126`                          | `chv-executor: sandbox boot failed: …`     | `failed` / `SANDBOX_TOOL_ERROR`          |\n| `executeTool()` throws         | `125`                          | `chv-executor: sandbox executeTool failed` | `failed` / `SANDBOX_TOOL_ERROR`          |\n| `executeTool()` exit_code != 0 | preserved (faithful)           | preserved (faithful)                       | `failed` / `SANDBOX_TOOL_ERROR`          |\n| `shutdown()` throws            | n/a — logged + swallowed       | n/a                                        | result already produced; not re-reported |\n\n`shutdown()` always runs, even on the boot-failure path (the `Sandbox`\ninterface documents `shutdown()` as idempotent).\n\n### Programmatic usage of the executor\n\n```typescript\nimport { ChvSandboxExecutor, EndpointStub } from \"@brainst0rm/endpoint-stub\";\n\nconst executor = new ChvSandboxExecutor({\n  config: {\n    apiSocketPath: \"/tmp/api.sock\",\n    kernel: { path: \"/srv/bsm/sandbox/bsm-sandbox-kernel\" },\n    rootfs: { path: \"/srv/bsm/sandbox/bsm-sandbox-rootfs.img\" },\n    vsock: { socketPath: \"/tmp/vsock.sock\", guestPort: 52000 },\n  },\n});\n\nconst stub = new EndpointStub({\n  // ...\n  executor: executor.execute,\n});\n```\n\n### Honest gaps in the executor\n\n- **Per-tool timeout above the sandbox's `deadline_ms`**: the executor\n  does not add a parallel wall-clock fence; the sandbox itself enforces\n  the deadline. If the sandbox's deadline machinery wedges, the\n  executor will wait with it.\n- **Queueing under load**: 10 simultaneous dispatches → 10 parallel\n  cold boots. Relay-side serialisation is the current backstop.\n- **Shared image-pool / page-cache priming**: every boot reads kernel\n  - initramfs + rootfs from disk. A `posix_fadvise(WILLNEED)` warmer\n    or shared image cache would reduce IO under burst.\n- **Reset between commands**: cold-boot-per-dispatch makes reset moot\n  — each command gets a fresh guest. The pool variant will need to\n  call `reset()` between dispatches.\n\n## Protocol contract enforced\n\nThe stub verifies, in order, before executing any tool:\n\n1. **Ed25519 signature** on the `CommandEnvelope` against the configured\n   `tenantPublicKey` (per `ed25519-jcs-sha256-v1`).\n2. **Audience — endpoint**: `target_endpoint_id` must equal this stub's\n   `endpoint_id` (F5: cross-endpoint envelope replay defense).\n3. **Audience — tenant**: `tenant_id` must match the stub's tenant.\n4. **Session epoch**: `session_id` must match the current connection's\n   session (F12: relay-restart stale-session defense).\n5. **Time skew**: `issued_at` must be within ±60 s of the endpoint's\n   wall clock.\n6. **Expiry**: `expires_at` must be in the future.\n7. **Lifetime cap**: `expires_at − issued_at` must not exceed 5 min.\n8. **Nonce uniqueness** (in-memory only — see \"out of scope\" below):\n   the same nonce cannot be replayed within a single stub process.\n\nA failure emits an `ErrorEvent` with one of:\n`ENDPOINT_SIGNATURE_INVALID`, `ENDPOINT_WRONG_AUDIENCE`,\n`ENDPOINT_SESSION_STALE`, `ENDPOINT_ENVELOPE_EXPIRED`,\n`ENDPOINT_NONCE_REPLAY`.\n\nAfter verification the stub sends `CommandAck` _before_ invoking the\nexecutor, matching the protocol's `dispatched → started` transition\ncontract.\n\n### Explicit out-of-scope (production agent's job)\n\n- **Persistent nonce store** that survives restart. The stub uses an\n  in-memory `Set<string>`; a process restart resets it. The production\n  agent must use a SQLite-backed nonce store with a `NONCE_CACHE_FULL`\n  fail-closed policy.\n- **`signing_key_id` lookup / revocation**. The stub trusts the single\n  `tenantPublicKey` passed in. Production must look up the key by\n  `signing_key_id` and check a revocation list.\n- **Atomic identity-file writes**. `loadOrCreateIdentity` uses\n  `writeFileSync(..., { mode: 0o600 })` — there is no temp-file +\n  rename. A crash between `writeFileSync` start and OS sync could\n  leave a partial JSON file. The threat model accepts this for the\n  laptop-loopback host.\n\n## Tests\n\n```bash\nnpm test --workspace=@brainst0rm/endpoint-stub\n```\n\nTests stand up a real relay (WebSocket + enrollment HTTP) on loopback,\npoint a real `EndpointStub` at it, drive a dispatch from a fake operator,\nand verify all 7 protocol-correctness invariants end-to-end.\n","readmeFilename":"README.md","_rev":"1-043a0b9c16a4d244da50270202427cb9"}