@sleep2agi/commhub-server 0.9.0-preview.4 → 0.9.0-preview.40

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 (117) 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 +107 -13
  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-tool-schema.test.ts +39 -0
  19. package/src/create-node-validate.test.ts +80 -1
  20. package/src/create-node-validate.ts +49 -5
  21. package/src/create-node.ts +51 -1
  22. package/src/dashboard-slash-routing-http.test.ts +103 -0
  23. package/src/db-adapter-guard.test.ts +85 -0
  24. package/src/db-adapter.ts +64 -23
  25. package/src/db-timestamp.test.ts +38 -0
  26. package/src/db-timestamp.ts +15 -0
  27. package/src/db.ts +622 -33
  28. package/src/delivered-without-push.test.ts +140 -0
  29. package/src/desktop-message.test.ts +286 -0
  30. package/src/ext-token-shared.test.ts +60 -0
  31. package/src/ext-validation-regression.test.ts +131 -0
  32. package/src/external-schedule-edits-http.test.ts +316 -0
  33. package/src/external-schedule-edits.ts +314 -0
  34. package/src/file-download-authz-dev-open.test.ts +135 -0
  35. package/src/file-download-authz.test.ts +403 -0
  36. package/src/file-download-date-bucket.test.ts +436 -0
  37. package/src/file-network-scope-dev-open.test.ts +97 -0
  38. package/src/file-network-scope.test.ts +731 -0
  39. package/src/health-redaction.test.ts +637 -0
  40. package/src/inbox-count-sse.test.ts +310 -0
  41. package/src/inbox-count.ts +29 -0
  42. package/src/index.ts +15 -2187
  43. package/src/lifecycle-guard.ts +50 -0
  44. package/src/list-host-supervisors.test.ts +314 -0
  45. package/src/mcp-write-network-resolution.test.ts +429 -0
  46. package/src/network-name-http.test.ts +71 -0
  47. package/src/network-scope.ts +101 -0
  48. package/src/node-attrs-validate.ts +88 -0
  49. package/src/node-attrs.test.ts +213 -0
  50. package/src/node-id-alias.test.ts +147 -0
  51. package/src/node-lifecycle-controllable.test.ts +64 -0
  52. package/src/node-lifecycle-controllable.ts +51 -0
  53. package/src/observer-avatar-http.test.ts +424 -0
  54. package/src/observer-push.test.ts +252 -0
  55. package/src/package-version-consistency.test.ts +25 -0
  56. package/src/peer-reply-atomic.test.ts +455 -0
  57. package/src/peer-reply-capability.test.ts +70 -0
  58. package/src/probe-validate.test.ts +221 -0
  59. package/src/probe-validate.ts +104 -0
  60. package/src/probe.test.ts +80 -0
  61. package/src/probe.ts +200 -0
  62. package/src/push.ts +163 -7
  63. package/src/reply-target-derivation.test.ts +96 -0
  64. package/src/require-explicit-test-db.ts +9 -0
  65. package/src/resolve-port.test.ts +57 -0
  66. package/src/resolve-port.ts +44 -0
  67. package/src/response-charset.test.ts +130 -0
  68. package/src/rest-explicit-columns-http.test.ts +212 -0
  69. package/src/rest-identity.test.ts +129 -0
  70. package/src/rest-identity.ts +100 -0
  71. package/src/rest-projections.ts +67 -0
  72. package/src/rest-write-network-resolution.test.ts +133 -0
  73. package/src/runtime-label.test.ts +22 -0
  74. package/src/runtime-label.ts +21 -0
  75. package/src/scheduled-run-terminal.test.ts +268 -0
  76. package/src/scheduled-tasks-http.test.ts +479 -0
  77. package/src/scheduled-tasks.ts +549 -0
  78. package/src/send-reply-agent-warning.test.ts +202 -0
  79. package/src/send-reply-alias-optional.test.ts +131 -0
  80. package/src/send-reply-attachments.test.ts +437 -0
  81. package/src/server.ts +3591 -0
  82. package/src/shared/external-schedule-contract.ts +60 -0
  83. package/src/shared/probe-host-allowlist-drift.test.ts +54 -0
  84. package/src/shared/probe-host-allowlist.ts +128 -0
  85. package/src/side-thread-command-transport.test.ts +302 -0
  86. package/src/side-thread-command-transport.ts +318 -0
  87. package/src/side-thread-http-integration.test.ts +229 -0
  88. package/src/side-thread-http.ts +504 -0
  89. package/src/side-thread-production.test.ts +72 -0
  90. package/src/side-thread-production.ts +118 -0
  91. package/src/side-thread.test.ts +1067 -0
  92. package/src/side-thread.ts +2495 -0
  93. package/src/skillhub-http.test.ts +88 -0
  94. package/src/start-node.test.ts +109 -0
  95. package/src/stop-delete-node.test.ts +952 -0
  96. package/src/task-auth-origin-http.test.ts +87 -0
  97. package/src/task-auth-origin.test.ts +24 -0
  98. package/src/task-auth-origin.ts +7 -0
  99. package/src/task-consumption.test.ts +471 -0
  100. package/src/task-diagnostic-http.test.ts +145 -0
  101. package/src/task-diagnostic.test.ts +115 -0
  102. package/src/task-diagnostic.ts +118 -0
  103. package/src/task-idempotency-mcp.test.ts +142 -0
  104. package/src/task-idempotency.test.ts +60 -0
  105. package/src/task-idempotency.ts +75 -0
  106. package/src/task-lifecycle-watcher.test.ts +200 -0
  107. package/src/task-lifecycle-watcher.ts +49 -0
  108. package/src/task-network-resolution-http.test.ts +137 -0
  109. package/src/tasks-pagination-http.test.ts +137 -0
  110. package/src/tools.ts +2952 -191
  111. package/src/update-provider.test.ts +234 -0
  112. package/src/upload-early-reject-guard.test.ts +155 -0
  113. package/src/uploads-http.test.ts +92 -14
  114. package/src/uploads.test.ts +66 -0
  115. package/src/uploads.ts +130 -5
  116. package/src/vault.test.ts +200 -0
  117. package/src/vault.ts +156 -0
@@ -0,0 +1,21 @@
1
+ /** Preserve exact agent-node runtimes before applying legacy family aliases. */
2
+ export function normalizeSessionRuntime(agent: unknown): string | null {
3
+ if (typeof agent !== "string" || agent.length === 0) return null;
4
+ if (agent === "claude-code") return "claude-code-cli";
5
+ const reported = agent.startsWith("agent-node:") ? agent.slice("agent-node:".length) : "";
6
+ const exact: Record<string, string> = {
7
+ "codex-app-server": "codex-app-server",
8
+ "grok-build-cli": "grok-build-cli",
9
+ opencode: "opencode-cli",
10
+ "opencode-cli": "opencode-cli",
11
+ codex: "codex-sdk",
12
+ "codex-sdk": "codex-sdk",
13
+ claude: "claude-agent-sdk",
14
+ "claude-agent-sdk": "claude-agent-sdk",
15
+ grok: "grok-build-acp",
16
+ "grok-build-acp": "grok-build-acp",
17
+ };
18
+ if (exact[reported]) return exact[reported];
19
+ if (agent === "http-api" || agent === "http" || agent === "api") return "http-api";
20
+ return null;
21
+ }
@@ -0,0 +1,268 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { db, chainReplyToParent } from "./db.js";
4
+ import { dispatchScheduledOccurrence } from "./scheduled-tasks.js";
5
+ import { registerTools } from "./tools.js";
6
+ import { patrolExpiredTasks } from "./server.js";
7
+
8
+ const NET = "net_690_scheduler_terminal";
9
+ const USER = "u_690_owner";
10
+ const NODE = "n_690_grok";
11
+ const ALIAS = "A站GrokTUI-690";
12
+
13
+ type Handler = (args: any, extra?: any) => Promise<{ content: Array<{ type: "text"; text: string }> }>;
14
+
15
+ function cleanup(): void {
16
+ for (const table of ["task_events", "inbox", "scheduled_task_runs", "scheduled_tasks", "tasks", "sessions", "nodes", "network_members", "networks", "users"]) {
17
+ try { db.run(`DELETE FROM ${table} WHERE network_id = ?1`, [NET]); } catch {}
18
+ }
19
+ try { db.run("DELETE FROM users WHERE user_id = ?1", [USER]); } catch {}
20
+ }
21
+
22
+ function seedIdentity(): void {
23
+ db.run("INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))", [USER, USER]);
24
+ db.run("INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))", [NET, USER]);
25
+ db.run("INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))", [USER, NET]);
26
+ db.run(
27
+ `INSERT INTO nodes (node_id, node_name, alias, runtime, network_id, lifecycle_state)
28
+ VALUES (?1, ?2, ?2, 'grok-build-acp', ?3, 'active')`,
29
+ [NODE, ALIAS, NET],
30
+ );
31
+ db.run(
32
+ `INSERT INTO sessions (resume_id, alias, status, node_id, network_id, updated_at, last_seen_at)
33
+ VALUES ('r_690', ?1, 'idle', ?2, ?3, datetime('now'), datetime('now'))`,
34
+ [ALIAS, NODE, NET],
35
+ );
36
+ }
37
+
38
+ function handlers(): Record<string, Handler> {
39
+ const server = new McpServer({ name: "test690", version: "0" }) as any;
40
+ const result: Record<string, Handler> = {};
41
+ const original = server.tool.bind(server);
42
+ server.tool = (name: string, ...args: any[]) => {
43
+ const handler = args.at(-1);
44
+ if (typeof handler === "function") result[name] = handler;
45
+ return original(name, ...args);
46
+ };
47
+ registerTools(server, undefined, NET, USER, USER, false, null);
48
+ return result;
49
+ }
50
+
51
+ async function call(handler: Handler, args: any): Promise<any> {
52
+ const reply = await handler(args);
53
+ return JSON.parse(reply.content[0].text);
54
+ }
55
+
56
+ function seedScheduledTask(status = "delivered", suffix = crypto.randomUUID()): { taskId: string; runId: string; scheduleId: string } {
57
+ const taskId = `task_${suffix}`;
58
+ const runId = `srun_${suffix}`;
59
+ const scheduleId = `sched_${suffix}`;
60
+ const meta = JSON.stringify({ scheduled_task_id: scheduleId, scheduled_run_id: runId, auth_origin: "hub_scheduler" });
61
+ db.run(
62
+ `INSERT INTO scheduled_tasks
63
+ (schedule_id, network_id, name, target_node_id, target_alias, task_content, priority,
64
+ schedule_type, schedule_json, timezone, status, next_run_at)
65
+ VALUES (?1, ?2, 'test690', ?3, ?4, 'test690 task', 'normal', 'interval',
66
+ '{"type":"interval","every_seconds":3600}', 'UTC', 'active', datetime('now', '+1 hour'))`,
67
+ [scheduleId, NET, NODE, ALIAS],
68
+ );
69
+ db.run(
70
+ `INSERT INTO tasks
71
+ (task_id, from_name, to_node_id, to_name, priority, status, content, requires_response,
72
+ created_at, delivered_at, expires_at, network_id, meta_json)
73
+ VALUES (?1, 'scheduler', ?2, ?3, 'normal', ?4, 'test690 task', 'reply',
74
+ datetime('now'), datetime('now'), datetime('now', '+1 hour'), ?5, ?6)`,
75
+ [taskId, NODE, ALIAS, status, NET, meta],
76
+ );
77
+ db.run(
78
+ `INSERT INTO inbox (id, task_id, session_name, node_id, type, priority, content, from_session, requires_response, network_id, meta_json)
79
+ VALUES (?1, ?1, ?2, ?3, 'task', 'normal', 'test690 task', 'scheduler', 'reply', ?4, ?5)`,
80
+ [taskId, ALIAS, NODE, NET, meta],
81
+ );
82
+ db.run(
83
+ `INSERT INTO scheduled_task_runs (run_id, schedule_id, network_id, scheduled_for, task_id, status, completed_at)
84
+ VALUES (?1, ?2, ?3, datetime('now'), ?4, ?5, datetime('now'))`,
85
+ [runId, scheduleId, NET, taskId, status],
86
+ );
87
+ return { taskId, runId, scheduleId };
88
+ }
89
+
90
+ const readRun = (runId: string) => db.get<any>(
91
+ "SELECT run_id, task_id, network_id, status, error_code, completed_at FROM scheduled_task_runs WHERE run_id = ?1",
92
+ runId,
93
+ );
94
+
95
+ beforeEach(() => { cleanup(); seedIdentity(); });
96
+ afterAll(cleanup);
97
+
98
+ describe("#690 scheduled run follows the real task lifecycle", () => {
99
+ test("dispatch is open until the task reaches a terminal state", () => {
100
+ const scheduleId = `sched_dispatch_${crypto.randomUUID()}`;
101
+ const row: any = {
102
+ schedule_id: scheduleId,
103
+ network_id: NET,
104
+ name: "dispatch open",
105
+ target_node_id: NODE,
106
+ target_alias: ALIAS,
107
+ task_content: "dispatch open",
108
+ priority: "normal",
109
+ schedule_type: "interval",
110
+ schedule_json: JSON.stringify({ type: "interval", every_seconds: 3600 }),
111
+ timezone: "UTC",
112
+ overlap_policy: "skip",
113
+ misfire_policy: "catch_up_once",
114
+ status: "active",
115
+ next_run_at: new Date(Date.now() + 3600_000).toISOString(),
116
+ last_run_at: null,
117
+ revision: 1,
118
+ };
119
+ db.run(
120
+ `INSERT INTO scheduled_tasks
121
+ (schedule_id, network_id, name, target_node_id, target_alias, task_content, priority,
122
+ schedule_type, schedule_json, timezone, overlap_policy, misfire_policy, status, next_run_at)
123
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)`,
124
+ [scheduleId, NET, row.name, NODE, ALIAS, row.task_content, row.priority, row.schedule_type,
125
+ row.schedule_json, row.timezone, row.overlap_policy, row.misfire_policy, row.status, row.next_run_at],
126
+ );
127
+ const dispatched = dispatchScheduledOccurrence(row, new Date().toISOString(), false);
128
+ const run = readRun(dispatched.runId);
129
+ expect(["delivered", "queued"]).toContain(run.status);
130
+ expect(run.completed_at).toBeNull();
131
+ });
132
+
133
+ test("send_reply terminalizes the exact bound run", async () => {
134
+ const { taskId, runId } = seedScheduledTask();
135
+ const reply = await call(handlers().send_reply, {
136
+ alias: ALIAS,
137
+ text: "verified",
138
+ in_reply_to: taskId,
139
+ status: "replied",
140
+ network_id: NET,
141
+ attachments: [{ type: "file", file_id: "file_690_reply", name: "evidence.json", size: 12, mime: "application/json" }],
142
+ });
143
+ expect(reply.ok).toBe(true);
144
+ expect(readRun(runId).status).toBe("replied");
145
+ expect(readRun(runId).completed_at).toBeTruthy();
146
+ });
147
+
148
+ test("task and run terminal transition roll back together on an injected run-write failure", async () => {
149
+ const { taskId, runId } = seedScheduledTask();
150
+ db.exec(
151
+ `CREATE TRIGGER test690_fail_run_terminal
152
+ BEFORE UPDATE OF status ON scheduled_task_runs
153
+ WHEN NEW.run_id = '${runId}' AND NEW.status = 'replied'
154
+ BEGIN
155
+ SELECT RAISE(ABORT, 'test690_injected_run_failure');
156
+ END`,
157
+ );
158
+ let threw = false;
159
+ try {
160
+ await call(handlers().send_reply, {
161
+ alias: ALIAS,
162
+ text: "must roll back",
163
+ in_reply_to: taskId,
164
+ status: "replied",
165
+ network_id: NET,
166
+ });
167
+ } catch (error: any) {
168
+ threw = /test690_injected_run_failure/.test(String(error?.message || error));
169
+ } finally {
170
+ db.exec("DROP TRIGGER IF EXISTS test690_fail_run_terminal");
171
+ }
172
+ expect(threw).toBe(true);
173
+ expect(db.get<{ status: string }>("SELECT status FROM tasks WHERE task_id = ?1", taskId)?.status).toBe("delivered");
174
+ expect(readRun(runId).status).toBe("delivered");
175
+ expect(db.get<{ count: number }>("SELECT COUNT(*) AS count FROM inbox WHERE in_reply_to = ?1", taskId)?.count).toBe(0);
176
+ });
177
+
178
+ test("cancel terminalizes, retry reopens, and the retry result closes the same run", async () => {
179
+ const { taskId, runId } = seedScheduledTask();
180
+ const tools = handlers();
181
+ expect((await call(tools.cancel_task, { task_id: taskId, reason: "operator", network_id: NET })).ok).toBe(true);
182
+ expect(readRun(runId).status).toBe("cancelled");
183
+ expect(readRun(runId).completed_at).toBeTruthy();
184
+
185
+ expect((await call(tools.retry_task, { task_id: taskId, network_id: NET })).ok).toBe(true);
186
+ expect(readRun(runId).status).toBe("delivered");
187
+ expect(readRun(runId).completed_at).toBeNull();
188
+
189
+ expect((await call(tools.send_reply, {
190
+ alias: ALIAS,
191
+ text: "runtime failed",
192
+ in_reply_to: taskId,
193
+ status: "failed",
194
+ network_id: NET,
195
+ })).ok).toBe(true);
196
+ expect(readRun(runId).status).toBe("failed");
197
+ expect(readRun(runId).error_code).toBe("task_failed");
198
+ });
199
+
200
+ test("report_completion terminalizes a scheduler run", async () => {
201
+ const { taskId, runId } = seedScheduledTask();
202
+ const reply = await call(handlers().report_completion, {
203
+ alias: ALIAS,
204
+ task: taskId,
205
+ result: "done",
206
+ network_id: NET,
207
+ });
208
+ expect(reply.ok).toBe(true);
209
+ expect(readRun(runId).status).toBe("replied");
210
+ });
211
+
212
+ test("expiration patrol closes the exact run in the same lifecycle pass", () => {
213
+ const { taskId, runId } = seedScheduledTask();
214
+ db.run("UPDATE tasks SET expires_at = datetime('now', '-1 minute') WHERE task_id = ?1", [taskId]);
215
+ patrolExpiredTasks();
216
+ expect(db.get<{ status: string }>("SELECT status FROM tasks WHERE task_id = ?1", taskId)?.status).toBe("expired");
217
+ expect(readRun(runId).status).toBe("expired");
218
+ expect(readRun(runId).error_code).toBe("task_expired");
219
+ expect(readRun(runId).completed_at).toBeTruthy();
220
+ });
221
+
222
+ test("child auto-chain terminalizes a scheduled parent", () => {
223
+ const { taskId: parentId, runId } = seedScheduledTask();
224
+ const childId = `child_${crypto.randomUUID()}`;
225
+ db.run(
226
+ `INSERT INTO tasks
227
+ (task_id, parent_task_id, from_name, to_name, priority, status, content, requires_response, created_at, network_id)
228
+ VALUES (?1, ?2, ?3, 'A站负责人', 'normal', 'replied', 'verify candidate', 'reply', datetime('now'), ?4)`,
229
+ [childId, parentId, ALIAS, NET],
230
+ );
231
+ const result = chainReplyToParent(childId, "news_id=14519", "replied", 5, NET);
232
+ expect(result.chained).toBe(true);
233
+ expect(readRun(runId).status).toBe("replied");
234
+ });
235
+
236
+ test("forged metadata cannot cross-bind another task or network run", async () => {
237
+ const victim = seedScheduledTask("delivered", "victim");
238
+ const attackerTaskId = "task_attacker";
239
+ const forgedMeta = JSON.stringify({ scheduled_task_id: victim.scheduleId, scheduled_run_id: victim.runId, auth_origin: "hub_scheduler" });
240
+ db.run(
241
+ `INSERT INTO tasks
242
+ (task_id, from_name, to_node_id, to_name, priority, status, content, requires_response, created_at, network_id, meta_json)
243
+ VALUES (?1, 'scheduler', ?2, ?3, 'normal', 'delivered', 'forged', 'reply', datetime('now'), ?4, ?5)`,
244
+ [attackerTaskId, NODE, ALIAS, NET, forgedMeta],
245
+ );
246
+ await call(handlers().send_reply, {
247
+ alias: ALIAS,
248
+ text: "forged close",
249
+ in_reply_to: attackerTaskId,
250
+ status: "replied",
251
+ network_id: NET,
252
+ });
253
+ expect(readRun(victim.runId).status).toBe("delivered");
254
+ });
255
+
256
+ test("a task cannot close a run whose persisted network binding differs", async () => {
257
+ const victim = seedScheduledTask("delivered", "foreign_network");
258
+ db.run("UPDATE scheduled_task_runs SET network_id = 'net_690_foreign' WHERE run_id = ?1", [victim.runId]);
259
+ await call(handlers().send_reply, {
260
+ alias: ALIAS,
261
+ text: "must not cross network",
262
+ in_reply_to: victim.taskId,
263
+ status: "replied",
264
+ network_id: NET,
265
+ });
266
+ expect(readRun(victim.runId).status).toBe("delivered");
267
+ });
268
+ });