@sleep2agi/commhub-server 0.9.0-preview.120 → 0.9.0-preview.121

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.
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { isReservedEnvKey } from "./shared/reserved-env.js";
11
11
  import { checkNodeName } from "./shared/node-name.js";
12
+ import { CODEX_CONFIG_ONLY_FLAG_KEYS, validateCodexConfigFlag } from "./codex-config-flags.js";
12
13
 
13
14
  export class ValidationError extends Error {
14
15
  constructor(public code: string, public detail?: Record<string, unknown>) {
@@ -52,7 +53,11 @@ export type Runtime = typeof RUNTIMES[number];
52
53
  // 🔴 故意放在 flags 里而不是 node_spec 顶层:老 hub 的 zod 会静默丢弃未知顶层字段、老 daemon 也静默忽略
53
54
  // —— 那正是 #584 的形状(用户选了共存,拿到无头节点,没有任何报错)。flags 的未知键在老 hub(这份
54
55
  // FLAG_KEYS)和老 daemon(buildAnetArgsDaemon)都**拒绝**,新 app 撞到老组件得到的是一条报错。
55
- export const FLAG_KEYS = ["permissionMode", "dangerouslySkipPermissions", "maxTurns", "budget", "timeout", "copresence", "opencodeGeneration", "opencodeUnsafeTools"] as const;
56
+ export const FLAG_KEYS = [
57
+ "permissionMode", "dangerouslySkipPermissions", "maxTurns", "budget", "timeout",
58
+ "copresence", "opencodeGeneration", "opencodeUnsafeTools",
59
+ "approvalPolicy", "sandboxMode", "skipGitRepoCheck", "copresenceFullAccess", "modelReasoningEffort",
60
+ ] as const;
56
61
 
57
62
  /** #584 —— 只有这些 runtime 认 `flags.copresence`。目前只有 codex-app-server 的共存靠 config 字段
58
63
  * (`codexCopresence`)决定;grok-build-cli / opencode-cli 各有自己的字段,接进来之前一律拒,不静默吞。 */
@@ -170,6 +175,11 @@ export function validateFlagValue(k: string, v: unknown): void {
170
175
  if (typeof v !== "boolean") throw new ValidationError("flag_value_invalid", { field: k, reason: "must be boolean" });
171
176
  return;
172
177
  default:
178
+ if (CODEX_CONFIG_ONLY_FLAG_KEYS.has(k)) {
179
+ const fail = validateCodexConfigFlag(k, v, null);
180
+ if (fail) throw new ValidationError("flag_value_invalid", { field: k, reason: fail.reason });
181
+ return;
182
+ }
173
183
  throw new ValidationError("flag_key_unknown", { field: k });
174
184
  }
175
185
  }
@@ -196,6 +206,38 @@ export function validateFlagsForRuntime(runtime: string, flags: Record<string, u
196
206
  field: "copresence", runtime, applicable: [...COPRESENCE_FLAG_RUNTIMES],
197
207
  });
198
208
  }
209
+ for (const [k, v] of Object.entries(flags)) {
210
+ if (!CODEX_CONFIG_ONLY_FLAG_KEYS.has(k)) continue;
211
+ const fail = validateCodexConfigFlag(k, v, runtime);
212
+ if (fail) throw new ValidationError("flag_value_invalid", { field: k, reason: fail.reason });
213
+ }
214
+ }
215
+
216
+ /**
217
+ * V2 co-presence start requires OpenCode's own `provider/model`.
218
+ * Call only after the unsafe-opt-in check: a missing opt-in stays
219
+ * `opencode_v2_requires_unsafe_opt_in`. This API does not take an anet
220
+ * provider preset or a vault key (#2565 owns presets).
221
+ */
222
+ export function validateOpenCodeV2ProviderModel(
223
+ runtime: string,
224
+ model: unknown,
225
+ flags: Record<string, unknown> | undefined | null,
226
+ ): void {
227
+ if (runtime !== "opencode-cli" || !flags || typeof flags !== "object") return;
228
+ if (flags.opencodeGeneration !== "v2" || flags.opencodeUnsafeTools !== true) return;
229
+ const native = typeof model === "string"
230
+ && model.length > 0
231
+ && model.length <= 100
232
+ && MODEL_RE.test(model)
233
+ && !MODEL_DOT_ONLY_SEGMENT.test(model)
234
+ && model.includes("/")
235
+ && model.indexOf("/") === model.lastIndexOf("/");
236
+ if (!native) {
237
+ throw new ValidationError("opencode_v2_requires_provider_model", {
238
+ reason: "OpenCode V2 uses the installed OpenCode provider/model (exactly one slash, for example provider/model). This API does not take an anet provider preset.",
239
+ });
240
+ }
199
241
  }
200
242
 
201
243
  // §4.4.7 — env_refs 7-step gate. networkSecretsGet is injected so
@@ -356,9 +398,11 @@ export function buildAnetArgs(spec: NodeSpec): string[] {
356
398
  // `anet node create --copresence` 是布尔开关,不吃值。
357
399
  if (k === "copresence") { if (v === true) args.push("--copresence"); continue; }
358
400
  if (k === "opencodeUnsafeTools") { if (v === true) args.push("--opencode-unsafe-tools"); continue; }
401
+ if (CODEX_CONFIG_ONLY_FLAG_KEYS.has(k)) continue;
359
402
  args.push(`--${kebab(k)}`, String(v));
360
403
  }
361
404
  validateFlagsForRuntime(spec.runtime, spec.flags);
362
405
  }
406
+ validateOpenCodeV2ProviderModel(spec.runtime, spec.model, spec.flags);
363
407
  return args;
364
408
  }
@@ -119,6 +119,19 @@ describe("create_node + workdir", () => {
119
119
  expect(pulled.node_spec.flags).toEqual(flags);
120
120
  });
121
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
+
122
135
  test("#829 missing opt-in rejected before request or child token mint", async () => {
123
136
  seed({ runtimes_supported: ["opencode-cli"] });
124
137
  const tokenCount = () => db.get<{ n: number }>("SELECT COUNT(*) AS n FROM api_tokens WHERE network_id = ?1", NET)!.n;
@@ -0,0 +1,256 @@
1
+ import "./require-explicit-test-db";
2
+ import { afterAll, beforeEach, expect, test } from "bun:test";
3
+ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { createHash } from 'node:crypto';
7
+ import { db } from "./db";
8
+ import { registerTools } from "./tools";
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+ import { reconcileProviderSnapshots, readLocalProviderSnapshot } from "../../agent-node/src/runtime/daemon-provider-snapshot";
11
+ import { DaemonProviderStore } from "../../agent-network/src/daemon-provider-store";
12
+ import { register, createNetworkTokenForNode } from "./auth";
13
+
14
+ const net = "net_dps906", user = "u_dps906", node = "node_dps906", token = "token_dps906", alias = "daemon906";
15
+ const root = mkdtempSync(join(tmpdir(), "dps906-"));
16
+ function cleanup() {
17
+ db.run('DELETE FROM daemon_provider_applications WHERE request_id IN (SELECT request_id FROM daemon_provider_snapshots WHERE network_id=?1)', [net]);
18
+ db.run('DELETE FROM daemon_provider_writes WHERE request_id IN (SELECT request_id FROM daemon_provider_snapshots WHERE network_id=?1)', [net]);
19
+ db.run('DELETE FROM node_daemon_bindings WHERE network_id=?1', [net]);
20
+ for (const t of ["daemon_provider_snapshots", "nodes", "api_tokens", "network_members"]) db.run(`DELETE FROM ${t} WHERE network_id=?1`, [net]);
21
+ db.run("DELETE FROM networks WHERE network_id=?1", [net]);
22
+ db.run("DELETE FROM users WHERE user_id=?1", [user]);
23
+ }
24
+ beforeEach(() => {
25
+ cleanup();
26
+ db.run("INSERT INTO users (user_id,username,password_hash,role) VALUES (?1,?1,'x','user')", [user]);
27
+ db.run("INSERT INTO networks (network_id,network_name,owner_id) VALUES (?1,?1,?2)", [net, user]);
28
+ db.run("INSERT INTO network_members (user_id,network_id,role) VALUES (?1,?2,'owner')", [user, net]);
29
+ db.run("INSERT INTO nodes (node_id,node_name,alias,network_id,config_snapshot) VALUES (?1,?2,?2,?3,?4)", [node, alias, net, JSON.stringify({ role: "host_supervisor" })]);
30
+ db.run("INSERT INTO api_tokens (token_id,user_id,network_id,scope,name,token_hash) VALUES (?1,?2,?3,'network',?4,?5)", [token, user, net, `node:${alias}`, `hash_${token}`]);
31
+ });
32
+ afterAll(() => { cleanup(); rmSync(root, { recursive: true, force: true }); });
33
+ function client(kind = "human", network = net) {
34
+ const handlers: Record<string, any> = {};
35
+ // Capture the actual registerTools handlers, including production guards.
36
+ const server = new McpServer({ name: "test906", version: "0" }) as any;
37
+ const tool = server.tool.bind(server), register = server.registerTool.bind(server);
38
+ server.tool = (name: string, description: any, schema: any, handler: any) => { handlers[name] = handler; return tool(name, description, schema, handler); };
39
+ server.registerTool = (name: string, config: any, handler: any) => { handlers[name] = handler; return register(name, config, handler); };
40
+ registerTools(server, undefined, kind === "daemon" ? network : null, kind === "anonymous" ? null : user,
41
+ alias, kind === "daemon", kind === "daemon" ? token : null);
42
+ return async (name: string, args: any = {}) => JSON.parse((await handlers[name](args)).content[0].text);
43
+ }
44
+ const snapshot = () => ({ network_id: net, daemon_node_id: node, revision: 0, source: "daemon", providers: [] });
45
+ const request = () => client()("daemon_provider_snapshot", { action: "refresh", network_id: net, id: node });
46
+
47
+ test('application transport: scoped intent, capability gate, exact public receipt and changed binding refusal', async () => {
48
+ const human = client(), daemon = client('daemon');
49
+ const child = 'node_apply906';
50
+ db.run("INSERT INTO nodes (node_id,node_name,alias,network_id,runtime) VALUES (?1,'child906','child906',?2,'codex-app-server')", [child, net]);
51
+ db.run("INSERT INTO node_daemon_bindings (request_id,network_id,node_id,daemon_node_id,workdir,requested_by,status,created_at,updated_at) VALUES ('binding_apply906',?1,?2,?3,?4,?5,'active',1,1)", [net, child, node, root, user]);
52
+ const selection = { node_id: child, node_revision: 'a'.repeat(64), provider_id: 'custom', auth_id: 'work', model: 'model-a', confirm_restart: true };
53
+ const args = { action: 'apply', id: node, network_id: net, revision: 1, provider: selection };
54
+ expect((await human('daemon_provider_snapshot', { ...args, provider: { ...selection, key: 'TEST-ONLY-key' } })).error).toBe('invalid_application');
55
+ expect((await human('daemon_provider_snapshot', { ...args, provider: { ...selection, node_id: node } })).error).toBe('application_target_not_bound');
56
+ expect((await daemon('daemon_provider_snapshot', args)).ok).toBe(false);
57
+ const r = await human('daemon_provider_snapshot', args);
58
+ expect(r.ok).toBe(true); expect(r.request_id).toMatch(/^dpa_/);
59
+ expect((await daemon('pull_daemon_provider_snapshots', { accept_writes: true })).requests).toEqual([]);
60
+ const dispatch = await daemon('pull_daemon_provider_snapshots', { accept_applications: true });
61
+ expect(dispatch.requests[0]).toMatchObject({ request_id: r.request_id, operation: 'apply', alias: 'child906', expected_revision: 1, selection });
62
+ const receipt = { status: 'applied', request_id: r.request_id, node_id: child, provider_id: 'custom', auth_id: 'work', model: 'model-a',
63
+ runtime_provider: 'anet_' + createHash('sha256').update(JSON.stringify(['custom', 'work'])).digest('hex').slice(0, 20), node_revision: 'c'.repeat(64), provider_revision: 1, verification: 'runtime_confirmed', applied_at: Date.now() };
64
+ // This is a protocol fixture, NOT evidence a runtime has switched.
65
+ for (const bad of [{ ...receipt, model: 'wrong' }, { ...receipt, key: 'TEST-ONLY-key' }, { ...receipt, provider_revision: 2 }, { ...receipt, runtime_provider: 'anet_' + 'b'.repeat(20) }]) {
66
+ expect((await daemon('ack_daemon_provider_snapshot', { request_id: r.request_id, application: bad })).error).toBe('invalid_application_receipt');
67
+ }
68
+ expect((await daemon('ack_daemon_provider_snapshot', { request_id: r.request_id, snapshot: snapshot() })).ok).toBe(false);
69
+ expect((await daemon('ack_daemon_provider_snapshot', { request_id: r.request_id, application: receipt })).status).toBe('succeeded');
70
+ await daemon('ack_daemon_provider_snapshot', { request_id: r.request_id, failed: true });
71
+ const read = await human('daemon_provider_snapshot', { action: 'read', id: r.request_id, network_id: net });
72
+ expect(read.status).toBe('succeeded'); expect(read.application).toEqual(receipt); expect(read.snapshot).toBeNull();
73
+ expect(db.get('SELECT * FROM daemon_provider_applications WHERE request_id=?1', r.request_id)).toBeNull();
74
+ const stale = await human('daemon_provider_snapshot', args);
75
+ db.run("UPDATE node_daemon_bindings SET daemon_node_id='other' WHERE request_id='binding_apply906'");
76
+ expect((await daemon('pull_daemon_provider_snapshots', { accept_applications: true })).requests).toEqual([]);
77
+ expect((await human('daemon_provider_snapshot', { action: 'read', id: stale.request_id, network_id: net })).error).toBe('application_target_not_bound');
78
+ });
79
+
80
+ test('write core chain: encrypted Hub request → target daemon CLI → durable replay → public readback', async () => {
81
+ process.env.ANET_HUB_SECRET_VAULT_KEY = '42'.repeat(32); // isolated fixture only
82
+ const work = mkdtempSync(join(root, 'write-'));
83
+ const dir = join(work, '.anet/nodes', alias); mkdirSync(dir, { recursive: true, mode: 0o700 });
84
+ writeFileSync(join(dir, 'config.json'), JSON.stringify({ role: 'host_supervisor', network_id: net, node_id: node }), { mode: 0o600 });
85
+ const key = 'TEST-ONLY-private-write-key';
86
+ const provider = { id: 'deepseek', label: 'DeepSeek', enabled: true, runtimes: [{ runtime: 'codex-tui', auth: [
87
+ { id: 'work', kind: 'api_key', models: ['model-a'], baseUrl: 'https://api.example.test/v1', key },
88
+ ] }] };
89
+ const human = client(), daemon = client('daemon');
90
+ const r = await human('daemon_provider_snapshot', { action: 'put', id: node, network_id: net, revision: 0, provider });
91
+ expect(r.ok).toBe(true); expect(JSON.stringify(r)).not.toContain(key);
92
+ const encrypted = db.get<any>('SELECT * FROM daemon_provider_writes WHERE request_id=?1', r.request_id);
93
+ expect(Buffer.from(encrypted.ciphertext).toString()).not.toContain(key);
94
+ expect((await daemon('pull_daemon_provider_snapshots')).requests).toEqual([]); // old/insecure readers receive no key
95
+ expect((await client('daemon', 'other')('pull_daemon_provider_snapshots', { accept_writes: true })).ok).toBe(false);
96
+ let drop = true;
97
+ const deps = { workDir: work, alias, networkId: net, daemonId: node, serverUrl: 'https://hub.example.test', warn: () => {},
98
+ callCommHub: async (name: string, args: any) => {
99
+ if (name === 'ack_daemon_provider_snapshot' && drop) { drop = false; throw Error('lost acknowledgement'); }
100
+ return daemon(name, args);
101
+ } };
102
+ await reconcileProviderSnapshots(deps);
103
+ expect(new DaemonProviderStore(dir).list().revision).toBe(1);
104
+ await reconcileProviderSnapshots(deps); // same request must not write revision 2 or lose success
105
+ const read = await human('daemon_provider_snapshot', { action: 'read', id: r.request_id, network_id: net });
106
+ expect(read.status).toBe('succeeded'); expect(read.snapshot.revision).toBe(1);
107
+ expect(read.snapshot.providers[0].runtimes[0].auth[0].models).toEqual(['model-a']);
108
+ expect(JSON.stringify(read)).not.toContain(key);
109
+ expect(db.get('SELECT * FROM daemon_provider_writes WHERE request_id=?1', r.request_id)).toBeNull();
110
+ const conflict = await human('daemon_provider_snapshot', { action: 'put', id: node, network_id: net, revision: 0, provider: { ...provider, label: 'must not overwrite' } });
111
+ await reconcileProviderSnapshots(deps);
112
+ expect((await human('daemon_provider_snapshot', { action: 'read', id: conflict.request_id, network_id: net })).status).toBe('failed');
113
+ expect(new DaemonProviderStore(dir).list().providers[0].label).toBe('DeepSeek');
114
+ db.run("UPDATE network_members SET role='member' WHERE user_id=?1 AND network_id=?2", [user, net]);
115
+ expect((await human('daemon_provider_snapshot', { action: 'put', id: node, network_id: net, revision: 1, provider })).error).toBe('provider_admin_required');
116
+ });
117
+
118
+ test("human authorization precedes dispatch; unknown, nodes and non-admins cannot request", async () => {
119
+ expect((await client("anonymous")("daemon_provider_snapshot", { action: "refresh", network_id: net, id: node })).error).toBe("user_token_required");
120
+ expect((await client("daemon")("daemon_provider_snapshot", { action: "refresh", network_id: net, id: node })).ok).toBe(false);
121
+ db.run("UPDATE network_members SET role='member' WHERE user_id=?1 AND network_id=?2", [user, net]);
122
+ expect((await request()).error).toBe("provider_admin_required");
123
+ expect(db.get<{ n: number }>("SELECT COUNT(*) AS n FROM daemon_provider_snapshots WHERE network_id=?1", net)?.n).toBe(0);
124
+ });
125
+ test("normal nodes cannot impersonate a daemon; revoked token cannot pull or ack", async () => {
126
+ const r = await request(); expect(r.ok).toBe(true);
127
+ db.run("UPDATE nodes SET config_snapshot='{}' WHERE node_id=?1", [node]);
128
+ expect((await client("daemon")("pull_daemon_provider_snapshots")).error).toBe("caller_not_a_daemon");
129
+ db.run("UPDATE nodes SET config_snapshot=?1 WHERE node_id=?2", [JSON.stringify({ role: "host_supervisor" }), node]);
130
+ db.run("UPDATE api_tokens SET revoked_at=datetime('now') WHERE token_id=?1", [token]);
131
+ expect((await client("daemon")("pull_daemon_provider_snapshots")).ok).toBe(false);
132
+ expect((await client("daemon")("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: snapshot() })).ok).toBe(false);
133
+ });
134
+ test("request/pull/ack/read lifecycle; duplicate click and ack cannot rewrite receipt", async () => {
135
+ const r = await request(); expect((await request()).request_id).toBe(r.request_id);
136
+ const human = client(), daemon = client("daemon");
137
+ expect((await human("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net })).snapshot).toBeNull();
138
+ expect((await daemon("pull_daemon_provider_snapshots")).requests[0].request_id).toBe(r.request_id);
139
+ expect((await daemon("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: snapshot() })).status).toBe("succeeded");
140
+ await daemon("ack_daemon_provider_snapshot", { request_id: r.request_id, failed: true });
141
+ const read = await human("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net });
142
+ expect(read.status).toBe("succeeded"); expect(read.snapshot).toEqual(snapshot()); expect(read.observed_at).toBeGreaterThan(0);
143
+ expect((await daemon("pull_daemon_provider_snapshots")).requests).toEqual([]);
144
+ });
145
+ test("network and daemon mismatch cannot read or acknowledge another scope", async () => {
146
+ const r = await request();
147
+ expect((await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: "foreign" })).ok).toBe(false);
148
+ expect((await client("daemon", "foreign")("pull_daemon_provider_snapshots")).ok).toBe(false);
149
+ const d = client("daemon");
150
+ expect((await d("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: { ...snapshot(), daemon_node_id: "other" } })).error).toBe("invalid_provider_snapshot");
151
+ db.run("UPDATE daemon_provider_snapshots SET daemon_node_id='other' WHERE request_id=?1", [r.request_id]);
152
+ expect((await d("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: snapshot() })).error).toBe("request_not_found");
153
+ });
154
+ test("timeout is explicit; late daemon reply cannot overwrite it", async () => {
155
+ const r = await request();
156
+ db.run("UPDATE daemon_provider_snapshots SET expires_at=?1 WHERE request_id=?2", [Date.now() - 1, r.request_id]);
157
+ expect((await client("daemon")("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: snapshot() })).status).toBe("expired");
158
+ expect((await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net })).snapshot).toBeNull();
159
+ });
160
+ test("secret fields / misleading verification are rejected, never saved in Hub DB", async () => {
161
+ const r = await request(); const secret = "TEST-ONLY-key-never-in-hub";
162
+ const auth = { id: "work", kind: "api_key", models: ["model-a"], baseUrl: "https://api.example.test/v1", credential_present: true, verification: "not_checked", application: "not_applied" };
163
+ const nested = (change: object) => ({ ...snapshot(), providers: [{ id: "deepseek", label: "DeepSeek", enabled: true, runtimes: [{ runtime: "codex-tui", auth: [{ ...auth, ...change }] }] }] });
164
+ for (const bad of [{ ...snapshot(), key: secret }, { ...snapshot(), source: "hub" }, { ...snapshot(), revision: -1 },
165
+ nested({ key: secret }), nested({ verification: "verified" }), nested({ application: "applied" }), nested({ baseUrl: `https://user:${secret}@api.example.test/v1` })]) {
166
+ const reply = await client("daemon")("ack_daemon_provider_snapshot", { request_id: r.request_id, snapshot: bad });
167
+ expect(reply.error).toBe("invalid_provider_snapshot"); expect(JSON.stringify(reply)).not.toContain(secret);
168
+ }
169
+ expect(db.get<{ snapshot_json: string | null }>("SELECT snapshot_json FROM daemon_provider_snapshots WHERE request_id=?1", r.request_id)?.snapshot_json).toBeNull();
170
+ });
171
+ test("mirrored contract cannot drift", () => {
172
+ expect(readFileSync(new URL("./shared/daemon-provider-snapshot.ts", import.meta.url), "utf8"))
173
+ .toBe(readFileSync(new URL("../../agent-node/src/shared/daemon-provider-snapshot.ts", import.meta.url), "utf8"));
174
+ });
175
+ test("daemon reconnection executes actual pinned CLI against private store and acks redacted data", async () => {
176
+ const dir = join(root, ".anet/nodes", alias); mkdirSync(dir, { recursive: true, mode: 0o700 });
177
+ writeFileSync(join(dir, "config.json"), JSON.stringify({ role: "host_supervisor", network_id: net, node_id: node }), { mode: 0o600 });
178
+ const key = "TEST-ONLY-private-906";
179
+ new DaemonProviderStore(dir).upsert({ id: "deepseek", label: "DeepSeek", enabled: true, runtimes: [{ runtime: "codex-tui", auth: [{ id: "work", kind: "api_key", models: ["model-a"], baseUrl: "https://api.example.test/v1", key }] }] }, 0);
180
+ const deps = { workDir: root, alias, networkId: net, daemonId: node, callCommHub: client("daemon"), warn: () => { throw Error("unexpected_warning"); } };
181
+ // Atomic local read must pass before the Hub/daemon round trip.
182
+ expect((await readLocalProviderSnapshot(deps) as any).revision).toBe(1);
183
+ const r = await request();
184
+ await Promise.all([reconcileProviderSnapshots(deps), reconcileProviderSnapshots(deps)]);
185
+ const read = await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net });
186
+ expect(read.status).toBe("succeeded"); expect(read.snapshot.providers[0].runtimes[0].auth[0].credential_present).toBe(true);
187
+ expect(JSON.stringify(read)).not.toContain(key); expect(read.snapshot.providers[0].runtimes[0].auth[0].verification).toBe("not_checked");
188
+ });
189
+ test("daemon raw errors are replaced with fixed failure receipt", async () => {
190
+ const r = await request();
191
+ await reconcileProviderSnapshots({ workDir: root, alias, networkId: net, daemonId: node, callCommHub: client("daemon"), warn: () => {},
192
+ readSnapshot: async () => { throw Error("TEST-ONLY-sensitive-error"); } });
193
+ const read = await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net });
194
+ expect(read.status).toBe("failed"); expect(read.error).toBe("provider_snapshot_unavailable"); expect(JSON.stringify(read)).not.toContain("TEST-ONLY-sensitive-error");
195
+ });
196
+ test("lost acknowledgement retries on reconnect, without swallowing a new doorbell", async () => {
197
+ const r = await request(); const real = client("daemon"); let loseAck = true, reads = 0;
198
+ const deps = { workDir: root, alias, networkId: net, daemonId: node, warn: () => {},
199
+ readSnapshot: async () => { reads++; return snapshot(); },
200
+ callCommHub: async (name: string, args: any) => {
201
+ if (name === "ack_daemon_provider_snapshot" && loseAck) { loseAck = false; throw Error("TEST-ONLY-transport-down"); }
202
+ return real(name, args);
203
+ } };
204
+ await reconcileProviderSnapshots(deps);
205
+ expect((await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net })).status).toBe("pending");
206
+ await reconcileProviderSnapshots(deps);
207
+ expect((await client()("daemon_provider_snapshot", { action: "read", id: r.request_id, network_id: net })).status).toBe("succeeded");
208
+ expect(reads).toBe(2);
209
+ });
210
+ test("real HTTP authentication -> daemon handler -> pinned CLI -> scoped public receipt", async () => {
211
+ process.env.COMMHUB_UPLOADS_DIR = join(root, "uploads");
212
+ const owner = register("provider906_http", "TestOnly-Provider906-Password!1");
213
+ expect(owner.ok).toBe(true);
214
+ const netId = owner.network_id!, nodeId = "node_dps_http", daemonAlias = "daemon-http";
215
+ const issued = createNetworkTokenForNode(owner.user!.user_id, netId, daemonAlias, nodeId);
216
+ expect(issued.ok).toBe(true);
217
+ db.run("UPDATE nodes SET config_snapshot=?1 WHERE node_id=?2", [JSON.stringify({ role: "host_supervisor" }), nodeId]);
218
+ const dir = join(root, ".anet/nodes", daemonAlias); mkdirSync(dir, { recursive: true, mode: 0o700 });
219
+ writeFileSync(join(dir, "config.json"), JSON.stringify({ role: "host_supervisor", network_id: netId, node_id: nodeId }), { mode: 0o600 });
220
+ for (const child of ['bound', 'neighbour']) {
221
+ const childId = `n_scan_${child}`;
222
+ db.run('INSERT INTO nodes (node_id,node_name,alias,network_id,runtime) VALUES (?1,?2,?2,?3,\'codex-app-server\')', [childId, child, netId]);
223
+ db.run('INSERT INTO node_daemon_bindings (request_id,network_id,node_id,daemon_node_id,workdir,requested_by,status,created_at,updated_at) VALUES (?1,?2,?3,?4,?5,?6,\'active\',1,1)',
224
+ [`binding_scan_${child}`, netId, childId, child === 'bound' ? nodeId : 'another-daemon', root, owner.user!.user_id]);
225
+ const home = join(root, '.anet/nodes', child, 'codex-home'); mkdirSync(home, { recursive: true, mode: 0o700 });
226
+ writeFileSync(join(home, '../config.json'), JSON.stringify({ node_id: childId, alias: child, runtime: 'codex-app-server', network_id: netId }), { mode: 0o600 });
227
+ writeFileSync(join(home, 'config.toml'), 'model_provider = "deepseek"\nmodel = "fixture-model"\n[model_providers.deepseek]\nenv_key = "TEST_API_KEY"\n', { mode: 0o600 });
228
+ }
229
+ const hub = (await import("./server")).bootServer({ port: 0, hostname: "127.0.0.1" });
230
+ const url = `http://127.0.0.1:${hub.port}/mcp`;
231
+ const rpc = (credential: string) => async (name: string, args: any) => {
232
+ const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${credential}`, "Content-Type": "application/json", Accept: "application/json, text/event-stream", "MCP-Protocol-Version": "2025-03-26" },
233
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }) });
234
+ expect(response.status).toBe(200);
235
+ const raw = await response.text(); const line = raw.split("\n").filter(x => x.startsWith("data:")).at(-1);
236
+ const message = JSON.parse(line ? line.slice(5) : raw);
237
+ expect(message.error).toBeUndefined();
238
+ return JSON.parse(message.result.content[0].text);
239
+ };
240
+ try {
241
+ const human = rpc(owner.token!), daemon = rpc(issued.token!);
242
+ const refresh = await human("daemon_provider_snapshot", { action: "refresh", id: nodeId, network_id: netId });
243
+ expect(refresh.status).toBe("pending");
244
+ expect((await human("pull_daemon_provider_snapshots", {})).ok).toBe(false);
245
+ expect((await daemon("daemon_provider_snapshot", { action: "refresh", id: nodeId, network_id: netId })).ok).toBe(false);
246
+ await reconcileProviderSnapshots({ workDir: root, alias: daemonAlias, networkId: netId, daemonId: nodeId, callCommHub: daemon,
247
+ warn: () => { throw Error("unexpected_HTTP_readback_failure"); } });
248
+ const read = await human("daemon_provider_snapshot", { action: "read", id: refresh.request_id, network_id: netId });
249
+ expect(read.status).toBe("succeeded"); expect(read.snapshot).toMatchObject({ network_id: netId, daemon_node_id: nodeId, revision: 0, source: "daemon", providers: [], codex_inventory: { scope: 'hub_bound_nodes' } });
250
+ expect(read.snapshot.codex_inventory.rows).toHaveLength(1);
251
+ expect(read.snapshot.codex_inventory.rows[0]).toMatchObject({ node_id: 'n_scan_bound', configured_provider: 'deepseek', configured_model: 'fixture-model', credential_status: 'unknown' });
252
+ const foreign = await human('daemon_provider_snapshot', { action: 'refresh', id: nodeId, network_id: netId });
253
+ const bad = structuredClone(read.snapshot); bad.codex_inventory.rows[0].node_id = 'n_scan_neighbour';
254
+ expect((await daemon('ack_daemon_provider_snapshot', { request_id: foreign.request_id, snapshot: bad })).ok).toBe(false);
255
+ } finally { hub.stop(true); }
256
+ });
@@ -0,0 +1,226 @@
1
+ // #906: human-requested readback of ONE daemon's private Provider metadata.
2
+ // Hub registry is not a fallback. No credential values enter this protocol.
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { db } from "./db";
6
+ import { getUserNetworkRole } from "./auth";
7
+ import { validateDaemonProviderSnapshot } from "./shared/daemon-provider-snapshot";
8
+ import { encryptSecret, decryptSecret, VaultError } from './vault';
9
+ import { buildControllableMap } from './node-lifecycle-controllable';
10
+ import { validProviderApplicationSelection, validProviderApplicationReceipt, type ProviderApplicationSelection } from './shared/provider-application';
11
+
12
+ export function providerScanTargets(networkId: string, daemonId: string) {
13
+ const ownership = buildControllableMap(
14
+ db.all<{ request_id: string; daemon_node_id: string }>('SELECT request_id,daemon_node_id FROM node_create_requests WHERE network_id=?1', networkId),
15
+ db.all<{ node_id: string; daemon_node_id: string }>('SELECT node_id,daemon_node_id FROM node_daemon_bindings WHERE network_id=?1 AND status=\'active\'', networkId));
16
+ const targets = db.all<{ node_id: string; alias: string }>('SELECT node_id,alias FROM nodes WHERE network_id=?1 ORDER BY node_id', networkId)
17
+ .filter(n => ownership.get(n.node_id) === daemonId && typeof n.alias === 'string');
18
+ if (targets.length > 200) throw Error('inventory_limit'); // no silent incomplete denominator
19
+ return targets;
20
+ }
21
+
22
+ type Human = { userId?: string | null; isNode: boolean; networkId: string | null };
23
+ type Daemon = { ok: false; error: string } | { ok: true; networkId: string; daemonNodeId: string; daemonAlias: string };
24
+ type Row = { request_id: string; network_id: string; daemon_node_id: string; status: string;
25
+ created_at: number; expires_at: number; observed_at: number | null; snapshot_json: string | null; error: string | null };
26
+ const denied = (error: string) => ({ ok: false as const, error });
27
+ function authorize(h: Human) {
28
+ if (h.isNode || !h.userId) return denied("user_token_required");
29
+ if (!h.networkId || !["owner", "admin"].includes(getUserNetworkRole(h.userId, h.networkId) || "")) return denied("provider_admin_required");
30
+ return null;
31
+ }
32
+ function daemonExists(networkId: string, nodeId: string): boolean {
33
+ const node = db.get<{ config_snapshot: string }>(
34
+ `SELECT n.config_snapshot FROM nodes n WHERE n.node_id=?1 AND n.network_id=?2
35
+ AND EXISTS (SELECT 1 FROM api_tokens t WHERE t.network_id=n.network_id
36
+ AND t.name='node:' || n.alias AND t.revoked_at IS NULL
37
+ AND (t.expires_at IS NULL OR t.expires_at > datetime('now')))`, nodeId, networkId);
38
+ try { return JSON.parse(node?.config_snapshot || "{}").role === "host_supervisor"; } catch { return false; }
39
+ }
40
+ function expire(networkId: string, daemonId?: string) {
41
+ db.run(`UPDATE daemon_provider_snapshots SET status='expired', error='request_expired'
42
+ WHERE network_id=?1 AND status='pending' AND expires_at <= ?2
43
+ AND (?3 IS NULL OR daemon_node_id=?3)`, [networkId, Date.now(), daemonId ?? null]);
44
+ db.run(`DELETE FROM daemon_provider_writes WHERE request_id IN
45
+ (SELECT request_id FROM daemon_provider_snapshots WHERE network_id=?1 AND status!='pending')`, [networkId]);
46
+ db.run(`DELETE FROM daemon_provider_applications WHERE request_id IN
47
+ (SELECT request_id FROM daemon_provider_snapshots WHERE network_id=?1 AND status!='pending')`, [networkId]);
48
+ db.run(`DELETE FROM daemon_provider_snapshots WHERE network_id=?1 AND expires_at < ?2`, [networkId, Date.now() - 7 * 86400_000]);
49
+ }
50
+ /** Validate the complete public projection before encrypting the private input. */
51
+ function validateWrite(provider: any, networkId: string, daemonId: string) {
52
+ if (Buffer.byteLength(JSON.stringify(provider) || '') > 64 * 1024) throw new Error('invalid_provider');
53
+ const publicProvider = { ...provider, runtimes: provider?.runtimes?.map((r: any) => ({ ...r,
54
+ auth: r.auth?.map((a: any) => {
55
+ const { key, ...rest } = a;
56
+ if (a.kind === 'chatgpt' && key !== undefined) throw new Error('invalid_provider');
57
+ if (key !== undefined && (typeof key !== 'string' || !key || key.length > 8192 || /[\s\x00-\x1f\x7f]/.test(key))) throw new Error('invalid_provider');
58
+ // Only CLI write fields may be supplied; public-state flags are not writable.
59
+ if (Object.keys(a).some(k => !['id','kind','models','baseUrl','key'].includes(k))) throw new Error('invalid_provider');
60
+ return { ...rest, credential_present: a.kind === 'api_key', verification: 'not_checked', application: 'not_applied' };
61
+ }),
62
+ })) };
63
+ validateDaemonProviderSnapshot({ network_id: networkId, daemon_node_id: daemonId, source: 'daemon', revision: 0, providers: [publicProvider] }, networkId, daemonId);
64
+ return publicProvider;
65
+ }
66
+ export function requestProviderWrite(h: Human, daemonId: string, expectedRevision: number, provider: unknown) {
67
+ const refusal = authorize(h); if (refusal) return refusal;
68
+ if (!daemonExists(h.networkId!, daemonId)) return denied('daemon_not_found');
69
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0 || expectedRevision >= Number.MAX_SAFE_INTEGER) return denied('invalid_revision');
70
+ try { validateWrite(provider, h.networkId!, daemonId); } catch { return denied('invalid_provider'); }
71
+ expire(h.networkId!, daemonId);
72
+ if (db.get('SELECT request_id FROM daemon_provider_snapshots WHERE network_id=?1 AND daemon_node_id=?2 AND status=\'pending\'', h.networkId!, daemonId)) return denied('provider_snapshot_busy');
73
+ try {
74
+ const enc = encryptSecret(JSON.stringify({ network_id: h.networkId, daemon_node_id: daemonId, provider }));
75
+ const requestId = `dps_${randomUUID()}`, now = Date.now();
76
+ db.transaction(() => {
77
+ db.run(`INSERT INTO daemon_provider_snapshots (request_id,network_id,daemon_node_id,requested_by,status,created_at,expires_at)
78
+ VALUES (?1,?2,?3,?4,'pending',?5,?6)`, [requestId, h.networkId!, daemonId, h.userId!, now, now + 120_000]);
79
+ db.run(`INSERT INTO daemon_provider_writes (request_id,expected_revision,ciphertext,iv,tag) VALUES (?1,?2,?3,?4,?5)`,
80
+ [requestId, expectedRevision, enc.ciphertext, enc.iv, enc.tag]);
81
+ });
82
+ return { ok: true as const, request_id: requestId, status: 'pending', expires_at: now + 120_000 };
83
+ } catch (error) { return denied(error instanceof VaultError ? error.code : 'provider_write_unavailable'); }
84
+ }
85
+ function privateWrite(requestId: string, networkId: string, daemonId: string) {
86
+ const row = db.get<{ expected_revision: number; ciphertext: any; iv: any; tag: any }>('SELECT * FROM daemon_provider_writes WHERE request_id=?1', requestId);
87
+ if (!row) return null;
88
+ const payload = JSON.parse(decryptSecret({ ciphertext: Buffer.from(row.ciphertext), iv: Buffer.from(row.iv), tag: Buffer.from(row.tag) }));
89
+ if (payload.network_id !== networkId || payload.daemon_node_id !== daemonId) throw new Error('scope_mismatch');
90
+ validateWrite(payload.provider, networkId, daemonId);
91
+ return { expected_revision: row.expected_revision, provider: payload.provider };
92
+ }
93
+ function applicationIntent(requestId: string) {
94
+ const row = db.get<{ expected_revision: number; selection_json: string }>('SELECT * FROM daemon_provider_applications WHERE request_id=?1', requestId);
95
+ if (!row) return null;
96
+ const selection = JSON.parse(row.selection_json);
97
+ if (!validProviderApplicationSelection(selection)) throw Error('invalid_application');
98
+ return { expected_revision: row.expected_revision, selection };
99
+ }
100
+ function boundApplicationTarget(networkId: string, daemonId: string, selection: ProviderApplicationSelection) {
101
+ return selection.node_id !== daemonId && providerScanTargets(networkId, daemonId).find(n => n.node_id === selection.node_id);
102
+ }
103
+ export function requestProviderApplication(h: Human, daemonId: string, revision: number, selection: unknown) {
104
+ const refusal = authorize(h); if (refusal) return refusal;
105
+ if (!daemonExists(h.networkId!, daemonId)) return denied('daemon_not_found');
106
+ if (!Number.isSafeInteger(revision) || revision < 0) return denied('invalid_revision');
107
+ if (!validProviderApplicationSelection(selection)) return denied('invalid_application');
108
+ try {
109
+ if (!boundApplicationTarget(h.networkId!, daemonId, selection)) return denied('application_target_not_bound');
110
+ expire(h.networkId!, daemonId);
111
+ const requestId = `dpa_${randomUUID()}`, now = Date.now();
112
+ db.transaction(() => {
113
+ db.run(`INSERT INTO daemon_provider_snapshots (request_id,network_id,daemon_node_id,requested_by,status,created_at,expires_at)
114
+ VALUES (?1,?2,?3,?4,'pending',?5,?6)`, [requestId, h.networkId!, daemonId, h.userId!, now, now + 600_000]);
115
+ db.run('INSERT INTO daemon_provider_applications (request_id,expected_revision,selection_json) VALUES (?1,?2,?3)',
116
+ [requestId, revision, JSON.stringify(selection)]);
117
+ });
118
+ return { ok: true as const, request_id: requestId, status: 'pending', expires_at: now + 600_000 };
119
+ } catch { return denied('provider_application_unavailable'); }
120
+ }
121
+ export function requestProviderSnapshot(h: Human, daemonId: string) {
122
+ const refusal = authorize(h); if (refusal) return refusal;
123
+ if (!daemonExists(h.networkId!, daemonId)) return denied("daemon_not_found");
124
+ expire(h.networkId!, daemonId);
125
+ // Repeated refresh clicks share the pending request; pending never means ready.
126
+ const pending = db.get<Row>(`SELECT * FROM daemon_provider_snapshots WHERE network_id=?1 AND daemon_node_id=?2 AND status='pending' ORDER BY created_at DESC LIMIT 1`, h.networkId!, daemonId);
127
+ if (pending) return { ok: true as const, request_id: pending.request_id, status: "pending", expires_at: pending.expires_at };
128
+ const requestId = `dps_${randomUUID()}`, now = Date.now();
129
+ db.run(`INSERT INTO daemon_provider_snapshots
130
+ (request_id,network_id,daemon_node_id,requested_by,status,created_at,expires_at)
131
+ VALUES (?1,?2,?3,?4,'pending',?5,?6) ON CONFLICT DO NOTHING`, [requestId, h.networkId!, daemonId, h.userId!, now, now + 120_000]);
132
+ const accepted = db.get<Row>(`SELECT * FROM daemon_provider_snapshots WHERE network_id=?1 AND daemon_node_id=?2 AND status='pending'`, h.networkId!, daemonId);
133
+ if (!accepted) return denied("provider_snapshot_busy");
134
+ return { ok: true as const, request_id: accepted.request_id, status: "pending", expires_at: accepted.expires_at };
135
+ }
136
+ export function getProviderSnapshot(h: Human, requestId: string) {
137
+ const refusal = authorize(h); if (refusal) return refusal;
138
+ expire(h.networkId!);
139
+ const row = db.get<Row>(`SELECT * FROM daemon_provider_snapshots WHERE request_id=?1 AND network_id=?2`, requestId, h.networkId!);
140
+ if (!row) return denied("request_not_found");
141
+ return { ok: true, request_id: row.request_id, daemon_node_id: row.daemon_node_id, status: row.status,
142
+ expires_at: row.expires_at, observed_at: row.observed_at, error: row.error,
143
+ snapshot: !row.request_id.startsWith('dpa_') && row.snapshot_json ? JSON.parse(row.snapshot_json) : null,
144
+ ...(row.request_id.startsWith('dpa_') ? { application: row.snapshot_json ? JSON.parse(row.snapshot_json) : null } : {}) };
145
+ }
146
+ export function pullProviderSnapshots(d: Daemon, acceptWrites = false, acceptApplications = false) {
147
+ if (!d.ok) return denied("caller_not_a_daemon");
148
+ if (!daemonExists(d.networkId, d.daemonNodeId)) return denied("caller_not_a_daemon");
149
+ expire(d.networkId, d.daemonNodeId);
150
+ const requests = db.all<{ request_id: string; expires_at: number }>(`SELECT request_id,expires_at FROM daemon_provider_snapshots
151
+ WHERE network_id=?1 AND daemon_node_id=?2 AND status='pending' ORDER BY created_at LIMIT 20`, d.networkId, d.daemonNodeId);
152
+ try { return { ok: true, requests: requests.flatMap<Record<string, unknown>>(r => {
153
+ if (r.request_id.startsWith('dpa_')) {
154
+ if (!acceptApplications) return []; // old daemons must not consume applications as scans
155
+ const application = applicationIntent(r.request_id);
156
+ const target = application && boundApplicationTarget(d.networkId, d.daemonNodeId, application.selection);
157
+ if (!target) {
158
+ db.run("UPDATE daemon_provider_snapshots SET status='failed',error='application_target_not_bound',observed_at=?1 WHERE request_id=?2 AND status='pending'", [Date.now(), r.request_id]);
159
+ db.run('DELETE FROM daemon_provider_applications WHERE request_id=?1', [r.request_id]);
160
+ return [];
161
+ }
162
+ return [{ ...r, operation: 'apply', ...application!, alias: target.alias }];
163
+ }
164
+ if (!acceptWrites && db.get('SELECT request_id FROM daemon_provider_writes WHERE request_id=?1', r.request_id)) return [];
165
+ const write = privateWrite(r.request_id, d.networkId, d.daemonNodeId);
166
+ return [write ? { ...r, operation: 'put', ...write } : { ...r, scan_targets: providerScanTargets(d.networkId, d.daemonNodeId) }];
167
+ }) }; } catch { return denied('provider_write_unavailable'); }
168
+ }
169
+ export function ackProviderSnapshot(d: Daemon, requestId: string, snapshot?: unknown, failed = false, appliedRevision?: number, application?: unknown) {
170
+ if (!d.ok || !daemonExists(d.networkId, d.daemonNodeId)) return denied("caller_not_a_daemon");
171
+ expire(d.networkId, d.daemonNodeId);
172
+ const row = db.get<Row>(`SELECT * FROM daemon_provider_snapshots WHERE request_id=?1 AND network_id=?2 AND daemon_node_id=?3`, requestId, d.networkId, d.daemonNodeId);
173
+ if (!row) return denied("request_not_found");
174
+ // First accepted receipt wins; reconnect replay cannot rewrite prior evidence.
175
+ if (row.status !== "pending") return { ok: row.status === "succeeded" || row.status === "failed", status: row.status };
176
+ if (requestId.startsWith('dpa_')) {
177
+ if (snapshot !== undefined || appliedRevision !== undefined || (failed && application !== undefined)) return denied('invalid_application_receipt');
178
+ if (!failed) {
179
+ try {
180
+ const intent = applicationIntent(requestId);
181
+ if (!intent || !boundApplicationTarget(d.networkId, d.daemonNodeId, intent.selection)
182
+ || !validProviderApplicationReceipt(application, requestId, intent.expected_revision, intent.selection)
183
+ || (application as any).runtime_provider !== `anet_${createHash('sha256').update(JSON.stringify([intent.selection.provider_id, intent.selection.auth_id])).digest('hex').slice(0, 20)}`
184
+ || (application as any).applied_at < row.created_at - 30_000) return denied('invalid_application_receipt');
185
+ } catch { return denied('invalid_application_receipt'); }
186
+ }
187
+ db.run(`UPDATE daemon_provider_snapshots SET status=?1,snapshot_json=?2,error=?3,observed_at=?4
188
+ WHERE request_id=?5 AND status='pending'`, [failed ? 'failed' : 'succeeded', failed ? null : JSON.stringify(application),
189
+ failed ? 'provider_application_unavailable' : null, Date.now(), requestId]);
190
+ db.run('DELETE FROM daemon_provider_applications WHERE request_id=?1', [requestId]);
191
+ return { ok: true, status: failed ? 'failed' : 'succeeded' };
192
+ }
193
+ if (application !== undefined) return denied('invalid_application_receipt');
194
+ let safe: unknown = null;
195
+ if (failed && snapshot !== undefined) return denied("invalid_provider_snapshot");
196
+ if (!failed) {
197
+ try { safe = validateDaemonProviderSnapshot(snapshot, d.networkId, d.daemonNodeId); }
198
+ catch { return denied("invalid_provider_snapshot"); }
199
+ if ((safe as any).codex_inventory) {
200
+ try {
201
+ const targets = new Map(providerScanTargets(d.networkId, d.daemonNodeId).map(n => [n.node_id, n.alias]));
202
+ const rows = (safe as any).codex_inventory.rows;
203
+ if (rows.length !== targets.size || rows.some((r: any) => targets.get(r.node_id) !== r.alias)) return denied('invalid_provider_snapshot');
204
+ } catch { return denied('invalid_provider_snapshot'); }
205
+ }
206
+ try {
207
+ const write = privateWrite(requestId, d.networkId, d.daemonNodeId);
208
+ if (write) {
209
+ const actual = safe as any;
210
+ if (appliedRevision !== write.expected_revision + 1 || actual.revision !== appliedRevision) return denied('provider_write_unconfirmed');
211
+ const expected = validateWrite(write.provider, d.networkId, d.daemonNodeId);
212
+ const saved = actual.providers.find((p: any) => p.id === expected.id);
213
+ // CLI canonicalizes label and endpoint. No credential is compared or returned here.
214
+ expected.label = expected.label.trim();
215
+ for (const r of expected.runtimes) for (const a of r.auth) if (a.baseUrl) a.baseUrl = new URL(a.baseUrl).toString().replace(/\/$/, '');
216
+ if (!isDeepStrictEqual(saved, expected)) return denied('provider_write_unconfirmed');
217
+ }
218
+ } catch { return denied('provider_write_unconfirmed'); }
219
+ }
220
+ db.run(`UPDATE daemon_provider_snapshots SET status=?1, snapshot_json=?2, error=?3, observed_at=?4
221
+ WHERE request_id=?5 AND status='pending'`, [failed ? "failed" : "succeeded", safe ? JSON.stringify(safe) : null,
222
+ failed ? "provider_snapshot_unavailable" : null, Date.now(), requestId]);
223
+ const accepted = db.get<Row>("SELECT * FROM daemon_provider_snapshots WHERE request_id=?1", requestId);
224
+ db.run('DELETE FROM daemon_provider_writes WHERE request_id=?1', [requestId]);
225
+ return { ok: true, status: accepted!.status };
226
+ }