@sleep2agi/commhub-server 0.9.0-preview.13 → 0.9.0-preview.130
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-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 +294 -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 +598 -49
- 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/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 +48 -0
- package/src/create-node-unicode-name.test.ts +120 -0
- package/src/create-node-validate.test.ts +193 -7
- package/src/create-node-validate.ts +227 -15
- package/src/create-node-workdir.test.ts +289 -0
- package/src/create-node.ts +77 -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 +107 -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 +930 -35
- 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 +260 -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/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/http-gzip.test.ts +94 -0
- package/src/http-gzip.ts +112 -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/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/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 +781 -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 +130 -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-skills-transport.test.ts +209 -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 +431 -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/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 +22 -2
- 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 +150 -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 +127 -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 +288 -0
- package/src/requirements-stats.ts +167 -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 +129 -0
- package/src/rest-identity.ts +125 -0
- package/src/rest-projections.ts +68 -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-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-reply-unread-http.test.ts +212 -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 +238 -0
- package/src/scheduled-stuck.ts +153 -0
- package/src/scheduled-tasks-http.test.ts +638 -0
- package/src/scheduled-tasks.ts +766 -0
- package/src/scoped-secrets.ts +124 -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 +5257 -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 -1
- package/src/task-access-http.test.ts +402 -0
- package/src/task-access.ts +420 -0
- package/src/task-auth-origin-http.test.ts +91 -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 +141 -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-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 +3927 -323
- 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,289 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import "./require-explicit-test-db.js";
|
|
3
|
+
import { db } from "./db.js";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { registerTools } from "./tools.js";
|
|
6
|
+
import { daemonDefaultWorkdirRoot, validateWorkdir, ValidationError } from "./create-node-validate.js";
|
|
7
|
+
import { addNetworkMember, login, register } from "./auth.js";
|
|
8
|
+
|
|
9
|
+
// app「新建节点」工作目录 —— hub 这一层管三件事:
|
|
10
|
+
// ① node_spec.workdir 的**形状**(绝对 / ~/… / C:\…),派发前就拒
|
|
11
|
+
// ② daemon 没自报 default_workdir_root(= 老 daemon,会静默忽略 workdir)⇒ 拒,不派
|
|
12
|
+
// ③ 请求行存下 workdir,get_create_request 原样交给 daemon;不带就**不出现**这个键
|
|
13
|
+
// 以及 /api/host-supervisors 只给 admin/owner 透出默认根(它是那台机器的家目录路径)。
|
|
14
|
+
|
|
15
|
+
const NET = "net_workdir_gate";
|
|
16
|
+
const USER_ID = "u_workdir_admin";
|
|
17
|
+
const DAEMON_ID = "node_workdir_daemon";
|
|
18
|
+
const DAEMON_ALIAS = "workdir-daemon";
|
|
19
|
+
const DAEMON_TOK = "tok_workdir_daemon";
|
|
20
|
+
const ROOT = "/home/user";
|
|
21
|
+
|
|
22
|
+
interface ToolHandler { (args: any, extra?: any): Promise<{ content: Array<{ type: "text"; text: string }> }>; }
|
|
23
|
+
interface Reply { ok?: boolean; error?: string; request_id?: string; node_spec?: any; [k: string]: unknown; }
|
|
24
|
+
|
|
25
|
+
function cleanup() {
|
|
26
|
+
for (const t of ["node_create_requests", "audit_log", "nodes", "sessions", "api_tokens", "network_members"]) {
|
|
27
|
+
try { db.run(`DELETE FROM ${t} WHERE network_id = ?1`, [NET]); } catch {}
|
|
28
|
+
}
|
|
29
|
+
try { db.run("DELETE FROM networks WHERE network_id = ?1", [NET]); } catch {}
|
|
30
|
+
try { db.run("DELETE FROM users WHERE user_id = ?1", [USER_ID]); } catch {}
|
|
31
|
+
}
|
|
32
|
+
beforeEach(cleanup);
|
|
33
|
+
afterAll(cleanup);
|
|
34
|
+
|
|
35
|
+
function seed(caps: Record<string, unknown>) {
|
|
36
|
+
db.run(`INSERT INTO users (user_id, username, password_hash, role, created_at) VALUES (?1, ?1, 'x', 'user', datetime('now'))`, [USER_ID]);
|
|
37
|
+
db.run(`INSERT OR REPLACE INTO networks (network_id, network_name, owner_id, created_at) VALUES (?1, ?1, ?2, datetime('now'))`, [NET, USER_ID]);
|
|
38
|
+
db.run(`INSERT INTO network_members (user_id, network_id, role, joined_at) VALUES (?1, ?2, 'admin', datetime('now'))`, [USER_ID, NET]);
|
|
39
|
+
db.run(
|
|
40
|
+
`INSERT INTO nodes (node_id, node_name, alias, network_id, config_snapshot, hostname, created_at, updated_at, lifecycle_state)
|
|
41
|
+
VALUES (?1, ?2, ?2, ?3, ?4, 'host-x', datetime('now'), datetime('now'), 'active')`,
|
|
42
|
+
[DAEMON_ID, DAEMON_ALIAS, NET, JSON.stringify({ role: "host_supervisor", daemon_capabilities: { runtimes_supported: ["claude-agent-sdk"], ...caps } })],
|
|
43
|
+
);
|
|
44
|
+
db.run(
|
|
45
|
+
`INSERT INTO api_tokens (token_id, user_id, network_id, scope, name, token_hash, expires_at, revoked_at)
|
|
46
|
+
VALUES (?1, ?2, ?3, 'network', ?4, ?5, NULL, NULL)`,
|
|
47
|
+
[DAEMON_TOK, USER_ID, NET, `node:${DAEMON_ALIAS}`, `hash_${DAEMON_TOK}`],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function tools(callerTokenId: string | null = null): Record<string, ToolHandler> {
|
|
52
|
+
const server = new McpServer({ name: "test", version: "0" }) as any;
|
|
53
|
+
const out: Record<string, ToolHandler> = {};
|
|
54
|
+
const origTool = server.tool.bind(server);
|
|
55
|
+
server.tool = (name: string, d: string, schema: any, handler: ToolHandler) => { out[name] = handler; return origTool(name, d, schema, handler); };
|
|
56
|
+
const origRegisterTool = server.registerTool?.bind(server);
|
|
57
|
+
if (origRegisterTool) {
|
|
58
|
+
server.registerTool = (name: string, cfg: any, handler: ToolHandler) => { out[name] = handler; return origRegisterTool(name, cfg, handler); };
|
|
59
|
+
}
|
|
60
|
+
registerTools(server, undefined, NET, USER_ID, null, true, callerTokenId);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function call(h: ToolHandler, args: any): Promise<Reply> {
|
|
65
|
+
return JSON.parse((await h(args)).content[0].text) as Reply;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const baseSpec = { name: "wd-child", runtime: "claude-agent-sdk", model: "claude-sonnet-4-5", flags: {} };
|
|
69
|
+
|
|
70
|
+
function rowCount(): number {
|
|
71
|
+
return db.get<{ n: number }>("SELECT COUNT(*) AS n FROM node_create_requests WHERE daemon_node_id = ?1", DAEMON_ID)?.n ?? -1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("validateWorkdir — shape only", () => {
|
|
75
|
+
const ok = ["/home/user/proj", "/srv/x", "~", "~/proj", "C:\\Users\\alice\\proj", "D:/work", " /padded "];
|
|
76
|
+
for (const v of ok) test(`accepts ${JSON.stringify(v)}`, () => { expect(() => validateWorkdir(v)).not.toThrow(); });
|
|
77
|
+
test("accepts undefined / null (field omitted)", () => {
|
|
78
|
+
expect(() => validateWorkdir(undefined)).not.toThrow();
|
|
79
|
+
expect(() => validateWorkdir(null)).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
const bad: unknown[] = ["", " ", "proj", "./proj", "../x", "~alice/x", "C:proj", "/a\nb", "/a\u0000b", "x".repeat(10) + "/", "/" + "a".repeat(1024), 42, {}];
|
|
82
|
+
for (const v of bad) {
|
|
83
|
+
test(`rejects ${JSON.stringify(v)?.slice(0, 40)}`, () => {
|
|
84
|
+
let code = "";
|
|
85
|
+
try { validateWorkdir(v); } catch (e) { code = (e as ValidationError).code; }
|
|
86
|
+
expect(code).toBe("workdir_invalid");
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("daemonDefaultWorkdirRoot — sanitize the self-report", () => {
|
|
92
|
+
const snap = (v: unknown) => JSON.stringify({ daemon_capabilities: { default_workdir_root: v } });
|
|
93
|
+
test("absolute posix root passes through", () => { expect(daemonDefaultWorkdirRoot(snap(ROOT))).toBe(ROOT); });
|
|
94
|
+
test("windows root passes through", () => { expect(daemonDefaultWorkdirRoot(snap("C:\\Users\\alice"))).toBe("C:\\Users\\alice"); });
|
|
95
|
+
test("object snapshot (not string) also accepted", () => {
|
|
96
|
+
expect(daemonDefaultWorkdirRoot({ daemon_capabilities: { default_workdir_root: ROOT } })).toBe(ROOT);
|
|
97
|
+
});
|
|
98
|
+
for (const v of [undefined, null, "", "relative", "~/x", 7, "/a\nb", "/" + "a".repeat(1100)]) {
|
|
99
|
+
test(`not reported / malformed ${JSON.stringify(v)?.slice(0, 30)} → null`, () => {
|
|
100
|
+
expect(daemonDefaultWorkdirRoot(snap(v))).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
test("null / garbage snapshot → null", () => {
|
|
104
|
+
expect(daemonDefaultWorkdirRoot(null)).toBeNull();
|
|
105
|
+
expect(daemonDefaultWorkdirRoot("{not json")).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("create_node + workdir", () => {
|
|
110
|
+
test("#829 V2 flags survive Hub storage and daemon pull", async () => {
|
|
111
|
+
seed({ runtimes_supported: ["opencode-cli"] });
|
|
112
|
+
const flags = { opencodeGeneration: "v2", opencodeUnsafeTools: true, timeout: 600000 };
|
|
113
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { name: "v2-child", runtime: "opencode-cli", model: "stub/model", flags } });
|
|
114
|
+
expect(r.ok).toBe(true);
|
|
115
|
+
const row = db.get<{ flags_json: string }>("SELECT flags_json FROM node_create_requests WHERE request_id = ?1", r.request_id);
|
|
116
|
+
expect(JSON.parse(row!.flags_json)).toEqual(flags);
|
|
117
|
+
const pulled = await call(tools(DAEMON_TOK).get_create_request, { request_id: r.request_id });
|
|
118
|
+
expect(pulled.ok).toBe(true);
|
|
119
|
+
expect(pulled.node_spec.flags).toEqual(flags);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("#829 V2 without native provider/model rejected before request or child token mint", async () => {
|
|
123
|
+
seed({ runtimes_supported: ["opencode-cli"] });
|
|
124
|
+
const tokenCount = () => db.get<{ n: number }>("SELECT COUNT(*) AS n FROM api_tokens WHERE network_id = ?1", NET)!.n;
|
|
125
|
+
const before = tokenCount();
|
|
126
|
+
const flags = { opencodeGeneration: "v2", opencodeUnsafeTools: true };
|
|
127
|
+
const missing = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { name: "v2-child", runtime: "opencode-cli", flags } });
|
|
128
|
+
expect(missing.error).toBe("opencode_v2_requires_provider_model");
|
|
129
|
+
const bare = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { name: "v2-child", runtime: "opencode-cli", model: "opencode", flags } });
|
|
130
|
+
expect(bare.error).toBe("opencode_v2_requires_provider_model");
|
|
131
|
+
expect(rowCount()).toBe(0);
|
|
132
|
+
expect(tokenCount()).toBe(before);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("#829 missing opt-in rejected before request or child token mint", async () => {
|
|
136
|
+
seed({ runtimes_supported: ["opencode-cli"] });
|
|
137
|
+
const tokenCount = () => db.get<{ n: number }>("SELECT COUNT(*) AS n FROM api_tokens WHERE network_id = ?1", NET)!.n;
|
|
138
|
+
const before = tokenCount();
|
|
139
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { name: "v2-child", runtime: "opencode-cli", flags: { opencodeGeneration: "v2" } } });
|
|
140
|
+
expect(r.error).toBe("opencode_v2_requires_unsafe_opt_in");
|
|
141
|
+
expect(rowCount()).toBe(0);
|
|
142
|
+
expect(tokenCount()).toBe(before);
|
|
143
|
+
});
|
|
144
|
+
test("🔴 old daemon (no default_workdir_root) + workdir → workdir_not_supported_by_daemon, NO row", async () => {
|
|
145
|
+
seed({});
|
|
146
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { ...baseSpec, workdir: `${ROOT}/wd-child` } });
|
|
147
|
+
expect(r.ok).toBe(false);
|
|
148
|
+
expect(r.error).toBe("workdir_not_supported_by_daemon");
|
|
149
|
+
expect(rowCount()).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("old daemon WITHOUT workdir still dispatches (no regression for existing callers)", async () => {
|
|
153
|
+
seed({});
|
|
154
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: baseSpec });
|
|
155
|
+
expect(r.ok).toBe(true);
|
|
156
|
+
const row = db.get<{ workdir: string | null }>("SELECT workdir FROM node_create_requests WHERE request_id = ?1", r.request_id);
|
|
157
|
+
expect(row?.workdir).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("bad shape rejected before dispatch → workdir_invalid, NO row", async () => {
|
|
161
|
+
seed({ default_workdir_root: ROOT });
|
|
162
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { ...baseSpec, workdir: "relative/dir" } });
|
|
163
|
+
expect(r.error).toBe("workdir_invalid");
|
|
164
|
+
expect(rowCount()).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("new daemon + workdir → row stores it and get_create_request hands it to the daemon", async () => {
|
|
168
|
+
seed({ default_workdir_root: ROOT });
|
|
169
|
+
const wd = `${ROOT}/wd-child`;
|
|
170
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { ...baseSpec, workdir: wd } });
|
|
171
|
+
expect(r.ok).toBe(true);
|
|
172
|
+
const row = db.get<{ workdir: string | null }>("SELECT workdir FROM node_create_requests WHERE request_id = ?1", r.request_id);
|
|
173
|
+
expect(row?.workdir).toBe(wd);
|
|
174
|
+
const g = await call(tools(DAEMON_TOK).get_create_request, { request_id: r.request_id });
|
|
175
|
+
expect(g.ok).toBe(true);
|
|
176
|
+
expect(g.node_spec.workdir).toBe(wd);
|
|
177
|
+
expect(g.node_spec.name).toBe(baseSpec.name);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("workdir is trimmed before storage", async () => {
|
|
181
|
+
seed({ default_workdir_root: ROOT });
|
|
182
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { ...baseSpec, workdir: ` ~/wd-child ` } });
|
|
183
|
+
expect(r.ok).toBe(true);
|
|
184
|
+
const row = db.get<{ workdir: string | null }>("SELECT workdir FROM node_create_requests WHERE request_id = ?1", r.request_id);
|
|
185
|
+
expect(row?.workdir).toBe("~/wd-child");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("new daemon WITHOUT workdir → get_create_request node_spec has NO workdir key (daemon keeps legacy layout)", async () => {
|
|
189
|
+
seed({ default_workdir_root: ROOT });
|
|
190
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: baseSpec });
|
|
191
|
+
expect(r.ok).toBe(true);
|
|
192
|
+
const g = await call(tools(DAEMON_TOK).get_create_request, { request_id: r.request_id });
|
|
193
|
+
expect(g.ok).toBe(true);
|
|
194
|
+
expect("workdir" in g.node_spec).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("audit detail records the requested workdir", async () => {
|
|
198
|
+
seed({ default_workdir_root: ROOT });
|
|
199
|
+
const wd = `${ROOT}/wd-child`;
|
|
200
|
+
const r = await call(tools().create_node, { daemon_node_id: DAEMON_ID, node_spec: { ...baseSpec, workdir: wd } });
|
|
201
|
+
const a = db.get<{ detail: string }>(
|
|
202
|
+
"SELECT detail FROM audit_log WHERE target_id = ?1 AND action = 'create_node_dispatch_attempted'", r.request_id,
|
|
203
|
+
);
|
|
204
|
+
expect(JSON.parse(a?.detail || "{}").workdir).toBe(wd);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("list_host_supervisors (MCP) surfaces default_workdir_root for admin", async () => {
|
|
208
|
+
seed({ default_workdir_root: ROOT });
|
|
209
|
+
const r = await call(tools().list_host_supervisors, {});
|
|
210
|
+
const d = (r as any).daemons.find((x: any) => x.daemon_node_id === DAEMON_ID);
|
|
211
|
+
expect(d.default_workdir_root).toBe(ROOT);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("list_host_supervisors (MCP) omits the key for an old daemon", async () => {
|
|
215
|
+
seed({});
|
|
216
|
+
const r = await call(tools().list_host_supervisors, {});
|
|
217
|
+
const d = (r as any).daemons.find((x: any) => x.daemon_node_id === DAEMON_ID);
|
|
218
|
+
expect(d).toBeDefined();
|
|
219
|
+
expect("default_workdir_root" in d).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── REST /api/host-supervisors: admin sees the root, member does not ──────────
|
|
224
|
+
let BASE = "";
|
|
225
|
+
let server: any = null;
|
|
226
|
+
let ownerTok = "", memberTok = "", restNet = "", ownerId = "";
|
|
227
|
+
describe("/api/host-supervisors — default_workdir_root visibility", () => {
|
|
228
|
+
beforeAll(async () => {
|
|
229
|
+
process.env.HOST = "127.0.0.1";
|
|
230
|
+
const sfx = `${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
|
231
|
+
const pw = "BootstrapPw123Aa!";
|
|
232
|
+
const s = register(`wd_seed_${sfx}`, pw, undefined, "seed");
|
|
233
|
+
if (!s.ok) throw new Error("seed failed");
|
|
234
|
+
db.run("UPDATE users SET role = 'admin' WHERE username = ?1", [`wd_seed_${sfx}`]);
|
|
235
|
+
const o = register(`wd_owner_${sfx}`, pw, undefined, "seed");
|
|
236
|
+
if (!o.ok || !o.token) throw new Error("owner register failed");
|
|
237
|
+
ownerTok = o.token; restNet = o.network_id ?? "";
|
|
238
|
+
ownerId = login(`wd_owner_${sfx}`, pw).user!.user_id;
|
|
239
|
+
const m = register(`wd_member_${sfx}`, pw, undefined, "seed");
|
|
240
|
+
if (!m.ok || !m.token) throw new Error("member register failed");
|
|
241
|
+
memberTok = m.token;
|
|
242
|
+
const memberId = login(`wd_member_${sfx}`, pw).user!.user_id;
|
|
243
|
+
// 多用户 Agent 权限:新成员默认只看授权 Agent;本测试测的是「完全信任的成员」旧语义,显式 agent_access=all。
|
|
244
|
+
const add = addNetworkMember(restNet, memberId, "member", undefined, { agentAccess: "all" });
|
|
245
|
+
if (!add.ok) throw new Error("add member failed: " + add.error);
|
|
246
|
+
const { bootServer } = await import("./server.js");
|
|
247
|
+
server = bootServer({ port: 0, hostname: "127.0.0.1" });
|
|
248
|
+
BASE = `http://127.0.0.1:${server.port}`;
|
|
249
|
+
for (const [alias, caps] of [["wd-new", { default_workdir_root: ROOT }], ["wd-old", {}]] as const) {
|
|
250
|
+
db.run(
|
|
251
|
+
`INSERT OR REPLACE INTO nodes (node_id, node_name, alias, runtime, model, config_path, channels, server, hostname, network_id, config_revision, config_snapshot)
|
|
252
|
+
VALUES (?1, ?2, ?2, 'claude-agent-sdk', 'm', '/tmp/cfg.json', '[]', 'h', 'h', ?3, 0, ?4)`,
|
|
253
|
+
[`wd_n_${alias}`, alias, restNet, JSON.stringify({ role: "host_supervisor", daemon_capabilities: caps })],
|
|
254
|
+
);
|
|
255
|
+
db.run(
|
|
256
|
+
`INSERT OR REPLACE INTO api_tokens (token_id, token_hash, user_id, network_id, name, scope) VALUES (?1, ?2, ?3, ?4, ?5, 'network')`,
|
|
257
|
+
[`wd_t_${alias}`, `hash_wd_${alias}`, ownerId, restNet, `node:${alias}`],
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
afterAll(() => {
|
|
262
|
+
try { server?.stop?.(true); } catch {}
|
|
263
|
+
try { db.run("DELETE FROM nodes WHERE network_id = ?1", [restNet]); } catch {}
|
|
264
|
+
try { db.run("DELETE FROM api_tokens WHERE network_id = ?1 AND name LIKE 'node:%'", [restNet]); } catch {}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
async function list(tok: string): Promise<any[]> {
|
|
268
|
+
const res = await fetch(`${BASE}/api/host-supervisors?network_id=${encodeURIComponent(restNet)}`, { headers: { Authorization: `Bearer ${tok}` } });
|
|
269
|
+
const b = await res.json();
|
|
270
|
+
if (!b.ok) throw new Error("list failed: " + JSON.stringify(b));
|
|
271
|
+
return b.daemons;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
test("owner sees default_workdir_root on the new daemon", async () => {
|
|
275
|
+
const d = (await list(ownerTok)).find(x => x.alias === "wd-new");
|
|
276
|
+
expect(d?.default_workdir_root).toBe(ROOT);
|
|
277
|
+
});
|
|
278
|
+
test("old daemon row has NO default_workdir_root key (app hides the row)", async () => {
|
|
279
|
+
const d = (await list(ownerTok)).find(x => x.alias === "wd-old");
|
|
280
|
+
expect(d).toBeDefined();
|
|
281
|
+
expect("default_workdir_root" in d).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
test("🔴 member does NOT see the host's home path", async () => {
|
|
284
|
+
const rows = await list(memberTok);
|
|
285
|
+
const d = rows.find(x => x.alias === "wd-new");
|
|
286
|
+
expect(d).toBeDefined();
|
|
287
|
+
expect("default_workdir_root" in d).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
package/src/create-node.ts
CHANGED
|
@@ -232,40 +232,91 @@ export type DaemonResolveResult =
|
|
|
232
232
|
| { ok: true; daemonNodeId: string; daemonAlias: string; networkId: string }
|
|
233
233
|
| { ok: false; error: string };
|
|
234
234
|
|
|
235
|
+
export type NodeCallerResolution =
|
|
236
|
+
| { ok: true; kind: "node"; nodeId: string; alias: string; networkId: string }
|
|
237
|
+
| { ok: true; kind: "unregistered"; alias: string; networkId: string }
|
|
238
|
+
| { ok: false; reason: "not_a_node_token" | "bound_node_missing" | "ambiguous" | "not_owner" };
|
|
239
|
+
|
|
240
|
+
type NodeCallerRow = { node_id: string; alias: string; network_id: string; owner_user_id: string | null };
|
|
241
|
+
|
|
242
|
+
// nodes has no unique (network, alias). An unbound token that hits several
|
|
243
|
+
// rows is not rejected outright: the row owned by the token's issuer wins;
|
|
244
|
+
// if that is still not unique, the node_id on the latest session for the
|
|
245
|
+
// alias wins. Still not unique, then refuse.
|
|
246
|
+
function disambiguateUnboundAlias(
|
|
247
|
+
rows: NodeCallerRow[],
|
|
248
|
+
userId: string | null,
|
|
249
|
+
alias: string,
|
|
250
|
+
networkId: string,
|
|
251
|
+
): NodeCallerRow | undefined {
|
|
252
|
+
const owned = userId ? rows.filter((row) => row.owner_user_id === userId) : [];
|
|
253
|
+
if (owned.length === 1) return owned[0]; // board679-unbound-dup
|
|
254
|
+
const pool = owned.length > 1 ? owned : rows;
|
|
255
|
+
const recent = db.get<{ node_id: string | null }>(
|
|
256
|
+
`SELECT node_id FROM sessions WHERE alias = ?1 AND network_id = ?2 AND node_id IS NOT NULL ORDER BY updated_at DESC LIMIT 1`,
|
|
257
|
+
alias,
|
|
258
|
+
networkId,
|
|
259
|
+
);
|
|
260
|
+
const hit = recent?.node_id ? pool.filter((row) => row.node_id === recent.node_id) : [];
|
|
261
|
+
return hit.length === 1 ? hit[0] : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// One lookup for every caller-identity entry. The token name is a hint.
|
|
265
|
+
// bound_node_id is the row: if that row is gone, refuse — do not look up by name.
|
|
266
|
+
// An unbound token with no row in its network stays the token name. First
|
|
267
|
+
// registration (report_status) still depends on that; sending and subscribing
|
|
268
|
+
// do not get a second exception.
|
|
269
|
+
export function resolveNodeCaller(tokenId: string | null | undefined): NodeCallerResolution {
|
|
270
|
+
if (!tokenId) return { ok: false, reason: "not_a_node_token" };
|
|
271
|
+
const tokRow = db.get<{ name: string; network_id: string | null; user_id: string | null; bound_node_id: string | null; node_identity_epoch: number }>(
|
|
272
|
+
`SELECT name, network_id, user_id, bound_node_id, node_identity_epoch FROM api_tokens WHERE token_id = ?1 AND revoked_at IS NULL`,
|
|
273
|
+
tokenId,
|
|
274
|
+
);
|
|
275
|
+
if (!tokRow || !tokRow.name || !tokRow.name.startsWith("node:") || !tokRow.network_id) {
|
|
276
|
+
return { ok: false, reason: "not_a_node_token" };
|
|
277
|
+
}
|
|
278
|
+
const tokenAlias = tokRow.name.slice(5);
|
|
279
|
+
type DaemonRow = { node_id: string; alias: string; network_id: string; owner_user_id: string | null };
|
|
280
|
+
const rows = tokRow.bound_node_id
|
|
281
|
+
? db.all<DaemonRow>(`SELECT node_id, alias, network_id, owner_user_id FROM nodes WHERE node_id = ?1 AND network_id = ?2`, tokRow.bound_node_id, tokRow.network_id)
|
|
282
|
+
: db.all<DaemonRow>(`SELECT node_id, alias, network_id, owner_user_id FROM nodes WHERE alias = ?1 AND network_id = ?2`, tokenAlias, tokRow.network_id);
|
|
283
|
+
const nodeRow = rows.length === 1
|
|
284
|
+
? rows[0]
|
|
285
|
+
: (!tokRow.bound_node_id && rows.length > 1
|
|
286
|
+
? disambiguateUnboundAlias(rows, tokRow.user_id, tokenAlias, tokRow.network_id)
|
|
287
|
+
: undefined);
|
|
288
|
+
// board679-bound-row-missing: a bound token with no row is refused. Do not look up by name.
|
|
289
|
+
if (tokRow.bound_node_id && !nodeRow) return { ok: false, reason: "bound_node_missing" };
|
|
290
|
+
if (!nodeRow && rows.length === 0) {
|
|
291
|
+
return { ok: true, kind: "unregistered", alias: tokenAlias, networkId: tokRow.network_id };
|
|
292
|
+
}
|
|
293
|
+
if (!nodeRow) return { ok: false, reason: "ambiguous" };
|
|
294
|
+
// Legacy unbound tokens keep their owner's authority. A token name is only a hint.
|
|
295
|
+
const ownsNode = !!tokRow.user_id && tokRow.user_id === nodeRow.owner_user_id;
|
|
296
|
+
const boundToNode = tokRow.bound_node_id === nodeRow.node_id;
|
|
297
|
+
const legacyOwnerless = nodeRow.owner_user_id === null && tokRow.node_identity_epoch === 0;
|
|
298
|
+
if (!ownsNode && !boundToNode && !legacyOwnerless) return { ok: false, reason: "not_owner" };
|
|
299
|
+
return { ok: true, kind: "node", nodeId: nodeRow.node_id, alias: nodeRow.alias, networkId: nodeRow.network_id };
|
|
300
|
+
}
|
|
301
|
+
|
|
235
302
|
export function resolveCallerDaemonTokenBound(opts: {
|
|
236
303
|
callerTokenIsNetwork: boolean;
|
|
237
304
|
callerTokenId: string | null | undefined;
|
|
238
305
|
enforceNetworkId: string | null | undefined;
|
|
239
306
|
}): DaemonResolveResult {
|
|
240
|
-
if (!opts.callerTokenIsNetwork || !opts.callerTokenId) {
|
|
241
|
-
return { ok: false, error: "caller_not_a_daemon" };
|
|
242
|
-
}
|
|
243
|
-
if (!opts.enforceNetworkId) {
|
|
244
|
-
return { ok: false, error: "caller_not_a_daemon" };
|
|
245
|
-
}
|
|
246
|
-
const tokRow = db.get<{ name: string; network_id: string | null }>(
|
|
247
|
-
`SELECT name, network_id FROM api_tokens WHERE token_id = ?1 AND revoked_at IS NULL`,
|
|
248
|
-
opts.callerTokenId,
|
|
249
|
-
);
|
|
250
|
-
if (!tokRow || !tokRow.name || !tokRow.name.startsWith("node:")) {
|
|
251
|
-
return { ok: false, error: "caller_not_a_daemon" };
|
|
252
|
-
}
|
|
253
|
-
if (tokRow.network_id !== opts.enforceNetworkId) {
|
|
307
|
+
if (!opts.callerTokenIsNetwork || !opts.callerTokenId || !opts.enforceNetworkId) {
|
|
254
308
|
return { ok: false, error: "caller_not_a_daemon" };
|
|
255
309
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
tokenAlias, tokRow.network_id,
|
|
260
|
-
);
|
|
261
|
-
if (!nodeRow) {
|
|
310
|
+
const resolved = resolveNodeCaller(opts.callerTokenId);
|
|
311
|
+
// No row yet is not a daemon. First registration stays on report_status.
|
|
312
|
+
if (!resolved.ok || resolved.kind !== "node" || resolved.networkId !== opts.enforceNetworkId) {
|
|
262
313
|
return { ok: false, error: "caller_not_a_daemon" };
|
|
263
314
|
}
|
|
264
315
|
return {
|
|
265
316
|
ok: true,
|
|
266
|
-
daemonNodeId:
|
|
267
|
-
daemonAlias:
|
|
268
|
-
networkId:
|
|
317
|
+
daemonNodeId: resolved.nodeId,
|
|
318
|
+
daemonAlias: resolved.alias,
|
|
319
|
+
networkId: resolved.networkId,
|
|
269
320
|
};
|
|
270
321
|
}
|
|
271
322
|
|
|
@@ -306,6 +357,7 @@ export function auditCreateNodeStrict(input: {
|
|
|
306
357
|
export function auditCreateNode(input: {
|
|
307
358
|
action:
|
|
308
359
|
| "create_node_dispatched"
|
|
360
|
+
| "create_node_dispatch_attempted"
|
|
309
361
|
| "create_node_rejected"
|
|
310
362
|
| "create_node_succeeded"
|
|
311
363
|
| "create_node_sweeper_revoked"
|
|
@@ -317,6 +369,8 @@ export function auditCreateNode(input: {
|
|
|
317
369
|
// wider migration (see P1.1 issue).
|
|
318
370
|
| "stop_node_dispatched"
|
|
319
371
|
| "stop_node_completed"
|
|
372
|
+
| "start_node_dispatched"
|
|
373
|
+
| "start_node_completed"
|
|
320
374
|
| "delete_node_dispatched"
|
|
321
375
|
| "delete_node_completed"
|
|
322
376
|
| "forced_stop_with_in_flight"
|