@sleep2agi/commhub-server 0.9.0-preview.3 → 0.9.0-preview.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +125 -7
  2. package/bin/commhub.ts +10 -2
  3. package/package.json +5 -5
  4. package/src/ack-create-request-transport.test.ts +159 -0
  5. package/src/ack-create-request.test.ts +331 -0
  6. package/src/admin-networks-http.test.ts +84 -0
  7. package/src/alias-filter.test.ts +72 -0
  8. package/src/alias-filter.ts +32 -0
  9. package/src/api-host-supervisors-fallback.test.ts +267 -0
  10. package/src/api-nodes-shape.test.ts +188 -0
  11. package/src/auth.ts +93 -14
  12. package/src/avatar-validate.test.ts +163 -0
  13. package/src/avatar-validate.ts +103 -0
  14. package/src/boot-side-effect.test.ts +124 -0
  15. package/src/config-apply-sec1.test.ts +107 -0
  16. package/src/config-apply-validate.test.ts +92 -0
  17. package/src/config-apply-validate.ts +81 -3
  18. package/src/create-node.ts +48 -1
  19. package/src/dashboard-slash-routing-http.test.ts +103 -0
  20. package/src/db-adapter-guard.test.ts +85 -0
  21. package/src/db-adapter.ts +64 -23
  22. package/src/db.ts +522 -32
  23. package/src/ext-token-shared.test.ts +60 -0
  24. package/src/ext-validation-regression.test.ts +131 -0
  25. package/src/external-schedule-edits-http.test.ts +316 -0
  26. package/src/external-schedule-edits.ts +314 -0
  27. package/src/file-download-authz-dev-open.test.ts +135 -0
  28. package/src/file-download-authz.test.ts +403 -0
  29. package/src/file-download-date-bucket.test.ts +436 -0
  30. package/src/file-network-scope-dev-open.test.ts +97 -0
  31. package/src/file-network-scope.test.ts +731 -0
  32. package/src/health-redaction.test.ts +637 -0
  33. package/src/index.ts +15 -2172
  34. package/src/lifecycle-guard.ts +50 -0
  35. package/src/list-host-supervisors.test.ts +314 -0
  36. package/src/mcp-write-network-resolution.test.ts +429 -0
  37. package/src/network-name-http.test.ts +71 -0
  38. package/src/network-scope.ts +101 -0
  39. package/src/node-attrs-validate.ts +88 -0
  40. package/src/node-attrs.test.ts +213 -0
  41. package/src/observer-avatar-http.test.ts +349 -0
  42. package/src/observer-push.test.ts +176 -0
  43. package/src/peer-reply-atomic.test.ts +455 -0
  44. package/src/peer-reply-capability.test.ts +70 -0
  45. package/src/probe-validate.test.ts +221 -0
  46. package/src/probe-validate.ts +104 -0
  47. package/src/probe.test.ts +80 -0
  48. package/src/probe.ts +200 -0
  49. package/src/push.ts +96 -7
  50. package/src/require-explicit-test-db.ts +9 -0
  51. package/src/resolve-port.test.ts +57 -0
  52. package/src/resolve-port.ts +44 -0
  53. package/src/response-charset.test.ts +130 -0
  54. package/src/rest-explicit-columns-http.test.ts +211 -0
  55. package/src/rest-identity.test.ts +129 -0
  56. package/src/rest-identity.ts +100 -0
  57. package/src/rest-projections.ts +67 -0
  58. package/src/rest-write-network-resolution.test.ts +133 -0
  59. package/src/runtime-label.test.ts +22 -0
  60. package/src/runtime-label.ts +21 -0
  61. package/src/scheduled-run-terminal.test.ts +268 -0
  62. package/src/scheduled-tasks-http.test.ts +479 -0
  63. package/src/scheduled-tasks.ts +549 -0
  64. package/src/send-reply-agent-warning.test.ts +202 -0
  65. package/src/send-reply-attachments.test.ts +437 -0
  66. package/src/server.ts +3492 -0
  67. package/src/shared/external-schedule-contract.ts +60 -0
  68. package/src/shared/probe-host-allowlist-drift.test.ts +54 -0
  69. package/src/shared/probe-host-allowlist.ts +128 -0
  70. package/src/side-thread-command-transport.test.ts +140 -0
  71. package/src/side-thread-command-transport.ts +309 -0
  72. package/src/side-thread-http-integration.test.ts +229 -0
  73. package/src/side-thread-http.ts +504 -0
  74. package/src/side-thread.test.ts +1038 -0
  75. package/src/side-thread.ts +2241 -0
  76. package/src/skillhub-http.test.ts +88 -0
  77. package/src/stop-delete-node.test.ts +720 -0
  78. package/src/task-auth-origin-http.test.ts +87 -0
  79. package/src/task-auth-origin.test.ts +24 -0
  80. package/src/task-auth-origin.ts +7 -0
  81. package/src/task-consumption.test.ts +398 -0
  82. package/src/task-diagnostic-http.test.ts +145 -0
  83. package/src/task-diagnostic.test.ts +115 -0
  84. package/src/task-diagnostic.ts +118 -0
  85. package/src/task-idempotency-mcp.test.ts +142 -0
  86. package/src/task-idempotency.test.ts +60 -0
  87. package/src/task-idempotency.ts +75 -0
  88. package/src/task-lifecycle-watcher.test.ts +200 -0
  89. package/src/task-lifecycle-watcher.ts +49 -0
  90. package/src/task-network-resolution-http.test.ts +137 -0
  91. package/src/tasks-pagination-http.test.ts +137 -0
  92. package/src/tools.ts +2302 -169
  93. package/src/update-provider.test.ts +234 -0
  94. package/src/upload-early-reject-guard.test.ts +155 -0
  95. package/src/uploads-http.test.ts +92 -14
  96. package/src/uploads.test.ts +66 -0
  97. package/src/uploads.ts +130 -5
  98. package/src/vault.test.ts +200 -0
  99. package/src/vault.ts +156 -0
package/README.md CHANGED
@@ -7,10 +7,27 @@
7
7
 
8
8
  CommHub: MCP Streamable HTTP + SSE push + REST API for an AI agent network. Single-process Bun server, SQLite-backed, zero config when launched through `anet`.
9
9
 
10
- The supported path is to install the `anet` CLI (`@sleep2agi/agent-network`, currently v2.2.10 at v0.10.11) and run `anet hub start`, which wires up the port, default admin account, recovery admin `utok_`, and local config for you.
10
+ The supported path is to install the `anet` CLI (`@sleep2agi/agent-network`) and run `anet hub start`, which wires up the port, default admin account, recovery admin `utok_`, and local config for you.
11
11
 
12
12
  ## Quick start (verified)
13
13
 
14
+ > ⚠️ **Do not run `bunx @sleep2agi/commhub-server` without a version**
15
+ > Without a version this resolves to the npm `latest` dist-tag, and **`latest` is currently `0.8.8` (published 2026-06-24)**.
16
+ >
17
+ > On `0.8.8` an anonymous `GET /health` **returns the `{networkId}:{alias}` of every live SSE connection** — no token required.
18
+ > The redaction fix is [`7bacb729`](https://github.com/sleep2agi/agent-network/commit/7bacb729) (`security(#473)`, **2026-07-29**),
19
+ > **35 days after** `0.8.8` shipped, so `0.8.8` does not contain it.
20
+ >
21
+ > **Pin a version, or use the preview channel:**
22
+ >
23
+ > ```bash
24
+ > bunx --bun @sleep2agi/commhub-server@preview # contains the redaction fix
25
+ > ```
26
+ >
27
+ > Or use the supported path, `anet hub start`, which pulls the version in `PINNED_SERVER_VERSION` rather than `latest`.
28
+ >
29
+ > Self-check: `curl -sS http://<host>:9200/health | jq 'has("sse_sessions")'` — `true` means you are on an affected build.
30
+
14
31
  ```bash
15
32
  # Recommended — through the anet CLI
16
33
  npm install -g @sleep2agi/agent-network
@@ -38,13 +55,16 @@ Once running:
38
55
 
39
56
  ## Pairs with
40
57
 
41
- | Package | Version |
58
+ | Package | |
42
59
  |---|---|
43
- | [`@sleep2agi/agent-network`](https://www.npmjs.com/package/@sleep2agi/agent-network) | 2.1.7 |
44
- | [`@sleep2agi/agent-network-dashboard`](https://www.npmjs.com/package/@sleep2agi/agent-network-dashboard) | 0.4.2 |
45
- | [`@sleep2agi/agent-node`](https://www.npmjs.com/package/@sleep2agi/agent-node) | 2.3.0 |
60
+ | [`@sleep2agi/agent-network`](https://www.npmjs.com/package/@sleep2agi/agent-network) | CLI |
61
+ | [`@sleep2agi/agent-network-dashboard`](https://www.npmjs.com/package/@sleep2agi/agent-network-dashboard) | Web UI |
62
+ | [`@sleep2agi/agent-node`](https://www.npmjs.com/package/@sleep2agi/agent-node) | Agent runtime host |
63
+
64
+ Install the matching versions with `npm install -g <pkg>` — the `latest` dist-tag on npm is
65
+ authoritative. Pinning versions here goes stale on every release and nobody comes back to fix it.
46
66
 
47
- ## MCP tools (17)
67
+ ## MCP tools
48
68
 
49
69
  ### Agent-side
50
70
  | Tool | Description |
@@ -71,6 +91,8 @@ Once running:
71
91
  | `list_tasks` | Task list, filterable by `network_id` |
72
92
  | `get_completions` | Completion history |
73
93
 
94
+ > The table above lists the 17 **collaboration-core** tools. Node lifecycle / provider ops tools ship on the same MCP surface — the authoritative full list is [docs-site/docs/api/mcp-tools.md](../docs-site/docs/api/mcp-tools.md). Don't read the count above as "17 tools total".
95
+
74
96
  ## REST API
75
97
 
76
98
  The server exposes ~33 endpoints across health, auth, networks, and observability surfaces. The endpoints in use today by the verified flow are:
@@ -96,7 +118,7 @@ Network-management endpoints (`/api/networks…`) are present and used by the cu
96
118
 
97
119
  Auth: `Authorization: Bearer <token>` header, or `?token=<token>` query.
98
120
 
99
- ## SQLite schema (13 tables)
121
+ ## SQLite schema
100
122
 
101
123
  Auto-created on first run.
102
124
 
@@ -146,6 +168,102 @@ DATABASE_URL=postgres://user:pass@host:5432/commhub bunx @sleep2agi/commhub-serv
146
168
  | `COMMHUB_DB` | `~/.commhub/commhub.db` | SQLite path |
147
169
  | `DATABASE_URL` | (none) | switches to PostgreSQL when set (unverified) |
148
170
 
171
+ ## Running the unit tests
172
+
173
+ ### 先说该走哪条路:Docker
174
+
175
+ 仓库规约是测试在 Docker 里跑(`CLAUDE.md`「所有测试在 Docker 里跑:不碰本地
176
+ 环境,不改生产」)。`server/src` 的测试在 `tests/` 下已经有 **24 个**按主题分片的
177
+ 套件覆盖(实测:遍历 main 上 204 个套件目录,run.sh 里跑 `server/src/*.test.ts`
178
+ 的有 24 个),每个都在容器内自带 `COMMHUB_DB`。跑其中任意一个都是这个形状:
179
+
180
+ ```bash
181
+ # 从仓根跑
182
+ docker build --build-arg SOURCE_COMMIT="$(git rev-parse HEAD)" \
183
+ -t anet-test624 -f tests/test624-task-cursor-pagination/Dockerfile .
184
+ docker run --rm anet-test624
185
+ ```
186
+
187
+ (实跑过:退出码 0,`6 pass / 0 fail`,`RESULT: PASS`。换成 `tests/` 下别的
188
+ `test*-*/Dockerfile` 同理,但 **build-arg 的名字要照抄那个 Dockerfile** ——
189
+ main 上 66 处写的是 `SOURCE_COMMIT`,另有若干写成 `TEST<编号>_SOURCE_COMMIT`
190
+ (如 `TEST686_SOURCE_COMMIT`、`TEST765_SOURCE_COMMIT`)。传错名字不会报错,
191
+ 只会让套件收到 `unknown` 然后按它自己的规则红或绿 —— 这不是你想要的那种绿。)
192
+
193
+ > ⚠️ **目前没有一个套件跑「`server/src` 下的全部」单测。** 上面那些是按主题
194
+ > 分片的;聚合门在 #798 里,尚未合入 main。所以「所有分片套件都绿」**不等于**
195
+ > 69 个单测文件都跑过 —— 在 #798 合入之前,不要把前者当成后者。
196
+
197
+ ### 直接在宿主上跑时的两条契约
198
+
199
+ 下面这两条是上面那些套件在容器内**已经替你满足**的东西。你只有在本地迭代
200
+ 单个文件时才需要自己处理它们 —— 而那种跑法不构成任何门禁证据。
201
+
202
+
203
+ 1. **`COMMHUB_DB` must be set.** It is not optional — `~/.commhub/commhub.db` is a
204
+ *live* database on any machine that has ever run a hub. This one is enforced:
205
+ with `NODE_ENV=test` (which `bun test` sets) and no `COMMHUB_DB`, the adapter
206
+ refuses with `REFUSING to open the default SQLite database`.
207
+
208
+ 2. **Each test *file* needs its own database.** This is *not* enforced — a single
209
+ shared `COMMHUB_DB` passes the guard above and then fails a handful of tests
210
+ for reasons that look like product bugs but are not. `scripts/qa.sh` encodes
211
+ the real contract:
212
+
213
+ ```bash
214
+ # 从仓根跑(见下),每个文件一个全新的库
215
+ f=server/src/auth-validate.test.ts
216
+ db=/tmp/anet-$(basename "$f" .test.ts).db
217
+ rm -f "$db" # 复用上一次的库同样会破坏隔离
218
+ COMMHUB_DB="$db" bun test "$f"
219
+ ```
220
+
221
+ 整套跑法:
222
+
223
+ ```bash
224
+ rc=0
225
+ # 用 find 而不是 `server/src/*.test.ts` —— glob 不进子目录,会漏掉
226
+ # server/src/shared/ 下的 3 个(实测:深度 1 是 66 个,递归是 69 个)。
227
+ # scripts/qa.sh 与 test798 用的就是 find,这里保持一致。
228
+ while IFS= read -r f; do
229
+ # 用完整路径派生库名,不用 basename:加入子目录之后,
230
+ # 两个不同目录下的同名文件会派生出同一个库(原来只扫单层时不可能)。
231
+ db=/tmp/anet-$(printf '%s' "${f%.test.ts}" | tr '/.' '--').db
232
+ rm -f "$db" "$db-wal" "$db-shm"
233
+ COMMHUB_DB="$db" bun test "$f" || { echo "FAILED: $f"; rc=1; }
234
+ done < <(find server/src -type f -name '*.test.ts' | sort)
235
+ [ "$rc" -eq 0 ]
236
+ ```
237
+
238
+ ⚠️ 最后那行 `[ "$rc" -eq 0 ]` 不是装饰,是这段的**门**。写成
239
+ `bun test "$f" || echo "FAILED: $f"` 会让循环打印一行然后继续,
240
+ 整段的退出码恒为 0 —— **测试全红,脚本报绿**。用 `[ ... ]` 而不是
241
+ `exit "$rc"`,是为了这段直接粘进终端时不会把你的 shell 关掉;
242
+ 存成脚本跑时,它是最后一条命令,脚本退出码就等于它。
243
+
244
+ ⚠️ 别把 `$name` 之类的占位符原样抄进命令行 —— 未定义的变量会展开成空串,
245
+ 于是每个文件都写进**同一个** `/tmp/…-.db`,正好是这一节要避免的事。
246
+
247
+ Symptom if you ignore it: `bun test src/` under one database turns
248
+ `admin-networks-http` and `scheduled-tasks-http` red. Both are **global-count
249
+ assertions that are correct under the contract**:
250
+
251
+ ```ts
252
+ // admin-networks-http.test.ts — the admin sees EXACTLY this file's two networks
253
+ expect(new Set(rows.map((row) => row.network_id)))
254
+ .toEqual(new Set([adminNetworkId, memberNetworkId]));
255
+
256
+ // scheduled-tasks-http.test.ts — EXACTLY one due occurrence was processed
257
+ expect(runDueScheduledTasks().processed).toBe(1);
258
+ ```
259
+
260
+ Share a database and other files' networks and due schedules join the count.
261
+ Nothing is broken; the run violated the contract.
262
+
263
+ `scripts/qa.sh` 的 L0 目前只点名跑 5 个 server 测试(白名单),不是全量;
264
+ 把 `server/src` 全域逐文件跑起来的那道 CI 门还在评审中(`#798`),
265
+ 未进 `main` —— 所以本节描述的是**契约**,不是「已经有东西在替你执行它」。
266
+
149
267
  ## Auth modes
150
268
 
151
269
  1. **V3 user system (default)** — `POST /api/auth/register` and `/api/auth/login` issue `utok_…` tokens; nodes get `ntok_…`.
package/bin/commhub.ts CHANGED
@@ -54,5 +54,13 @@ Examples:
54
54
  }
55
55
  }
56
56
 
57
- // Load the server
58
- import("../src/index.js");
57
+ // Opt into the default production DB path before loading the server graph.
58
+ // Ordinary imports / bun -e probes do not receive this capability.
59
+ process.env.COMMHUB_SERVER = "1";
60
+
61
+ // Load the server module (side-effect-free since the #438 corrective:
62
+ // importing binds nothing) and start the hub EXPLICITLY. Never rely on
63
+ // import side effects or import.meta.main here — this bin is a dynamic
64
+ // importer, where import.meta.main is false inside the module.
65
+ const { startHub } = await import("../src/server.js");
66
+ startHub();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.3",
4
- "description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
3
+ "version": "0.9.0-preview.30",
4
+ "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
@@ -12,9 +12,9 @@
12
12
  "bin"
13
13
  ],
14
14
  "scripts": {
15
- "start": "bun run src/index.ts",
16
- "dev": "bun --watch run src/index.ts",
17
- "test": "COMMHUB_DB=/tmp/test-commhub-$$.db bun test src/"
15
+ "start": "COMMHUB_SERVER=1 bun run src/index.ts",
16
+ "dev": "COMMHUB_SERVER=1 bun --watch run src/index.ts",
17
+ "test": "bun run scripts/test-aggregate.ts"
18
18
  },
19
19
  "keywords": [
20
20
  "commhub",
@@ -0,0 +1,159 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { db } from "./db.js";
6
+ import { registerTools } from "./tools.js";
7
+
8
+ const NET = "net_ack_transport";
9
+ const DAEMON_NODE_ID = "node_ack_transport_daemon";
10
+ const DAEMON_ALIAS = "ack-transport-daemon";
11
+ const DAEMON_USER = "u_ack_transport_owner";
12
+ const DAEMON_TOKEN_ID = "tok_ack_transport_daemon";
13
+
14
+ function cleanup() {
15
+ try { db.run("DELETE FROM audit_log WHERE network_id = ?1", [NET]); } catch {}
16
+ try { db.run("DELETE FROM node_create_requests WHERE network_id = ?1", [NET]); } catch {}
17
+ try { db.run("DELETE FROM nodes WHERE network_id = ?1", [NET]); } catch {}
18
+ try { db.run("DELETE FROM api_tokens WHERE network_id = ?1", [NET]); } catch {}
19
+ try { db.run("DELETE FROM network_members WHERE network_id = ?1", [NET]); } catch {}
20
+ try { db.run("DELETE FROM networks WHERE network_id = ?1", [NET]); } catch {}
21
+ try { db.run("DELETE FROM users WHERE user_id = ?1", [DAEMON_USER]); } catch {}
22
+ }
23
+
24
+ function seedWorld() {
25
+ db.run(
26
+ `INSERT INTO users (user_id, username, password_hash, role, created_at)
27
+ VALUES (?1, ?2, 'x', 'user', datetime('now'))`,
28
+ [DAEMON_USER, DAEMON_USER],
29
+ );
30
+ db.run(
31
+ `INSERT INTO networks (network_id, network_name, owner_id, created_at)
32
+ VALUES (?1, ?2, ?3, datetime('now'))`,
33
+ [NET, NET, DAEMON_USER],
34
+ );
35
+ db.run(
36
+ `INSERT INTO network_members (user_id, network_id, role, joined_at)
37
+ VALUES (?1, ?2, 'owner', datetime('now'))`,
38
+ [DAEMON_USER, NET],
39
+ );
40
+ db.run(
41
+ `INSERT INTO nodes (node_id, node_name, alias, network_id, created_at, updated_at)
42
+ VALUES (?1, ?2, ?3, ?4, datetime('now'), datetime('now'))`,
43
+ [DAEMON_NODE_ID, DAEMON_ALIAS, DAEMON_ALIAS, NET],
44
+ );
45
+ db.run(
46
+ `INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
47
+ VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
48
+ [DAEMON_TOKEN_ID, DAEMON_USER, NET, `node:${DAEMON_ALIAS}`, "hash_ack_transport_daemon"],
49
+ );
50
+ }
51
+
52
+ function seedRequest(requestId: string) {
53
+ db.run(
54
+ `INSERT INTO node_create_requests
55
+ (request_id, daemon_node_id, child_name, network_id, runtime, model, flags_json, env_keys, status, created_at, created_by_token)
56
+ VALUES (?1, ?2, ?3, ?4, 'codex-sdk', 'x', '{}', '[]', 'delivered', ?5, ?6)`,
57
+ [requestId, DAEMON_NODE_ID, `child_${requestId}`, NET, Date.now(), DAEMON_TOKEN_ID],
58
+ );
59
+ }
60
+
61
+ async function connectClient() {
62
+ const server = new McpServer({ name: "ack-transport-test", version: "1" });
63
+ registerTools(
64
+ server,
65
+ undefined,
66
+ NET,
67
+ DAEMON_USER,
68
+ DAEMON_ALIAS,
69
+ true,
70
+ DAEMON_TOKEN_ID,
71
+ );
72
+ const client = new Client({ name: "ack-transport-client", version: "1" });
73
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
74
+ await server.connect(serverTransport);
75
+ await client.connect(clientTransport);
76
+ return { client, server };
77
+ }
78
+
79
+ type ToolResult = { content: Array<{ type: string; text?: string }>; isError?: boolean };
80
+
81
+ function responseJson(result: ToolResult) {
82
+ const first = result.content[0];
83
+ if (!first || first.type !== "text" || typeof first.text !== "string") throw new Error("missing text tool result");
84
+ return JSON.parse(first.text) as { ok?: boolean; status?: string };
85
+ }
86
+
87
+ function expectTransportValidationError(result: ToolResult) {
88
+ expect(result.isError).toBe(true);
89
+ const first = result.content[0];
90
+ expect(first?.type).toBe("text");
91
+ expect(first?.text).toMatch(/Input validation error.*ack_create_request/i);
92
+ }
93
+
94
+ beforeEach(() => {
95
+ cleanup();
96
+ seedWorld();
97
+ });
98
+ afterAll(cleanup);
99
+
100
+ describe("#344 ack_create_request real in-process MCP transport", () => {
101
+ test("accepts runtime_capability_check_failed plus string runtime through the SDK zod gate", async () => {
102
+ const requestId = "cr_transport_accept";
103
+ seedRequest(requestId);
104
+ const { client, server } = await connectClient();
105
+ try {
106
+ const result = await client.callTool({
107
+ name: "ack_create_request",
108
+ arguments: {
109
+ request_id: requestId,
110
+ status: "runtime_capability_check_failed",
111
+ error: "child died within 5000ms post-spawn",
112
+ runtime: "codex-sdk",
113
+ },
114
+ });
115
+ expect(responseJson(result)).toMatchObject({ ok: true, status: "runtime_capability_check_failed" });
116
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
117
+ .toBe("runtime_capability_check_failed");
118
+ } finally {
119
+ await client.close();
120
+ await server.close();
121
+ }
122
+ });
123
+
124
+ test("rejects unknown status at the transport gate before the handler mutates the row", async () => {
125
+ const requestId = "cr_transport_bad_status";
126
+ seedRequest(requestId);
127
+ const { client, server } = await connectClient();
128
+ try {
129
+ const result = await client.callTool({
130
+ name: "ack_create_request",
131
+ arguments: { request_id: requestId, status: "capability_failed_typo", runtime: "codex-sdk" },
132
+ });
133
+ expectTransportValidationError(result);
134
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
135
+ .toBe("delivered");
136
+ } finally {
137
+ await client.close();
138
+ await server.close();
139
+ }
140
+ });
141
+
142
+ test("rejects non-string runtime at the transport gate before the handler mutates the row", async () => {
143
+ const requestId = "cr_transport_bad_runtime";
144
+ seedRequest(requestId);
145
+ const { client, server } = await connectClient();
146
+ try {
147
+ const result = await client.callTool({
148
+ name: "ack_create_request",
149
+ arguments: { request_id: requestId, status: "runtime_capability_check_failed", runtime: 42 },
150
+ });
151
+ expectTransportValidationError(result);
152
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
153
+ .toBe("delivered");
154
+ } finally {
155
+ await client.close();
156
+ await server.close();
157
+ }
158
+ });
159
+ });