@sleep2agi/commhub-server 0.9.0-preview.14 → 0.9.0-preview.141
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.
- package/README.md +130 -10
- package/bin/commhub.ts +99 -11
- package/package.json +7 -7
- package/src/ack-create-request-transport.test.ts +182 -0
- package/src/ack-create-request.test.ts +50 -7
- package/src/admin-delete-network-http.test.ts +228 -0
- package/src/admin-networks-http.test.ts +88 -0
- package/src/adoption-candidates-http.test.ts +137 -0
- package/src/adoption-candidates.ts +158 -0
- package/src/agent-access-batch.test.ts +210 -0
- package/src/agent-access.ts +487 -0
- package/src/agent-acl-groups-http.test.ts +247 -0
- package/src/agent-acl-http.test.ts +659 -0
- package/src/agent-acl-slice3-http.test.ts +267 -0
- package/src/agent-department-http.test.ts +148 -0
- package/src/agent-groups-perf.test.ts +223 -0
- package/src/agent-notice.ts +48 -0
- package/src/agent-team-whoami-http.test.ts +98 -0
- package/src/agent-teams-http.test.ts +323 -0
- package/src/agent-teams.ts +244 -0
- package/src/alias-filter.test.ts +72 -0
- package/src/alias-filter.ts +42 -0
- package/src/api-host-supervisors-can-create-nodes.test.ts +350 -0
- package/src/api-host-supervisors-fallback.test.ts +271 -0
- package/src/api-host-supervisors-runtime-readiness.test.ts +195 -0
- package/src/api-nodes-shape.test.ts +26 -10
- package/src/auth-password-change-cli-tokens-http.test.ts +423 -0
- package/src/auth-sessions-http.test.ts +250 -0
- package/src/auth-validate.test.ts +5 -1
- package/src/auth-weak-password-login.test.ts +176 -0
- package/src/auth.ts +604 -51
- package/src/avatar-validate.test.ts +163 -0
- package/src/avatar-validate.ts +103 -0
- package/src/binding-generation-http.test.ts +121 -0
- package/src/blocked-exit-on-reply.test.ts +122 -0
- package/src/boot-side-effect.test.ts +124 -0
- package/src/chain-attempt-dedup.test.ts +447 -0
- package/src/clone-node.test.ts +248 -0
- package/src/clone-node.ts +289 -0
- package/src/codex-config-flags.ts +77 -0
- package/src/codex-fork-contract.ts +23 -0
- package/src/codex-fork-migration.test.ts +38 -0
- package/src/commhub-bin-args.test.ts +85 -0
- package/src/config-apply-sec1.test.ts +107 -0
- package/src/config-apply-validate.test.ts +134 -1
- package/src/config-apply-validate.ts +111 -5
- package/src/create-node-blocked-gate.test.ts +356 -0
- package/src/create-node-network-id.test.ts +79 -0
- package/src/create-node-tool-schema.test.ts +49 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +199 -7
- package/src/create-node-validate.ts +232 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +88 -23
- package/src/daemon-provider-snapshots.test.ts +421 -0
- package/src/daemon-provider-snapshots.ts +327 -0
- package/src/daemon-token-owner-http.test.ts +236 -0
- package/src/dashboard-slash-routing-http.test.ts +109 -0
- package/src/db-adapter-guard.test.ts +148 -0
- package/src/db-adapter-translate.test.ts +128 -0
- package/src/db-adapter.ts +323 -87
- package/src/db-timestamp.test.ts +38 -0
- package/src/db-timestamp.ts +15 -0
- package/src/db.ts +1119 -42
- package/src/delivered-without-push.test.ts +140 -0
- package/src/department-groups-http.test.ts +209 -0
- package/src/department-groups-sync-http.test.ts +328 -0
- package/src/department-groups.ts +434 -0
- package/src/department-heads-http.test.ts +330 -0
- package/src/department-heads.ts +226 -0
- package/src/departments-http.test.ts +185 -0
- package/src/departments.ts +276 -0
- package/src/desktop-message-seam.test.ts +150 -0
- package/src/desktop-message.test.ts +368 -0
- package/src/dm-file-scope-http.test.ts +178 -0
- package/src/env-transport-gate-http.test.ts +146 -0
- package/src/env-transport-gate.ts +74 -0
- package/src/env-transport-policy.ts +21 -0
- package/src/ext-token-shared.test.ts +60 -0
- package/src/ext-validation-regression.test.ts +131 -0
- package/src/external-schedule-edits-http.test.ts +321 -0
- package/src/external-schedule-edits-timestamps.test.ts +64 -0
- package/src/external-schedule-edits.ts +327 -0
- package/src/file-download-authz-dev-open.test.ts +135 -0
- package/src/file-download-authz.test.ts +404 -0
- package/src/file-download-date-bucket.test.ts +436 -0
- package/src/file-network-scope-dev-open.test.ts +97 -0
- package/src/file-network-scope.test.ts +732 -0
- package/src/group-membership-events-http.test.ts +264 -0
- package/src/group-messages-http.test.ts +482 -0
- package/src/group-messages.ts +297 -0
- package/src/health-redaction.test.ts +642 -0
- package/src/host-supervisor-identity-http.test.ts +152 -0
- package/src/host-supervisor-identity.ts +92 -0
- package/src/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -0
- package/src/hub-catalog-read-http.test.ts +187 -0
- package/src/hub-provider-config.test.ts +139 -0
- package/src/hub-provider-config.ts +105 -0
- package/src/hub-timestamp.test.ts +38 -0
- package/src/hub-timestamp.ts +27 -0
- package/src/human-dm.ts +215 -0
- package/src/human-inbox.ts +42 -0
- package/src/inbox-caller.ts +49 -0
- package/src/inbox-count-sse.test.ts +310 -0
- package/src/inbox-count.ts +29 -0
- package/src/index.ts +15 -2362
- package/src/lifecycle-guard.ts +1 -1
- package/src/list-host-supervisors.test.ts +5 -4
- package/src/mcp-context-cost-http.test.ts +123 -0
- package/src/mcp-projects-events-http.test.ts +162 -0
- package/src/mcp-route-label-http.test.ts +120 -0
- package/src/mcp-write-network-resolution.test.ts +432 -0
- package/src/member-audit-network-id-http.test.ts +93 -0
- package/src/member-presence-http.test.ts +210 -0
- package/src/messages-ack-by-agent-http.test.ts +142 -0
- package/src/migrate-sqlite-to-pg.test.ts +47 -0
- package/src/migrate-sqlite-to-pg.ts +263 -0
- package/src/model-auth-notify-http.test.ts +181 -0
- package/src/model-auth-notify.ts +116 -0
- package/src/network-name-http.test.ts +75 -0
- package/src/network-scope.ts +276 -0
- package/src/network-token-membership-http.test.ts +139 -0
- package/src/node-alias-control.ts +12 -0
- package/src/node-attrs-validate.ts +88 -0
- package/src/node-attrs.test.ts +217 -0
- package/src/node-caller-identity-http.test.ts +1083 -0
- package/src/node-create-request-status-http.test.ts +105 -0
- package/src/node-daemon-bindings.test.ts +132 -0
- package/src/node-daemon-bindings.ts +135 -0
- package/src/node-files-transport.test.ts +344 -0
- package/src/node-health-codex-login.test.ts +120 -0
- package/src/node-health-dispatch-http.test.ts +279 -0
- package/src/node-health-guard.ts +105 -0
- package/src/node-health-passthrough.test.ts +115 -0
- package/src/node-health-store.ts +101 -0
- package/src/node-id-alias.test.ts +190 -0
- package/src/node-identity-conflict-http.test.ts +269 -0
- package/src/node-identity-conflict.ts +142 -0
- package/src/node-identity-mismatch.test.ts +74 -0
- package/src/node-lifecycle-controllable.test.ts +64 -0
- package/src/node-lifecycle-controllable.ts +53 -0
- package/src/node-lifecycle-read-http.test.ts +245 -0
- package/src/node-lifecycle-read.ts +121 -0
- package/src/node-logs-transport.test.ts +263 -0
- package/src/node-permissions-http.test.ts +276 -0
- package/src/node-permissions.ts +285 -0
- package/src/node-request-retention.test.ts +281 -0
- package/src/node-request-retention.ts +120 -0
- package/src/node-secret-rest-http.test.ts +190 -0
- package/src/node-skills-transport.test.ts +209 -0
- package/src/node-token-identity.ts +35 -0
- package/src/node-token-ownership.ts +51 -0
- package/src/node-token-privilege-http.test.ts +158 -0
- package/src/observer-avatar-http.test.ts +442 -0
- package/src/observer-push.test.ts +252 -0
- package/src/opencode-v2-catalog.test.ts +268 -0
- package/src/opencode-v2-catalog.ts +352 -0
- package/src/package-version-consistency.test.ts +25 -0
- package/src/password-network-tokens.ts +25 -0
- package/src/peer-reply-atomic.test.ts +455 -0
- package/src/peer-reply-capability.test.ts +70 -0
- package/src/pg-worker.ts +87 -0
- package/src/project-dir-warn-clear-http.test.ts +132 -0
- package/src/project-viewer-can-http.test.ts +114 -0
- package/src/push.ts +429 -10
- package/src/redact-tokens.test.ts +65 -0
- package/src/redact-tokens.ts +40 -0
- package/src/rename.ts +38 -10
- package/src/reply-target-derivation.test.ts +96 -0
- package/src/report-status-handover-capabilities-http.test.ts +124 -0
- package/src/report-status-host-schema.test.ts +235 -0
- package/src/report-status-resume-id-handover.test.ts +184 -0
- package/src/report-status-secret-keys.test.ts +140 -0
- package/src/require-explicit-test-db.ts +9 -0
- package/src/requirement-due-reminders-http.test.ts +620 -0
- package/src/requirement-due-reminders.ts +416 -0
- package/src/requirement-events-http.test.ts +268 -0
- package/src/requirement-events.test.ts +45 -0
- package/src/requirement-events.ts +144 -0
- package/src/requirement-notify.ts +165 -0
- package/src/requirement-participant-notify-http.test.ts +309 -0
- package/src/requirement-tag-ops-http.test.ts +266 -0
- package/src/requirement-tags.ts +89 -0
- package/src/requirements-abandoned-http.test.ts +236 -0
- package/src/requirements-comment-http.test.ts +167 -0
- package/src/requirements-completed-at-migration.test.ts +70 -0
- package/src/requirements-errors-mcp-http.test.ts +164 -0
- package/src/requirements-errors.ts +143 -0
- package/src/requirements-etag-http.test.ts +91 -0
- package/src/requirements-http.test.ts +709 -0
- package/src/requirements-last-event-http.test.ts +349 -0
- package/src/requirements-list-mcp-slim-http.test.ts +155 -0
- package/src/requirements-list-slim-http.test.ts +319 -0
- package/src/requirements-mcp.test.ts +163 -0
- package/src/requirements-migrate.ts +259 -0
- package/src/requirements-owner-strict-mcp-http.test.ts +145 -0
- package/src/requirements-people-http.test.ts +114 -0
- package/src/requirements-people-mcp-http.test.ts +225 -0
- package/src/requirements-people.ts +131 -0
- package/src/requirements-priority-lowest.test.ts +114 -0
- package/src/requirements-search-http.test.ts +182 -0
- package/src/requirements-search.test.ts +32 -0
- package/src/requirements-search.ts +145 -0
- package/src/requirements-seq-http.test.ts +193 -0
- package/src/requirements-seq-migration.test.ts +63 -0
- package/src/requirements-start-date.test.ts +125 -0
- package/src/requirements-stats-http.test.ts +357 -0
- package/src/requirements-stats.ts +195 -0
- package/src/requirements-status-participants-http.test.ts +222 -0
- package/src/requirements-upgrade-e2e.test.ts +126 -0
- package/src/requirements-upgrade-migration.test.ts +98 -0
- package/src/requirements.ts +1806 -0
- package/src/resolve-port.test.ts +57 -0
- package/src/resolve-port.ts +44 -0
- package/src/response-charset.test.ts +130 -0
- package/src/rest-explicit-columns-http.test.ts +222 -0
- package/src/rest-identity.test.ts +130 -0
- package/src/rest-identity.ts +141 -0
- package/src/rest-projections.ts +70 -0
- package/src/rest-write-network-resolution.test.ts +133 -0
- package/src/restricted-files.ts +76 -0
- package/src/retention.ts +15 -3
- package/src/route-callers-http.test.ts +120 -0
- package/src/route-timing.test.ts +211 -0
- package/src/route-timing.ts +258 -0
- package/src/rules-file-duplicate-alias.test.ts +234 -0
- package/src/rules-file-session-target.test.ts +207 -0
- package/src/rules-file-transport.test.ts +237 -0
- package/src/runtime-label.test.ts +22 -0
- package/src/runtime-label.ts +21 -0
- package/src/runtime-readiness.ts +90 -0
- package/src/schedule-agent-mcp-http.test.ts +381 -0
- package/src/schedule-agent.ts +192 -0
- package/src/scheduled-backoff-http.test.ts +193 -0
- package/src/scheduled-backoff.ts +64 -0
- package/src/scheduled-doorbell-nodeless-session.test.ts +134 -0
- package/src/scheduled-failures-http.test.ts +233 -0
- package/src/scheduled-failures.ts +179 -0
- package/src/scheduled-offline.test.ts +131 -0
- package/src/scheduled-offline.ts +66 -0
- package/src/scheduled-outcome-guidance-http.test.ts +139 -0
- package/src/scheduled-outcome-guidance.ts +9 -0
- package/src/scheduled-reply-unread-http.test.ts +212 -0
- package/src/scheduled-requirement-wake-http.test.ts +264 -0
- package/src/scheduled-requirement-wake.ts +125 -0
- package/src/scheduled-run-terminal.test.ts +268 -0
- package/src/scheduled-skip-blocking-task.test.ts +120 -0
- package/src/scheduled-stuck-http.test.ts +312 -0
- package/src/scheduled-stuck.ts +186 -0
- package/src/scheduled-tasks-http.test.ts +679 -0
- package/src/scheduled-tasks.ts +885 -0
- package/src/scheduled-work-outcome-http.test.ts +154 -0
- package/src/scheduled-work-outcome.ts +50 -0
- package/src/scoped-secrets.ts +139 -0
- package/src/send-dedup-request-id.test.ts +229 -0
- package/src/send-reply-agent-warning.test.ts +202 -0
- package/src/send-reply-alias-optional.test.ts +131 -0
- package/src/send-reply-attachments.test.ts +458 -0
- package/src/send_dedup.test.ts +54 -0
- package/src/send_dedup.ts +52 -6
- package/src/serve-error.test.ts +38 -0
- package/src/serve-error.ts +27 -0
- package/src/server.ts +5398 -0
- package/src/session-task-on-dispatch.ts +14 -0
- package/src/session-token-ownership-http.test.ts +122 -0
- package/src/shared/codex-inventory.ts +41 -0
- package/src/shared/daemon-provider-login.ts +31 -0
- package/src/shared/daemon-provider-snapshot.ts +52 -0
- package/src/shared/external-schedule-contract.ts +60 -0
- package/src/shared/node-name.test.ts +62 -0
- package/src/shared/node-name.ts +161 -0
- package/src/shared/provider-application.ts +29 -0
- package/src/shared/provider-collection.ts +14 -0
- package/src/shared/provider-initialization.ts +8 -0
- package/src/shared/reserved-env-drift.test.ts +10 -0
- package/src/shared/scoped-secret-contract.ts +5 -0
- package/src/shared/task-content-limit-drift.test.ts +28 -0
- package/src/shared/task-content-limit.ts +25 -0
- package/src/side-thread-command-transport.test.ts +312 -0
- package/src/side-thread-command-transport.ts +318 -0
- package/src/side-thread-http-integration.test.ts +233 -0
- package/src/side-thread-http.ts +504 -0
- package/src/side-thread-production.test.ts +72 -0
- package/src/side-thread-production.ts +118 -0
- package/src/side-thread.test.ts +1067 -0
- package/src/side-thread.ts +2499 -0
- package/src/skillhub-http.test.ts +108 -0
- package/src/sqlite-close-checkpoint.test.ts +78 -0
- package/src/start-node.test.ts +264 -0
- package/src/status-alias-filter-http.test.ts +106 -0
- package/src/status-alias-resolver-http.test.ts +163 -0
- package/src/status-alias-resolver.ts +19 -0
- package/src/status-node-id-filter-http.test.ts +161 -0
- package/src/status-read-cache-http.test.ts +229 -0
- package/src/status-read-cache.ts +122 -0
- package/src/stop-delete-node.test.ts +509 -2
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-activity.ts +60 -0
- package/src/task-auth-origin-http.test.ts +93 -0
- package/src/task-auth-origin.test.ts +24 -0
- package/src/task-auth-origin.ts +7 -0
- package/src/task-consumption.test.ts +482 -0
- package/src/task-diagnostic-http.test.ts +149 -0
- package/src/task-diagnostic.test.ts +115 -0
- package/src/task-diagnostic.ts +118 -0
- package/src/task-expiry-notice-http.test.ts +287 -0
- package/src/task-expiry-notice.ts +315 -0
- package/src/task-idempotency-mcp.test.ts +142 -0
- package/src/task-idempotency.test.ts +60 -0
- package/src/task-idempotency.ts +75 -0
- package/src/task-late-replies.ts +54 -0
- package/src/task-late-reply-http.test.ts +152 -0
- package/src/task-lifecycle-watcher.test.ts +204 -0
- package/src/task-lifecycle-watcher.ts +49 -0
- package/src/task-network-resolution-http.test.ts +140 -0
- package/src/task-orphan-http.test.ts +287 -0
- package/src/task-orphan.ts +119 -0
- package/src/task-queue-ahead-http.test.ts +309 -0
- package/src/task-queue-ahead-scale.test.ts +236 -0
- package/src/task-queue-ahead.ts +186 -0
- package/src/task-rejection-log-http.test.ts +160 -0
- package/src/task-rejection-log.ts +49 -0
- package/src/task-sender-http.test.ts +143 -0
- package/src/task-stale-open-http.test.ts +290 -0
- package/src/task-stale-open.ts +103 -0
- package/src/tasks-pagination-http.test.ts +141 -0
- package/src/tool-audience-http.test.ts +220 -0
- package/src/tool-audience.ts +118 -0
- package/src/tools-list-json-schema.test.ts +67 -0
- package/src/tools.ts +4008 -355
- package/src/upload-early-reject-guard.test.ts +155 -0
- package/src/uploads-http.test.ts +97 -15
- package/src/uploads.ts +103 -6
- package/src/user-avatar-http.test.ts +74 -0
- package/src/user-avatar-migration.test.ts +25 -0
- package/src/user-inbox-by-agent-http.test.ts +161 -0
- package/src/user-inbox-netid-notnull.test.ts +122 -0
- package/src/user-inbox-read-http.test.ts +223 -0
- package/src/user-inbox-schema.test.ts +54 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// 同一 alias 换 resume_id 上报时,hub 的 report_status 走的是「DELETE 旧行 + INSERT 新行」,
|
|
2
|
+
// ON CONFLICT 里的 COALESCE 一次都不会触发 —— 上报里没带的描述性列(version / agent /
|
|
3
|
+
// hostname / 遥测 / registered_at)全部变 NULL。
|
|
4
|
+
//
|
|
5
|
+
// 2026-09-04 DEV 真机(hub 日志):
|
|
6
|
+
// 12:46:47 grok-v1 (sdk-n_6c) → report_status: working version=2.5.0-preview.64
|
|
7
|
+
// 12:46:53 grok-v1 (grok-cli) → report_status: working ← TUI 的 MCP 上报,不带 version
|
|
8
|
+
// 12:46:54 grok-v1 (sdk-n_6c) → report_status: idle ← 又换回来,也不带 version
|
|
9
|
+
// 结果:version=NULL、registered_at 被改成 12:46:54。舰队里 grok 3/10、claude 5/15 有 version,
|
|
10
|
+
// codex 34/34 有 —— 差别就是有没有第二个上报者。
|
|
11
|
+
//
|
|
12
|
+
// 判据:换 resume_id 之后,只有一行;上报了的列以上报为准,没上报的列从被替换的那行接手。
|
|
13
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { db } from "./db.js";
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { registerTools } from "./tools.js";
|
|
17
|
+
|
|
18
|
+
const NET_ID = "net_handover";
|
|
19
|
+
const USER_ID = "u_handover";
|
|
20
|
+
const NODE_ID = "n_handover";
|
|
21
|
+
const ALIAS = "grok-handover";
|
|
22
|
+
const TOK_ID = "tok_handover";
|
|
23
|
+
const RESUME_SDK = `sdk-${NODE_ID}`;
|
|
24
|
+
const RESUME_CLI = `grok-cli-${NODE_ID}`;
|
|
25
|
+
const OLD_REGISTERED_AT = "2026-01-02 03:04:05";
|
|
26
|
+
|
|
27
|
+
type ToolHandler = (args: any, extra?: any) => Promise<{ content: Array<{ type: "text"; text: string }> }>;
|
|
28
|
+
|
|
29
|
+
function cleanup() {
|
|
30
|
+
for (const [sql, params] of [
|
|
31
|
+
["DELETE FROM agent_telemetry WHERE network_id = ?1", [NET_ID]],
|
|
32
|
+
["DELETE FROM sessions WHERE network_id = ?1", [NET_ID]],
|
|
33
|
+
["DELETE FROM nodes WHERE network_id = ?1", [NET_ID]],
|
|
34
|
+
["DELETE FROM api_tokens WHERE network_id = ?1", [NET_ID]],
|
|
35
|
+
["DELETE FROM network_members WHERE network_id = ?1", [NET_ID]],
|
|
36
|
+
["DELETE FROM networks WHERE network_id = ?1", [NET_ID]],
|
|
37
|
+
["DELETE FROM users WHERE user_id = ?1", [USER_ID]],
|
|
38
|
+
] as const) {
|
|
39
|
+
try { db.run(sql, params as any); } catch {}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
beforeEach(cleanup);
|
|
43
|
+
afterAll(cleanup);
|
|
44
|
+
|
|
45
|
+
function seed() {
|
|
46
|
+
db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))`, [USER_ID, USER_ID]);
|
|
47
|
+
db.run(`INSERT INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?2, ?3, datetime('now'))`, [NET_ID, NET_ID, USER_ID]);
|
|
48
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))`, [USER_ID, NET_ID]);
|
|
49
|
+
db.run(
|
|
50
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, hostname, created_at, updated_at, lifecycle_state)
|
|
51
|
+
VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'), datetime('now'), 'active')`,
|
|
52
|
+
[NODE_ID, ALIAS, ALIAS, NET_ID, "dev-box"],
|
|
53
|
+
);
|
|
54
|
+
// agent-node 自己注册的那行:带 version / agent / hostname / 遥测 / 早就存在的 registered_at
|
|
55
|
+
db.run(
|
|
56
|
+
`INSERT INTO sessions (resume_id, alias, status, node_id, network_id, version, agent, hostname, server, project_dir,
|
|
57
|
+
cpu_cores, mem_total_gb, disk_total_gb, registered_at, updated_at, last_seen_at)
|
|
58
|
+
VALUES (?1, ?2, 'working', ?3, ?4, '2.5.0-preview.64', 'agent-node:grok-build-cli', 'dev-box', 'dev-box', '/home/x/proj',
|
|
59
|
+
8, 61.4, 500, ?5, datetime('now'), datetime('now'))`,
|
|
60
|
+
[RESUME_SDK, ALIAS, NODE_ID, NET_ID, OLD_REGISTERED_AT],
|
|
61
|
+
);
|
|
62
|
+
db.run(
|
|
63
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
|
|
64
|
+
VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
|
|
65
|
+
[TOK_ID, USER_ID, NET_ID, `node:${ALIAS}`, `hash_${TOK_ID}`],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildHandlers(): Record<string, ToolHandler> {
|
|
70
|
+
const server = new McpServer({ name: "test", version: "0" }) as any;
|
|
71
|
+
const tools: Record<string, ToolHandler> = {};
|
|
72
|
+
const origTool = server.tool.bind(server);
|
|
73
|
+
server.tool = (name: string, _desc: string, _schema: any, handler: ToolHandler) => { tools[name] = handler; return origTool(name, _desc, _schema, handler); };
|
|
74
|
+
const origRegisterTool = server.registerTool?.bind(server);
|
|
75
|
+
if (origRegisterTool) {
|
|
76
|
+
server.registerTool = (name: string, _cfg: any, handler: ToolHandler) => { tools[name] = handler; return origRegisterTool(name, _cfg, handler); };
|
|
77
|
+
}
|
|
78
|
+
registerTools(server, undefined, NET_ID, USER_ID, ALIAS, true, TOK_ID);
|
|
79
|
+
return tools;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function call(handler: ToolHandler, args: any): Promise<any> {
|
|
83
|
+
const r = await handler(args);
|
|
84
|
+
return JSON.parse(r.content[0]!.text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Row = { resume_id: string; status: string; version: string | null; agent: string | null; hostname: string | null;
|
|
88
|
+
cpu_cores: number | null; mem_total_gb: number | null; registered_at: string; task: string | null };
|
|
89
|
+
function rows(): Row[] {
|
|
90
|
+
return db.all("SELECT resume_id, status, version, agent, hostname, cpu_cores, mem_total_gb, registered_at, task FROM sessions WHERE alias = ?1 AND network_id = ?2", [ALIAS, NET_ID]) as Row[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type OsRow = { resume_id: string; os_user: string | null };
|
|
94
|
+
function osRows(): OsRow[] {
|
|
95
|
+
return db.all("SELECT resume_id, os_user FROM sessions WHERE alias = ?1 AND network_id = ?2", [ALIAS, NET_ID]) as OsRow[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("report_status: 同 alias 换 resume_id 时,描述性列从被替换的那行接手", () => {
|
|
99
|
+
test("第二个上报者(grok-cli)不带 version/agent/遥测 → 这些列保留,status/task 以本次为准,仍只有一行", async () => {
|
|
100
|
+
seed();
|
|
101
|
+
const tools = buildHandlers();
|
|
102
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "working", task: "PING 已收到", network_id: NET_ID });
|
|
103
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
104
|
+
const all = rows();
|
|
105
|
+
expect(all.length).toBe(1);
|
|
106
|
+
const row = all[0]!;
|
|
107
|
+
expect(row.resume_id).toBe(RESUME_CLI);
|
|
108
|
+
expect(row.status).toBe("working");
|
|
109
|
+
expect(row.task).toBe("PING 已收到");
|
|
110
|
+
expect(row.version).toBe("2.5.0-preview.64");
|
|
111
|
+
expect(row.agent).toBe("agent-node:grok-build-cli");
|
|
112
|
+
expect(row.hostname).toBe("dev-box");
|
|
113
|
+
expect(row.cpu_cores).toBe(8);
|
|
114
|
+
expect(row.mem_total_gb).toBe(61.4);
|
|
115
|
+
expect(row.registered_at).toBe(OLD_REGISTERED_AT);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("上报里带了的列以上报为准(version 变了就用新的)", async () => {
|
|
119
|
+
seed();
|
|
120
|
+
const tools = buildHandlers();
|
|
121
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "idle", version: "9.9.9", hostname: "other-box", network_id: NET_ID });
|
|
122
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
123
|
+
const row = rows()[0]!;
|
|
124
|
+
expect(rows().length).toBe(1);
|
|
125
|
+
expect(row.version).toBe("9.9.9");
|
|
126
|
+
expect(row.hostname).toBe("other-box");
|
|
127
|
+
expect(row.agent).toBe("agent-node:grok-build-cli");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("再换回原 resume_id(sdk)也不丢:两个上报者轮流说话,version 始终在", async () => {
|
|
131
|
+
seed();
|
|
132
|
+
const tools = buildHandlers();
|
|
133
|
+
await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "working", network_id: NET_ID });
|
|
134
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "idle", network_id: NET_ID });
|
|
135
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
136
|
+
const all = rows();
|
|
137
|
+
expect(all.length).toBe(1);
|
|
138
|
+
expect(all[0]!.resume_id).toBe(RESUME_SDK);
|
|
139
|
+
expect(all[0]!.status).toBe("idle");
|
|
140
|
+
expect(all[0]!.version).toBe("2.5.0-preview.64");
|
|
141
|
+
expect(all[0]!.registered_at).toBe(OLD_REGISTERED_AT);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("没有别的行可接手时(全新 alias)行为不变:INSERT 一行,registered_at 取 now", async () => {
|
|
145
|
+
seed();
|
|
146
|
+
db.run("DELETE FROM sessions WHERE network_id = ?1", [NET_ID]);
|
|
147
|
+
const tools = buildHandlers();
|
|
148
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "idle", network_id: NET_ID });
|
|
149
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
150
|
+
const all = rows();
|
|
151
|
+
expect(all.length).toBe(1);
|
|
152
|
+
expect(all[0]!.version).toBeNull();
|
|
153
|
+
expect(all[0]!.registered_at).not.toBe(OLD_REGISTERED_AT);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("same resume_id stores os_user, and a later report that omits it keeps the value", async () => {
|
|
157
|
+
seed();
|
|
158
|
+
const tools = buildHandlers();
|
|
159
|
+
const set = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "working", os_user: "alice", network_id: NET_ID });
|
|
160
|
+
expect(set.ok, JSON.stringify(set)).toBe(true);
|
|
161
|
+
expect(osRows()).toEqual([{ resume_id: RESUME_SDK, os_user: "alice" }]);
|
|
162
|
+
const keep = await call(tools.report_status!, { resume_id: RESUME_SDK, alias: ALIAS, status: "idle", network_id: NET_ID });
|
|
163
|
+
expect(keep.ok, JSON.stringify(keep)).toBe(true);
|
|
164
|
+
expect(osRows()).toEqual([{ resume_id: RESUME_SDK, os_user: "alice" }]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("换 resume_id 且本次不带 os_user 时,从被替换的那行接手", async () => {
|
|
168
|
+
seed();
|
|
169
|
+
db.run("UPDATE sessions SET os_user = ?1 WHERE resume_id = ?2", ["alice", RESUME_SDK]);
|
|
170
|
+
const tools = buildHandlers();
|
|
171
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "working", network_id: NET_ID });
|
|
172
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
173
|
+
expect(osRows()).toEqual([{ resume_id: RESUME_CLI, os_user: "alice" }]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("换 resume_id 且本次带了 os_user 时,以上报为准", async () => {
|
|
177
|
+
seed();
|
|
178
|
+
db.run("UPDATE sessions SET os_user = ?1 WHERE resume_id = ?2", ["alice", RESUME_SDK]);
|
|
179
|
+
const tools = buildHandlers();
|
|
180
|
+
const r = await call(tools.report_status!, { resume_id: RESUME_CLI, alias: ALIAS, status: "idle", os_user: "bob", network_id: NET_ID });
|
|
181
|
+
expect(r.ok, JSON.stringify(r)).toBe(true);
|
|
182
|
+
expect(osRows()).toEqual([{ resume_id: RESUME_CLI, os_user: "bob" }]);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// #1474 (#909) — `report_status` accepts the NAMES of the env keys a node has.
|
|
2
|
+
//
|
|
3
|
+
// Called through a real MCP client (InMemoryTransport) rather than the raw
|
|
4
|
+
// handler: the shape rules live in the tool's zod schema, and invoking the
|
|
5
|
+
// handler directly bypasses them — the first version of this file "passed" a
|
|
6
|
+
// `lower-case` key because no validation ran.
|
|
7
|
+
//
|
|
8
|
+
// Two behaviours are load-bearing:
|
|
9
|
+
// 1. `[]` is a REAL report ("this node has no secrets") and must overwrite the
|
|
10
|
+
// stored list; only an ABSENT field keeps the previous value.
|
|
11
|
+
// 2. A value accidentally placed in the array must be rejected at the
|
|
12
|
+
// boundary, not stored and later rendered.
|
|
13
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { db } from "./db.js";
|
|
18
|
+
import { registerTools } from "./tools.js";
|
|
19
|
+
|
|
20
|
+
const NET = "net_secret_keys";
|
|
21
|
+
const USER = "u_secret_keys";
|
|
22
|
+
const RESUME = "r_secret_keys";
|
|
23
|
+
const ALIAS = "sk-node";
|
|
24
|
+
|
|
25
|
+
type ToolResult = { content: Array<{ type: string; text?: string }>; isError?: boolean };
|
|
26
|
+
|
|
27
|
+
async function connectClient() {
|
|
28
|
+
const server = new McpServer({ name: "secret-keys-test", version: "1" });
|
|
29
|
+
// report_status is a node heartbeat: the harness presents a NETWORK token.
|
|
30
|
+
registerTools(server, undefined, NET, USER, ALIAS, true, null);
|
|
31
|
+
const client = new Client({ name: "secret-keys-client", version: "1" });
|
|
32
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
33
|
+
await server.connect(serverTransport);
|
|
34
|
+
await client.connect(clientTransport);
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const report = (extra: Record<string, unknown> = {}) => ({
|
|
39
|
+
resume_id: RESUME, alias: ALIAS, status: "idle", network_id: NET, ...extra,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function call(client: Client, args: Record<string, unknown>): Promise<{ ok: boolean; body: any }> {
|
|
43
|
+
const res = (await client.callTool({ name: "report_status", arguments: args })) as ToolResult;
|
|
44
|
+
const text = res.content[0]?.text;
|
|
45
|
+
if (!text) throw new Error("missing text tool result");
|
|
46
|
+
// A schema rejection comes back as a plain error string ("MCP error ..."),
|
|
47
|
+
// not JSON: that is exactly the boundary we want to observe.
|
|
48
|
+
let body: any;
|
|
49
|
+
try { body = JSON.parse(text); } catch { body = { error: text }; }
|
|
50
|
+
return { ok: res.isError !== true, body };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stored = () =>
|
|
54
|
+
db.get<{ secret_keys: string | null }>("SELECT secret_keys FROM sessions WHERE resume_id = ?1", RESUME)?.secret_keys ?? null;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
db.run("INSERT OR IGNORE INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?2, 'x', 'user', datetime('now'))", [USER, USER]);
|
|
58
|
+
db.run("INSERT OR IGNORE INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))", [NET, USER]);
|
|
59
|
+
db.run("INSERT OR IGNORE INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'owner', datetime('now'))", [USER, NET]);
|
|
60
|
+
db.run("DELETE FROM sessions WHERE resume_id = ?1", [RESUME]);
|
|
61
|
+
db.run(
|
|
62
|
+
`INSERT INTO sessions (resume_id, alias, status, network_id, updated_at, last_seen_at)
|
|
63
|
+
VALUES (?1, ?2, 'idle', ?3, datetime('now'), datetime('now'))`,
|
|
64
|
+
[RESUME, ALIAS, NET],
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("report_status — secret key names", () => {
|
|
69
|
+
test("stores the names as a JSON array", async () => {
|
|
70
|
+
const client = await connectClient();
|
|
71
|
+
const r = await call(client, report({ secret_keys: ["ALPHA_KEY", "BETA_KEY"] }));
|
|
72
|
+
expect(r.body.ok).toBe(true);
|
|
73
|
+
expect(stored()).toBe(JSON.stringify(["ALPHA_KEY", "BETA_KEY"]));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("an absent field keeps the previous list", async () => {
|
|
77
|
+
const client = await connectClient();
|
|
78
|
+
await call(client, report({ secret_keys: ["ALPHA_KEY"] }));
|
|
79
|
+
await call(client, report());
|
|
80
|
+
expect(stored()).toBe(JSON.stringify(["ALPHA_KEY"]));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("an EMPTY array is a real report and clears the list", async () => {
|
|
84
|
+
const client = await connectClient();
|
|
85
|
+
await call(client, report({ secret_keys: ["ALPHA_KEY"] }));
|
|
86
|
+
await call(client, report({ secret_keys: [] }));
|
|
87
|
+
expect(stored()).toBe("[]");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a malformed name is rejected at the boundary and nothing is written", async () => {
|
|
91
|
+
const client = await connectClient();
|
|
92
|
+
await call(client, report({ secret_keys: ["ALPHA_KEY"] }));
|
|
93
|
+
const r = await call(client, report({ secret_keys: ["lower-case"] }));
|
|
94
|
+
expect(r.ok).toBe(false);
|
|
95
|
+
expect(stored()).toBe(JSON.stringify(["ALPHA_KEY"]));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("more than 50 names is rejected", async () => {
|
|
99
|
+
const client = await connectClient();
|
|
100
|
+
const many = Array.from({ length: 51 }, (_, i) => `KEY_${String(i).padStart(3, "0")}`);
|
|
101
|
+
const r = await call(client, report({ secret_keys: many }));
|
|
102
|
+
expect(r.ok).toBe(false);
|
|
103
|
+
expect(stored()).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a value-shaped string is rejected (the field is a name list, not free text)", async () => {
|
|
107
|
+
const client = await connectClient();
|
|
108
|
+
const value = "ghp_" + "A".repeat(40);
|
|
109
|
+
const r = await call(client, report({ secret_keys: [value] }));
|
|
110
|
+
expect(r.ok).toBe(false);
|
|
111
|
+
expect(stored()).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// #1510 — the node reports how it reaches this hub; reporting only.
|
|
116
|
+
describe("report_status — env_transport", () => {
|
|
117
|
+
test("stores the reported link kind", async () => {
|
|
118
|
+
const client = await connectClient();
|
|
119
|
+
const r = await call(client, report({ env_transport: "loopback" }));
|
|
120
|
+
expect(r.body.ok).toBe(true);
|
|
121
|
+
const row = db.get<{ env_transport: string | null }>("SELECT env_transport FROM sessions WHERE resume_id = ?1", RESUME);
|
|
122
|
+
expect(row?.env_transport).toBe("loopback");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("an unknown value is rejected at the boundary", async () => {
|
|
126
|
+
const client = await connectClient();
|
|
127
|
+
const r = await call(client, report({ env_transport: "carrier-pigeon" }));
|
|
128
|
+
expect(r.ok).toBe(false);
|
|
129
|
+
const row = db.get<{ env_transport: string | null }>("SELECT env_transport FROM sessions WHERE resume_id = ?1", RESUME);
|
|
130
|
+
expect(row?.env_transport ?? null).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("an absent field keeps the previous value", async () => {
|
|
134
|
+
const client = await connectClient();
|
|
135
|
+
await call(client, report({ env_transport: "https" }));
|
|
136
|
+
await call(client, report());
|
|
137
|
+
const row = db.get<{ env_transport: string | null }>("SELECT env_transport FROM sessions WHERE resume_id = ?1", RESUME);
|
|
138
|
+
expect(row?.env_transport).toBe("https");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// #434: real-server integration tests must know their DB before importing
|
|
2
|
+
// auth/db/server modules. Setting COMMHUB_DB in beforeAll is too late because
|
|
3
|
+
// ESM dependencies have already evaluated the db singleton.
|
|
4
|
+
if (!process.env.COMMHUB_DB) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"explicit_test_database_required: run `cd server && bun run test`, " +
|
|
7
|
+
"or set COMMHUB_DB to an isolated temporary path before running this file",
|
|
8
|
+
);
|
|
9
|
+
}
|