@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.131

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 (318) hide show
  1. package/README.md +130 -10
  2. package/bin/commhub.ts +99 -11
  3. package/package.json +7 -7
  4. package/src/ack-create-request-transport.test.ts +182 -0
  5. package/src/ack-create-request.test.ts +50 -7
  6. package/src/admin-delete-network-http.test.ts +228 -0
  7. package/src/admin-networks-http.test.ts +88 -0
  8. package/src/adoption-candidates-http.test.ts +137 -0
  9. package/src/adoption-candidates.ts +158 -0
  10. package/src/agent-access-batch.test.ts +210 -0
  11. package/src/agent-access.ts +487 -0
  12. package/src/agent-acl-groups-http.test.ts +247 -0
  13. package/src/agent-acl-http.test.ts +659 -0
  14. package/src/agent-acl-slice3-http.test.ts +267 -0
  15. package/src/agent-groups-perf.test.ts +223 -0
  16. package/src/agent-notice.ts +48 -0
  17. package/src/agent-team-whoami-http.test.ts +98 -0
  18. package/src/agent-teams-http.test.ts +323 -0
  19. package/src/agent-teams.ts +244 -0
  20. package/src/alias-filter.test.ts +72 -0
  21. package/src/alias-filter.ts +42 -0
  22. package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
  23. package/src/api-host-supervisors-fallback.test.ts +271 -0
  24. package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
  25. package/src/api-nodes-shape.test.ts +26 -10
  26. package/src/auth-password-change-cli-tokens-http.test.ts +294 -0
  27. package/src/auth-sessions-http.test.ts +250 -0
  28. package/src/auth-validate.test.ts +5 -1
  29. package/src/auth-weak-password-login.test.ts +176 -0
  30. package/src/auth.ts +598 -49
  31. package/src/avatar-validate.test.ts +163 -0
  32. package/src/avatar-validate.ts +103 -0
  33. package/src/binding-generation-http.test.ts +121 -0
  34. package/src/blocked-exit-on-reply.test.ts +122 -0
  35. package/src/boot-side-effect.test.ts +124 -0
  36. package/src/clone-node.test.ts +248 -0
  37. package/src/clone-node.ts +289 -0
  38. package/src/codex-config-flags.ts +77 -0
  39. package/src/codex-fork-contract.ts +23 -0
  40. package/src/codex-fork-migration.test.ts +38 -0
  41. package/src/commhub-bin-args.test.ts +85 -0
  42. package/src/config-apply-sec1.test.ts +107 -0
  43. package/src/config-apply-validate.test.ts +134 -1
  44. package/src/config-apply-validate.ts +111 -5
  45. package/src/create-node-blocked-gate.test.ts +356 -0
  46. package/src/create-node-network-id.test.ts +79 -0
  47. package/src/create-node-tool-schema.test.ts +48 -0
  48. package/src/create-node-unicode-name.test.ts +120 -0
  49. package/src/create-node-validate.test.ts +193 -7
  50. package/src/create-node-validate.ts +227 -15
  51. package/src/create-node-workdir.test.ts +289 -0
  52. package/src/create-node.ts +77 -23
  53. package/src/daemon-provider-snapshots.test.ts +421 -0
  54. package/src/daemon-provider-snapshots.ts +327 -0
  55. package/src/daemon-token-owner-http.test.ts +236 -0
  56. package/src/dashboard-slash-routing-http.test.ts +107 -0
  57. package/src/db-adapter-guard.test.ts +148 -0
  58. package/src/db-adapter-translate.test.ts +128 -0
  59. package/src/db-adapter.ts +323 -87
  60. package/src/db-timestamp.test.ts +38 -0
  61. package/src/db-timestamp.ts +15 -0
  62. package/src/db.ts +933 -36
  63. package/src/delivered-without-push.test.ts +140 -0
  64. package/src/department-groups-http.test.ts +209 -0
  65. package/src/department-groups-sync-http.test.ts +328 -0
  66. package/src/department-groups.ts +434 -0
  67. package/src/department-heads-http.test.ts +330 -0
  68. package/src/department-heads.ts +226 -0
  69. package/src/departments-http.test.ts +185 -0
  70. package/src/departments.ts +260 -0
  71. package/src/desktop-message-seam.test.ts +150 -0
  72. package/src/desktop-message.test.ts +368 -0
  73. package/src/dm-file-scope-http.test.ts +178 -0
  74. package/src/ext-token-shared.test.ts +60 -0
  75. package/src/ext-validation-regression.test.ts +131 -0
  76. package/src/external-schedule-edits-http.test.ts +321 -0
  77. package/src/external-schedule-edits-timestamps.test.ts +64 -0
  78. package/src/external-schedule-edits.ts +327 -0
  79. package/src/file-download-authz-dev-open.test.ts +135 -0
  80. package/src/file-download-authz.test.ts +404 -0
  81. package/src/file-download-date-bucket.test.ts +436 -0
  82. package/src/file-network-scope-dev-open.test.ts +97 -0
  83. package/src/file-network-scope.test.ts +732 -0
  84. package/src/group-membership-events-http.test.ts +264 -0
  85. package/src/group-messages-http.test.ts +482 -0
  86. package/src/group-messages.ts +297 -0
  87. package/src/health-redaction.test.ts +642 -0
  88. package/src/http-gzip.test.ts +94 -0
  89. package/src/http-gzip.ts +112 -0
  90. package/src/hub-provider-config.test.ts +139 -0
  91. package/src/hub-provider-config.ts +105 -0
  92. package/src/hub-timestamp.test.ts +38 -0
  93. package/src/hub-timestamp.ts +27 -0
  94. package/src/human-dm.ts +215 -0
  95. package/src/inbox-caller.ts +49 -0
  96. package/src/inbox-count-sse.test.ts +310 -0
  97. package/src/inbox-count.ts +29 -0
  98. package/src/index.ts +15 -2362
  99. package/src/lifecycle-guard.ts +1 -1
  100. package/src/mcp-context-cost-http.test.ts +123 -0
  101. package/src/mcp-projects-events-http.test.ts +162 -0
  102. package/src/mcp-route-label-http.test.ts +120 -0
  103. package/src/mcp-write-network-resolution.test.ts +432 -0
  104. package/src/member-audit-network-id-http.test.ts +93 -0
  105. package/src/member-presence-http.test.ts +210 -0
  106. package/src/messages-ack-by-agent-http.test.ts +142 -0
  107. package/src/migrate-sqlite-to-pg.test.ts +47 -0
  108. package/src/migrate-sqlite-to-pg.ts +263 -0
  109. package/src/model-auth-notify-http.test.ts +181 -0
  110. package/src/model-auth-notify.ts +116 -0
  111. package/src/network-name-http.test.ts +75 -0
  112. package/src/network-scope.ts +276 -0
  113. package/src/network-token-membership-http.test.ts +139 -0
  114. package/src/node-alias-control.ts +12 -0
  115. package/src/node-attrs-validate.ts +88 -0
  116. package/src/node-attrs.test.ts +217 -0
  117. package/src/node-caller-identity-http.test.ts +781 -0
  118. package/src/node-create-request-status-http.test.ts +105 -0
  119. package/src/node-daemon-bindings.test.ts +132 -0
  120. package/src/node-daemon-bindings.ts +130 -0
  121. package/src/node-files-transport.test.ts +344 -0
  122. package/src/node-health-codex-login.test.ts +120 -0
  123. package/src/node-health-dispatch-http.test.ts +279 -0
  124. package/src/node-health-guard.ts +105 -0
  125. package/src/node-health-passthrough.test.ts +115 -0
  126. package/src/node-health-store.ts +101 -0
  127. package/src/node-id-alias.test.ts +190 -0
  128. package/src/node-identity-conflict-http.test.ts +269 -0
  129. package/src/node-identity-conflict.ts +142 -0
  130. package/src/node-identity-mismatch.test.ts +74 -0
  131. package/src/node-lifecycle-controllable.test.ts +64 -0
  132. package/src/node-lifecycle-controllable.ts +53 -0
  133. package/src/node-lifecycle-read-http.test.ts +245 -0
  134. package/src/node-lifecycle-read.ts +121 -0
  135. package/src/node-logs-transport.test.ts +263 -0
  136. package/src/node-permissions-http.test.ts +276 -0
  137. package/src/node-permissions.ts +285 -0
  138. package/src/node-request-retention.test.ts +281 -0
  139. package/src/node-request-retention.ts +120 -0
  140. package/src/node-skills-transport.test.ts +209 -0
  141. package/src/node-token-ownership.ts +51 -0
  142. package/src/node-token-privilege-http.test.ts +158 -0
  143. package/src/observer-avatar-http.test.ts +431 -0
  144. package/src/observer-push.test.ts +252 -0
  145. package/src/opencode-v2-catalog.test.ts +268 -0
  146. package/src/opencode-v2-catalog.ts +352 -0
  147. package/src/package-version-consistency.test.ts +25 -0
  148. package/src/peer-reply-atomic.test.ts +455 -0
  149. package/src/peer-reply-capability.test.ts +70 -0
  150. package/src/pg-worker.ts +87 -0
  151. package/src/project-dir-warn-clear-http.test.ts +132 -0
  152. package/src/project-viewer-can-http.test.ts +114 -0
  153. package/src/push.ts +429 -10
  154. package/src/redact-tokens.test.ts +65 -0
  155. package/src/redact-tokens.ts +40 -0
  156. package/src/rename.ts +22 -2
  157. package/src/reply-target-derivation.test.ts +96 -0
  158. package/src/report-status-handover-capabilities-http.test.ts +124 -0
  159. package/src/report-status-host-schema.test.ts +235 -0
  160. package/src/report-status-resume-id-handover.test.ts +150 -0
  161. package/src/require-explicit-test-db.ts +9 -0
  162. package/src/requirement-due-reminders-http.test.ts +620 -0
  163. package/src/requirement-due-reminders.ts +416 -0
  164. package/src/requirement-events-http.test.ts +268 -0
  165. package/src/requirement-events.test.ts +45 -0
  166. package/src/requirement-events.ts +144 -0
  167. package/src/requirement-notify.ts +165 -0
  168. package/src/requirement-participant-notify-http.test.ts +309 -0
  169. package/src/requirement-tag-ops-http.test.ts +266 -0
  170. package/src/requirement-tags.ts +89 -0
  171. package/src/requirements-abandoned-http.test.ts +236 -0
  172. package/src/requirements-comment-http.test.ts +167 -0
  173. package/src/requirements-completed-at-migration.test.ts +70 -0
  174. package/src/requirements-errors-mcp-http.test.ts +164 -0
  175. package/src/requirements-errors.ts +143 -0
  176. package/src/requirements-etag-http.test.ts +91 -0
  177. package/src/requirements-http.test.ts +709 -0
  178. package/src/requirements-last-event-http.test.ts +349 -0
  179. package/src/requirements-list-mcp-slim-http.test.ts +155 -0
  180. package/src/requirements-list-slim-http.test.ts +319 -0
  181. package/src/requirements-mcp.test.ts +163 -0
  182. package/src/requirements-migrate.ts +259 -0
  183. package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
  184. package/src/requirements-people-http.test.ts +114 -0
  185. package/src/requirements-people-mcp-http.test.ts +225 -0
  186. package/src/requirements-people.ts +127 -0
  187. package/src/requirements-priority-lowest.test.ts +114 -0
  188. package/src/requirements-search-http.test.ts +182 -0
  189. package/src/requirements-search.test.ts +32 -0
  190. package/src/requirements-search.ts +145 -0
  191. package/src/requirements-seq-http.test.ts +193 -0
  192. package/src/requirements-seq-migration.test.ts +63 -0
  193. package/src/requirements-start-date.test.ts +125 -0
  194. package/src/requirements-stats-http.test.ts +288 -0
  195. package/src/requirements-stats.ts +167 -0
  196. package/src/requirements-status-participants-http.test.ts +222 -0
  197. package/src/requirements-upgrade-e2e.test.ts +126 -0
  198. package/src/requirements-upgrade-migration.test.ts +98 -0
  199. package/src/requirements.ts +1806 -0
  200. package/src/resolve-port.test.ts +57 -0
  201. package/src/resolve-port.ts +44 -0
  202. package/src/response-charset.test.ts +130 -0
  203. package/src/rest-explicit-columns-http.test.ts +222 -0
  204. package/src/rest-identity.test.ts +130 -0
  205. package/src/rest-identity.ts +139 -0
  206. package/src/rest-projections.ts +68 -0
  207. package/src/rest-write-network-resolution.test.ts +133 -0
  208. package/src/restricted-files.ts +76 -0
  209. package/src/retention.ts +15 -3
  210. package/src/route-callers-http.test.ts +120 -0
  211. package/src/route-timing.test.ts +211 -0
  212. package/src/route-timing.ts +258 -0
  213. package/src/rules-file-duplicate-alias.test.ts +234 -0
  214. package/src/rules-file-session-target.test.ts +207 -0
  215. package/src/rules-file-transport.test.ts +237 -0
  216. package/src/runtime-label.test.ts +22 -0
  217. package/src/runtime-label.ts +21 -0
  218. package/src/runtime-readiness.ts +90 -0
  219. package/src/schedule-agent-mcp-http.test.ts +381 -0
  220. package/src/schedule-agent.ts +192 -0
  221. package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
  222. package/src/scheduled-failures-http.test.ts +233 -0
  223. package/src/scheduled-failures.ts +179 -0
  224. package/src/scheduled-offline.test.ts +131 -0
  225. package/src/scheduled-offline.ts +66 -0
  226. package/src/scheduled-reply-unread-http.test.ts +212 -0
  227. package/src/scheduled-run-terminal.test.ts +268 -0
  228. package/src/scheduled-skip-blocking-task.test.ts +120 -0
  229. package/src/scheduled-stuck-http.test.ts +238 -0
  230. package/src/scheduled-stuck.ts +153 -0
  231. package/src/scheduled-tasks-http.test.ts +638 -0
  232. package/src/scheduled-tasks.ts +766 -0
  233. package/src/scoped-secrets.ts +124 -0
  234. package/src/send-dedup-request-id.test.ts +229 -0
  235. package/src/send-reply-agent-warning.test.ts +202 -0
  236. package/src/send-reply-alias-optional.test.ts +131 -0
  237. package/src/send-reply-attachments.test.ts +458 -0
  238. package/src/send_dedup.test.ts +54 -0
  239. package/src/send_dedup.ts +52 -6
  240. package/src/serve-error.test.ts +38 -0
  241. package/src/serve-error.ts +27 -0
  242. package/src/server.ts +5257 -0
  243. package/src/session-task-on-dispatch.ts +14 -0
  244. package/src/session-token-ownership-http.test.ts +122 -0
  245. package/src/shared/codex-inventory.ts +41 -0
  246. package/src/shared/daemon-provider-login.ts +31 -0
  247. package/src/shared/daemon-provider-snapshot.ts +52 -0
  248. package/src/shared/external-schedule-contract.ts +60 -0
  249. package/src/shared/node-name.test.ts +62 -0
  250. package/src/shared/node-name.ts +161 -0
  251. package/src/shared/provider-application.ts +29 -0
  252. package/src/shared/provider-collection.ts +14 -0
  253. package/src/shared/provider-initialization.ts +8 -0
  254. package/src/shared/reserved-env-drift.test.ts +10 -0
  255. package/src/shared/scoped-secret-contract.ts +5 -0
  256. package/src/shared/task-content-limit-drift.test.ts +28 -0
  257. package/src/shared/task-content-limit.ts +25 -0
  258. package/src/side-thread-command-transport.test.ts +312 -0
  259. package/src/side-thread-command-transport.ts +318 -0
  260. package/src/side-thread-http-integration.test.ts +233 -0
  261. package/src/side-thread-http.ts +504 -0
  262. package/src/side-thread-production.test.ts +72 -0
  263. package/src/side-thread-production.ts +118 -0
  264. package/src/side-thread.test.ts +1067 -0
  265. package/src/side-thread.ts +2499 -0
  266. package/src/skillhub-http.test.ts +108 -0
  267. package/src/sqlite-close-checkpoint.test.ts +78 -0
  268. package/src/start-node.test.ts +264 -0
  269. package/src/status-alias-filter-http.test.ts +106 -0
  270. package/src/status-alias-resolver-http.test.ts +163 -0
  271. package/src/status-alias-resolver.ts +19 -0
  272. package/src/status-node-id-filter-http.test.ts +161 -0
  273. package/src/status-read-cache-http.test.ts +229 -0
  274. package/src/status-read-cache.ts +122 -0
  275. package/src/stop-delete-node.test.ts +509 -1
  276. package/src/task-access-http.test.ts +402 -0
  277. package/src/task-access.ts +420 -0
  278. package/src/task-auth-origin-http.test.ts +93 -0
  279. package/src/task-auth-origin.test.ts +24 -0
  280. package/src/task-auth-origin.ts +7 -0
  281. package/src/task-consumption.test.ts +482 -0
  282. package/src/task-diagnostic-http.test.ts +149 -0
  283. package/src/task-diagnostic.test.ts +115 -0
  284. package/src/task-diagnostic.ts +118 -0
  285. package/src/task-expiry-notice-http.test.ts +287 -0
  286. package/src/task-expiry-notice.ts +315 -0
  287. package/src/task-idempotency-mcp.test.ts +142 -0
  288. package/src/task-idempotency.test.ts +60 -0
  289. package/src/task-idempotency.ts +75 -0
  290. package/src/task-late-replies.ts +54 -0
  291. package/src/task-late-reply-http.test.ts +152 -0
  292. package/src/task-lifecycle-watcher.test.ts +204 -0
  293. package/src/task-lifecycle-watcher.ts +49 -0
  294. package/src/task-network-resolution-http.test.ts +140 -0
  295. package/src/task-orphan-http.test.ts +287 -0
  296. package/src/task-orphan.ts +119 -0
  297. package/src/task-queue-ahead-http.test.ts +309 -0
  298. package/src/task-queue-ahead-scale.test.ts +236 -0
  299. package/src/task-queue-ahead.ts +186 -0
  300. package/src/task-rejection-log-http.test.ts +160 -0
  301. package/src/task-rejection-log.ts +49 -0
  302. package/src/task-sender-http.test.ts +143 -0
  303. package/src/task-stale-open-http.test.ts +290 -0
  304. package/src/task-stale-open.ts +103 -0
  305. package/src/tasks-pagination-http.test.ts +141 -0
  306. package/src/tool-audience-http.test.ts +220 -0
  307. package/src/tool-audience.ts +118 -0
  308. package/src/tools-list-json-schema.test.ts +67 -0
  309. package/src/tools.ts +3940 -325
  310. package/src/upload-early-reject-guard.test.ts +155 -0
  311. package/src/uploads-http.test.ts +97 -15
  312. package/src/uploads.ts +103 -6
  313. package/src/user-avatar-http.test.ts +74 -0
  314. package/src/user-avatar-migration.test.ts +25 -0
  315. package/src/user-inbox-by-agent-http.test.ts +161 -0
  316. package/src/user-inbox-netid-notnull.test.ts +122 -0
  317. package/src/user-inbox-read-http.test.ts +223 -0
  318. package/src/user-inbox-schema.test.ts +54 -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
 
@@ -128,9 +150,11 @@ delivered/acked/running → reassign → delivered (new agent)
128
150
 
129
151
  ## PostgreSQL (community extension point — not on the maintained roadmap)
130
152
 
153
+ > **PostgreSQL support is still in development and does not work yet** (PostgreSQL 支持尚在开发中,目前不可用). With `DATABASE_URL=postgres://…` the hub builds its schema, but `startHub()` refuses to run on PostgreSQL unless you also set `COMMHUB_PG_EXPERIMENTAL=1` (experimental, for testing only: it switches on the scheduler, task runtime evidence and side-thread commands on PostgreSQL before their PostgreSQL tests exist; RFC-039 S4 removes the flag). Progress: [RFC-039](https://github.com/sleep2agi/agent-network/blob/main/docs/rfcs/RFC-039-hub-postgresql-backend.md).
154
+
131
155
  > v0.8+ product direction is **SQLite only** (see [docs/v3-postgresql-design.md banner](https://github.com/sleep2agi/agent-network/blob/main/docs/v3-postgresql-design.md)). The PostgreSQL adapter interface is preserved as a community extension point — no E2E coverage on the current stable line; **not recommended for mainline production**.
132
156
 
133
- Set `DATABASE_URL` to switch to PostgreSQL — the SQL layer auto-translates SQLite-isms (datetime, parameter placeholders) so application code is unchanged. Requires `bun add pg`.
157
+ Design intent (not working today): setting `DATABASE_URL` switches to PostgreSQL, and the SQL layer translates SQLite-isms (datetime, parameter placeholders). Requires `bun add pg`.
134
158
 
135
159
  ```bash
136
160
  DATABASE_URL=postgres://user:pass@host:5432/commhub bunx @sleep2agi/commhub-server
@@ -144,7 +168,103 @@ DATABASE_URL=postgres://user:pass@host:5432/commhub bunx @sleep2agi/commhub-serv
144
168
  | `HOST` | `0.0.0.0` in the server package, `127.0.0.1` when launched by `anet hub start` | listen address |
145
169
  | `COMMHUB_AUTH_TOKEN` | (none) | Bearer token gate (legacy) |
146
170
  | `COMMHUB_DB` | `~/.commhub/commhub.db` | SQLite path |
147
- | `DATABASE_URL` | (none) | switches to PostgreSQL when set (unverified) |
171
+ | `DATABASE_URL` | (none) | switches to PostgreSQL when set — **does not work yet**, see above |
172
+
173
+ ## Running the unit tests
174
+
175
+ ### 先说该走哪条路:Docker
176
+
177
+ 仓库规约是测试在 Docker 里跑(`CLAUDE.md`「所有测试在 Docker 里跑:不碰本地
178
+ 环境,不改生产」)。`server/src` 的测试在 `tests/` 下已经有 **24 个**按主题分片的
179
+ 套件覆盖(实测:遍历 main 上 204 个套件目录,run.sh 里跑 `server/src/*.test.ts`
180
+ 的有 24 个),每个都在容器内自带 `COMMHUB_DB`。跑其中任意一个都是这个形状:
181
+
182
+ ```bash
183
+ # 从仓根跑
184
+ docker build --build-arg SOURCE_COMMIT="$(git rev-parse HEAD)" \
185
+ -t anet-test624 -f tests/test624-task-cursor-pagination/Dockerfile .
186
+ docker run --rm anet-test624
187
+ ```
188
+
189
+ (实跑过:退出码 0,`6 pass / 0 fail`,`RESULT: PASS`。换成 `tests/` 下别的
190
+ `test*-*/Dockerfile` 同理,但 **build-arg 的名字要照抄那个 Dockerfile** ——
191
+ main 上 66 处写的是 `SOURCE_COMMIT`,另有若干写成 `TEST<编号>_SOURCE_COMMIT`
192
+ (如 `TEST686_SOURCE_COMMIT`、`TEST765_SOURCE_COMMIT`)。传错名字不会报错,
193
+ 只会让套件收到 `unknown` 然后按它自己的规则红或绿 —— 这不是你想要的那种绿。)
194
+
195
+ > ⚠️ **目前没有一个套件跑「`server/src` 下的全部」单测。** 上面那些是按主题
196
+ > 分片的;聚合门在 #798 里,尚未合入 main。所以「所有分片套件都绿」**不等于**
197
+ > 69 个单测文件都跑过 —— 在 #798 合入之前,不要把前者当成后者。
198
+
199
+ ### 直接在宿主上跑时的两条契约
200
+
201
+ 下面这两条是上面那些套件在容器内**已经替你满足**的东西。你只有在本地迭代
202
+ 单个文件时才需要自己处理它们 —— 而那种跑法不构成任何门禁证据。
203
+
204
+
205
+ 1. **`COMMHUB_DB` must be set.** It is not optional — `~/.commhub/commhub.db` is a
206
+ *live* database on any machine that has ever run a hub. This one is enforced:
207
+ with `NODE_ENV=test` (which `bun test` sets) and no `COMMHUB_DB`, the adapter
208
+ refuses with `REFUSING to open the default SQLite database`.
209
+
210
+ 2. **Each test *file* needs its own database.** This is *not* enforced — a single
211
+ shared `COMMHUB_DB` passes the guard above and then fails a handful of tests
212
+ for reasons that look like product bugs but are not. `scripts/qa.sh` encodes
213
+ the real contract:
214
+
215
+ ```bash
216
+ # 从仓根跑(见下),每个文件一个全新的库
217
+ f=server/src/auth-validate.test.ts
218
+ db=/tmp/anet-$(basename "$f" .test.ts).db
219
+ rm -f "$db" # 复用上一次的库同样会破坏隔离
220
+ COMMHUB_DB="$db" bun test "$f"
221
+ ```
222
+
223
+ 整套跑法:
224
+
225
+ ```bash
226
+ rc=0
227
+ # 用 find 而不是 `server/src/*.test.ts` —— glob 不进子目录,会漏掉
228
+ # server/src/shared/ 下的 3 个(实测:深度 1 是 66 个,递归是 69 个)。
229
+ # scripts/qa.sh 与 test798 用的就是 find,这里保持一致。
230
+ while IFS= read -r f; do
231
+ # 用完整路径派生库名,不用 basename:加入子目录之后,
232
+ # 两个不同目录下的同名文件会派生出同一个库(原来只扫单层时不可能)。
233
+ db=/tmp/anet-$(printf '%s' "${f%.test.ts}" | tr '/.' '--').db
234
+ rm -f "$db" "$db-wal" "$db-shm"
235
+ COMMHUB_DB="$db" bun test "$f" || { echo "FAILED: $f"; rc=1; }
236
+ done < <(find server/src -type f -name '*.test.ts' | sort)
237
+ [ "$rc" -eq 0 ]
238
+ ```
239
+
240
+ ⚠️ 最后那行 `[ "$rc" -eq 0 ]` 不是装饰,是这段的**门**。写成
241
+ `bun test "$f" || echo "FAILED: $f"` 会让循环打印一行然后继续,
242
+ 整段的退出码恒为 0 —— **测试全红,脚本报绿**。用 `[ ... ]` 而不是
243
+ `exit "$rc"`,是为了这段直接粘进终端时不会把你的 shell 关掉;
244
+ 存成脚本跑时,它是最后一条命令,脚本退出码就等于它。
245
+
246
+ ⚠️ 别把 `$name` 之类的占位符原样抄进命令行 —— 未定义的变量会展开成空串,
247
+ 于是每个文件都写进**同一个** `/tmp/…-.db`,正好是这一节要避免的事。
248
+
249
+ Symptom if you ignore it: `bun test src/` under one database turns
250
+ `admin-networks-http` and `scheduled-tasks-http` red. Both are **global-count
251
+ assertions that are correct under the contract**:
252
+
253
+ ```ts
254
+ // admin-networks-http.test.ts — the admin sees EXACTLY this file's two networks
255
+ expect(new Set(rows.map((row) => row.network_id)))
256
+ .toEqual(new Set([adminNetworkId, memberNetworkId]));
257
+
258
+ // scheduled-tasks-http.test.ts — EXACTLY one due occurrence was processed
259
+ expect(runDueScheduledTasks().processed).toBe(1);
260
+ ```
261
+
262
+ Share a database and other files' networks and due schedules join the count.
263
+ Nothing is broken; the run violated the contract.
264
+
265
+ `scripts/qa.sh` 的 L0 目前只点名跑 5 个 server 测试(白名单),不是全量;
266
+ 把 `server/src` 全域逐文件跑起来的那道 CI 门还在评审中(`#798`),
267
+ 未进 `main` —— 所以本节描述的是**契约**,不是「已经有东西在替你执行它」。
148
268
 
149
269
  ## Auth modes
150
270
 
@@ -156,7 +276,7 @@ DATABASE_URL=postgres://user:pass@host:5432/commhub bunx @sleep2agi/commhub-serv
156
276
  ## Not verified
157
277
 
158
278
  - `/api/license*` — experimental legacy trial/pro-license endpoints.
159
- - PostgreSQL backend — translation layer exists, no E2E run.
279
+ - PostgreSQL backend — in development, does not work yet (the hub refuses to start on it without the experimental opt-in; RFC-039).
160
280
  - Telegram / WeChat / Feishu channel endpoints — channel code exists, but only Telegram-oriented agent-node paths are actively exercised.
161
281
 
162
282
  ## License
package/bin/commhub.ts CHANGED
@@ -12,19 +12,56 @@
12
12
 
13
13
  const args = process.argv.slice(2);
14
14
 
15
- for (let i = 0; i < args.length; i++) {
16
- if (args[i] === "--port" || args[i] === "-p") process.env.PORT = args[++i];
17
- if (args[i] === "--host") process.env.HOST = args[++i];
18
- if (args[i] === "--token" || args[i] === "-t") process.env.COMMHUB_AUTH_TOKEN = args[++i];
19
- if (args[i] === "--db") process.env.COMMHUB_DB = args[++i];
20
- if (args[i] === "--cors") process.env.COMMHUB_CORS_ORIGINS = args[++i];
21
- if (args[i] === "--dev-open") process.env.COMMHUB_DEV_OPEN = "1";
22
- if (args[i] === "--help" || args[i] === "-h") {
15
+ // RFC-039 S5 — offline SQLite → PostgreSQL copy. Handled before any server
16
+ // option parsing: it never starts a Hub and never opens the default database.
17
+ if (args[0] === "migrate-to-pg") {
18
+ const flag = (name: string) => args.includes(name);
19
+ const value = (name: string) => { const i = args.indexOf(name); return i > 0 ? args[i + 1] : undefined; };
20
+ const MIGRATE_VALUE_FLAGS = new Set(["--from", "--to"]);
21
+ const MIGRATE_FLAGS = new Set(["--dry-run", "--i-know-this-is-a-copy", "--help", "-h"]);
22
+ for (let i = 1; i < args.length; i++) {
23
+ if (MIGRATE_VALUE_FLAGS.has(args[i])) { i++; continue; }
24
+ if (!MIGRATE_FLAGS.has(args[i])) {
25
+ console.error(`commhub-server migrate-to-pg: unknown argument ${args[i]}\nRun "commhub-server migrate-to-pg --help" for usage.`);
26
+ process.exit(2);
27
+ }
28
+ }
29
+ if (flag("--help") || flag("-h") || !value("--from") || !value("--to")) {
23
30
  console.log(`
31
+ Usage:
32
+ commhub-server migrate-to-pg --from <sqlite file> --to <postgres url> [--dry-run] [--i-know-this-is-a-copy]
33
+
34
+ Copies a stopped Hub's SQLite database into an EMPTY PostgreSQL database, in one
35
+ transaction, verifying per-table row counts and content hashes before COMMIT.
36
+ Experimental (RFC-039); available from commhub-server 0.9.0-preview.73.
37
+ Files under the uploads directory are not copied.
38
+
39
+ --dry-run do everything, then roll back and leave the target empty
40
+ --i-know-this-is-a-copy allow a source under ~/.commhub (refused by default)
41
+ `);
42
+ process.exit(flag("--help") || flag("-h") ? 0 : 2);
43
+ }
44
+ const { migrateSqliteToPg, MigrateRefused } = await import("../src/migrate-sqlite-to-pg.ts");
45
+ try {
46
+ await migrateSqliteToPg({
47
+ from: value("--from")!,
48
+ to: value("--to")!,
49
+ dryRun: flag("--dry-run"),
50
+ allowCommhubCopy: flag("--i-know-this-is-a-copy"),
51
+ });
52
+ process.exit(0);
53
+ } catch (error) {
54
+ console.error(error instanceof MigrateRefused ? error.message : `[migrate-to-pg] FAILED, rolled back: ${(error as Error)?.message ?? error}`);
55
+ process.exit(error instanceof MigrateRefused ? 2 : 1);
56
+ }
57
+ }
58
+
59
+ const HELP = `
24
60
  CommHub MCP Server — AI Agent 通信中枢
25
61
 
26
62
  Usage:
27
63
  commhub-server [options]
64
+ commhub-server migrate-to-pg --help Copy a stopped Hub's SQLite database into PostgreSQL (experimental)
28
65
 
29
66
  Options:
30
67
  --port, -p <port> Port to listen on (default: 9200, env: PORT)
@@ -34,6 +71,7 @@ Options:
34
71
  --cors <origins> CORS origins, comma-separated (env: COMMHUB_CORS_ORIGINS)
35
72
  --dev-open Explicit unauthenticated local development mode
36
73
  --help, -h Show this help
74
+ --version, -v Print the version and exit
37
75
 
38
76
  Environment Variables:
39
77
  PORT Server port (default: 9200)
@@ -49,10 +87,60 @@ Examples:
49
87
  commhub-server --port 9200 --token my-secret-token
50
88
  commhub-server --port 9200 --dev-open
51
89
  PORT=9200 COMMHUB_AUTH_TOKEN=secret commhub-server
52
- `);
90
+ `;
91
+
92
+ // Anything this bin does not recognise is a usage error: exit 2 and never
93
+ // start a Hub. Before this, unknown words and flags were ignored and a Hub
94
+ // started on the defaults — `commhub-server <some-subcommand>` on a version
95
+ // without that subcommand opened ~/.commhub/commhub.db on :9200.
96
+ function usageError(message: string): never {
97
+ console.error(`commhub-server: ${message}\nRun "commhub-server --help" for usage.`);
98
+ process.exit(2);
99
+ }
100
+
101
+ const VALUE_FLAGS: Record<string, string> = {
102
+ "--port": "PORT", "-p": "PORT",
103
+ "--host": "HOST",
104
+ "--token": "COMMHUB_AUTH_TOKEN", "-t": "COMMHUB_AUTH_TOKEN",
105
+ "--db": "COMMHUB_DB",
106
+ "--cors": "COMMHUB_CORS_ORIGINS",
107
+ };
108
+
109
+ for (let i = 0; i < args.length; i++) {
110
+ const arg = args[i];
111
+ if (Object.hasOwn(VALUE_FLAGS, arg)) {
112
+ const value = args[i + 1];
113
+ if (value === undefined || value.startsWith("--")) usageError(`${arg} needs a value`);
114
+ process.env[VALUE_FLAGS[arg]] = value;
115
+ i++;
116
+ } else if (arg === "--dev-open") {
117
+ process.env.COMMHUB_DEV_OPEN = "1";
118
+ } else if (arg === "--help" || arg === "-h") {
119
+ console.log(HELP);
53
120
  process.exit(0);
121
+ } else if (arg === "--version" || arg === "-v") {
122
+ // Set by bin/anet-bin.ts before this file is imported. A compiled binary
123
+ // has no package.json beside import.meta.url (/$bunfs). npm leaves it unset.
124
+ const embedded = process.env.ANET_EMBEDDED_COMMHUB_VERSION?.trim() ?? "";
125
+ if (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(embedded)) {
126
+ console.log(embedded);
127
+ process.exit(0);
128
+ }
129
+ const pkg = (await import("../package.json", { with: { type: "json" } })).default as { version: string };
130
+ console.log(pkg.version);
131
+ process.exit(0);
132
+ } else {
133
+ usageError(arg.startsWith("-") ? `unknown option ${arg}` : `unknown command ${arg}`);
54
134
  }
55
135
  }
56
136
 
57
- // Load the server
58
- import("../src/index.js");
137
+ // Opt into the default production DB path before loading the server graph.
138
+ // Ordinary imports / bun -e probes do not receive this capability.
139
+ process.env.COMMHUB_SERVER = "1";
140
+
141
+ // Load the server module (side-effect-free since the #438 corrective:
142
+ // importing binds nothing) and start the hub EXPLICITLY. Never rely on
143
+ // import side effects or import.meta.main here — this bin is a dynamic
144
+ // importer, where import.meta.main is false inside the module.
145
+ const { startHub } = await import("../src/server.js");
146
+ startHub();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.13",
4
- "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.",
3
+ "version": "0.9.0-preview.131",
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",
@@ -46,9 +46,9 @@
46
46
  "bun": ">=1.2.0"
47
47
  },
48
48
  "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.12.0",
49
+ "@modelcontextprotocol/sdk": "1.32.0",
50
50
  "bun-types": "^1.3.13",
51
51
  "hono": "^4.12.25",
52
- "zod": "^4.4.3"
52
+ "zod": "4.6.5"
53
53
  }
54
54
  }
@@ -0,0 +1,182 @@
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("explicit launch proof survives transport while malformed booleans are rejected", async () => {
102
+ const requestId = 'cr_transport_verified';
103
+ seedRequest(requestId);
104
+ const { client, server } = await connectClient();
105
+ try {
106
+ const read = () => db.get<{ launch_verified_at: number | null }>(
107
+ 'SELECT launch_verified_at FROM node_create_requests WHERE request_id=?1', requestId);
108
+ const bad = await client.callTool({ name: 'ack_create_request', arguments: {
109
+ request_id: requestId, status: 'started', child_pid: 7777, launch_verified: 'true',
110
+ } });
111
+ expectTransportValidationError(bad);
112
+ expect(read()?.launch_verified_at).toBeNull();
113
+ const good = await client.callTool({ name: 'ack_create_request', arguments: {
114
+ request_id: requestId, status: 'started', child_pid: 7777, launch_verified: true,
115
+ } });
116
+ expect(responseJson(good).ok).toBe(true);
117
+ expect(read()?.launch_verified_at).toBeGreaterThan(0);
118
+ } finally {
119
+ await client.close();
120
+ await server.close();
121
+ }
122
+ });
123
+
124
+ test("accepts runtime_capability_check_failed plus string runtime through the SDK zod gate", async () => {
125
+ const requestId = "cr_transport_accept";
126
+ seedRequest(requestId);
127
+ const { client, server } = await connectClient();
128
+ try {
129
+ const result = await client.callTool({
130
+ name: "ack_create_request",
131
+ arguments: {
132
+ request_id: requestId,
133
+ status: "runtime_capability_check_failed",
134
+ error: "child died within 5000ms post-spawn",
135
+ runtime: "codex-sdk",
136
+ },
137
+ });
138
+ expect(responseJson(result)).toMatchObject({ ok: true, status: "runtime_capability_check_failed" });
139
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
140
+ .toBe("runtime_capability_check_failed");
141
+ } finally {
142
+ await client.close();
143
+ await server.close();
144
+ }
145
+ });
146
+
147
+ test("rejects unknown status at the transport gate before the handler mutates the row", async () => {
148
+ const requestId = "cr_transport_bad_status";
149
+ seedRequest(requestId);
150
+ const { client, server } = await connectClient();
151
+ try {
152
+ const result = await client.callTool({
153
+ name: "ack_create_request",
154
+ arguments: { request_id: requestId, status: "capability_failed_typo", runtime: "codex-sdk" },
155
+ });
156
+ expectTransportValidationError(result);
157
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
158
+ .toBe("delivered");
159
+ } finally {
160
+ await client.close();
161
+ await server.close();
162
+ }
163
+ });
164
+
165
+ test("rejects non-string runtime at the transport gate before the handler mutates the row", async () => {
166
+ const requestId = "cr_transport_bad_runtime";
167
+ seedRequest(requestId);
168
+ const { client, server } = await connectClient();
169
+ try {
170
+ const result = await client.callTool({
171
+ name: "ack_create_request",
172
+ arguments: { request_id: requestId, status: "runtime_capability_check_failed", runtime: 42 },
173
+ });
174
+ expectTransportValidationError(result);
175
+ expect(db.get<{ status: string }>("SELECT status FROM node_create_requests WHERE request_id = ?1", requestId)?.status)
176
+ .toBe("delivered");
177
+ } finally {
178
+ await client.close();
179
+ await server.close();
180
+ }
181
+ });
182
+ });
@@ -13,11 +13,11 @@ import { registerTools } from "./tools.js";
13
13
  // `daemon_capability_lied` audit was written when in fact the action wasn't
14
14
  // in the audit enum and no insert ran.
15
15
  //
16
- // This handler-driven suite drives the registered MCP tool through
17
- // registerTools() so a daemon's exact ack shape (status + runtime field)
18
- // flows through the real zod schema + handler branch + audit insert. It
19
- // would have caught the lane-recurring "single component passes but cross-
20
- // component path stays broken" failure mode immediately.
16
+ // This handler-driven suite captures and invokes the registered callback
17
+ // directly. It validates handler branches and DB effects, but deliberately
18
+ // does NOT claim to exercise the MCP SDK's zod transport gate. The
19
+ // complementary in-process transport coverage lives in
20
+ // ack-create-request-transport.test.ts (#344).
21
21
 
22
22
  const NET = "net_ack_t";
23
23
  const DAEMON_NODE_ID = "node_ack_daemon";
@@ -138,8 +138,8 @@ async function callAck(handler: ToolHandler, args: any): Promise<AckReply> {
138
138
  }
139
139
 
140
140
  function readRequest(request_id: string) {
141
- return db.get<{ status: string; error: string | null; acked_at: number | null }>(
142
- `SELECT status, error, acked_at FROM node_create_requests WHERE request_id = ?1`,
141
+ return db.get<{ status: string; error: string | null; acked_at: number | null; launch_verified_at: number | null }>(
142
+ `SELECT status, error, acked_at, launch_verified_at FROM node_create_requests WHERE request_id = ?1`,
143
143
  request_id,
144
144
  );
145
145
  }
@@ -297,6 +297,29 @@ describe("ack_create_request HANDLER — B2 audit_log daemon_capability_lied", (
297
297
  });
298
298
 
299
299
  describe("ack_create_request HANDLER — pre-existing happy-path stays green (regression)", () => {
300
+ for (const status of ['failed', 'rejected', 'runtime_capability_check_failed']) {
301
+ test(`late ${status} after registration cannot leave succeeded with a revoked token`, async () => {
302
+ seedDaemonWorld();
303
+ const request_id = `cr_late_${status}`;
304
+ const child_token_id = `tok_late_${status}`;
305
+ seedCreateRequest({ request_id, child_token_id, status: 'succeeded' });
306
+ const handler = buildAckHandler();
307
+ await callAck(handler, { request_id, status: 'started', child_pid: 7777, launch_verified: true });
308
+ expect(readRequest(request_id)?.launch_verified_at).toBeGreaterThan(0);
309
+ const reply = await callAck(handler, { request_id, status, error: 'post-register launch verdict' });
310
+ expect(reply).toMatchObject({ ok: true, status });
311
+ expect(readRequest(request_id)?.status).toBe(status);
312
+ expect(readRequest(request_id)?.error).toBe('post-register launch verdict');
313
+ expect(readRequest(request_id)?.acked_at).toBeGreaterThan(0);
314
+ expect(readToken(child_token_id)?.revoked_at).not.toBeNull();
315
+ // Delayed success must not resurrect either the request or its token.
316
+ await callAck(handler, { request_id, status: 'started', child_pid: 7777, launch_verified: true });
317
+ expect(readRequest(request_id)?.status).toBe(status);
318
+ expect(readRequest(request_id)?.launch_verified_at).toBeNull();
319
+ expect(readToken(child_token_id)?.revoked_at).not.toBeNull();
320
+ });
321
+ }
322
+
300
323
  test("status='started' still flips acked_at without terminal teardown", async () => {
301
324
  seedDaemonWorld();
302
325
  const reqId = "cr_ack_started_regression";
@@ -310,10 +333,30 @@ describe("ack_create_request HANDLER — pre-existing happy-path stays green (re
310
333
  // 'started' doesn't flip to terminal; status stays 'delivered'.
311
334
  expect(row?.status).toBe("delivered");
312
335
  expect(row?.acked_at).toBeGreaterThan(0);
336
+ expect(row?.launch_verified_at).toBeNull();
313
337
  // child ntok is NOT revoked on 'started'.
314
338
  expect(readToken(childTok)?.revoked_at).toBeNull();
315
339
  });
316
340
 
341
+ for (const status of ['pending', 'delivered', 'succeeded'] as const) {
342
+ test(`verified launch persists independently of registration: ${status}`, async () => {
343
+ seedDaemonWorld();
344
+ const request_id = `cr_verified_${status}`;
345
+ seedCreateRequest({ request_id, child_token_id: `tok_verified_${status}`, status });
346
+ const handler = buildAckHandler();
347
+ expect(readRequest(request_id)?.launch_verified_at).toBeNull();
348
+ const bad = await callAck(handler, { request_id, status: 'started', launch_verified: true });
349
+ expect(bad.ok).toBe(false);
350
+ expect(readRequest(request_id)?.launch_verified_at).toBeNull();
351
+ await callAck(handler, { request_id, status: 'started', child_pid: 7777, launch_verified: true });
352
+ const first = readRequest(request_id)!;
353
+ expect(first.status).toBe(status);
354
+ expect(first.launch_verified_at).toBeGreaterThan(0);
355
+ await callAck(handler, { request_id, status: 'started', child_pid: 7777, launch_verified: true });
356
+ expect(readRequest(request_id)?.launch_verified_at).toBe(first.launch_verified_at);
357
+ });
358
+ }
359
+
317
360
  test("status='failed' still revokes child + flips to failed (no audit_log daemon_capability_lied)", async () => {
318
361
  seedDaemonWorld();
319
362
  const reqId = "cr_ack_failed_regression";