@sleep2agi/commhub-server 0.9.0-preview.126 → 0.9.0-preview.128

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.126",
3
+ "version": "0.9.0-preview.128",
4
4
  "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and MCP tools (17 collaboration-core + node/provider ops tools; authoritative list: docs-site/docs/api/mcp-tools.md).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -7,7 +7,7 @@ import { db } from './db';
7
7
  import { registerTools } from './tools';
8
8
  import { vaultGet } from './vault';
9
9
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
- import { reconcileProviderSnapshots } from '../../agent-node/src/runtime/daemon-provider-snapshot';
10
+ import { reconcileProviderSnapshots, secureProviderTransport } from '../../agent-node/src/runtime/daemon-provider-snapshot';
11
11
  import { DaemonProviderStore } from '../../agent-network/src/daemon-provider-store';
12
12
  const net = 'net_hub907', user = 'u_hub907', daemonId = 'n_hub907', alias = 'daemon907', token = 'tok_hub907';
13
13
  const root = mkdtempSync(join(tmpdir(), 'hub907-')), key = 'TEST-ONLY-shared-provider-key';
@@ -39,6 +39,29 @@ function client(kind = 'human') {
39
39
  const provider = () => ({ id: 'shared', label: 'Shared model', enabled: true, protocol: 'openai-responses', baseUrl: 'https://provider.example/v1', models: ['fixture-model'], key });
40
40
  const save = (human: ReturnType<typeof client>, value = provider(), revision = 0) => human('daemon_provider_snapshot', { action: 'hub_put', id: value.id, network_id: net, revision, provider: value });
41
41
  const bind = (human: ReturnType<typeof client>, target = daemonId, revision = 1) => human('daemon_provider_snapshot', { action: 'hub_bind', network_id: net, id: 'shared', revision, provider: { daemon_node_id: target, daemon_revision: 0, confirm: true } });
42
+ test('HTTP Hub and API endpoint support save, explicit bind and exact private-store readback', async () => {
43
+ expect(secureProviderTransport('http://hub907.example.test')).toBe(true);
44
+ expect(secureProviderTransport('ftp://hub907.example.test')).toBe(false);
45
+ expect(secureProviderTransport('http://user:password@hub907.example.test')).toBe(false);
46
+ const human = client(), daemon = client('daemon');
47
+ const config = { ...provider(), baseUrl: 'http://provider.example/v1' };
48
+ const saved = await save(human, config); expect(saved.ok).toBe(true);
49
+ expect(saved.provider.baseUrl).toBe(config.baseUrl);
50
+ const queued = await bind(human); expect(queued.ok).toBe(true);
51
+ await reconcileProviderSnapshots({ workDir: root, alias, networkId: net, daemonId,
52
+ serverUrl: 'http://hub907.example.test', callCommHub: daemon, warn: message => console.warn(message) });
53
+ const receipt = await human('daemon_provider_snapshot', { action: 'read', id: queued.request_id, network_id: net });
54
+ expect(receipt.status).toBe('succeeded');
55
+ expect(receipt.snapshot.providers[0].runtimes[0].auth[0].baseUrl).toBe(config.baseUrl);
56
+ expect(JSON.stringify([saved, queued, receipt])).not.toContain(key);
57
+ const store = new DaemonProviderStore(join(root, '.anet/nodes', alias));
58
+ await store.withApiKey({ revision: 1, providerId: queued.provider_reference.daemon_provider_id, authId: 'hub_key', model: 'fixture-model' }, async actual => {
59
+ expect(actual.baseUrl).toBe(config.baseUrl); expect(actual.key).toBe(key);
60
+ }); // Exact synthetic key in local callback, no external model request.
61
+ for (const baseUrl of ['ftp://provider.example/v1', 'http://user:pass@provider.example/v1', 'http://provider.example/v1?key=secret']) {
62
+ expect(await save(human, { ...provider(), id: 'invalid', baseUrl })).toMatchObject({ ok: false, error: 'invalid_hub_provider' });
63
+ }
64
+ });
42
65
  test('existing names-only vault key → Provider reference → actual daemon store, without re-entering key', async () => {
43
66
  const human = client(), daemon = client('daemon');
44
67
  expect(await human('upsert_network_secret', { network_id: net, key: 'SHARED_API_KEY', value: key })).toMatchObject({ ok: true });
@@ -24,7 +24,7 @@ function validConfig(value: any): value is Config {
24
24
  || !Array.isArray(value.models) || !value.models.length || value.models.length > 200
25
25
  || new Set(value.models).size !== value.models.length
26
26
  || value.models.some((m: unknown) => typeof m !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,99}$/.test(m))) return false;
27
- try { const u = new URL(value.baseUrl); return u.protocol === 'https:' && !u.username && !u.password && !u.search && !u.hash; }
27
+ try { const u = new URL(value.baseUrl); return ['https:', 'http:'].includes(u.protocol) && !u.username && !u.password && !u.search && !u.hash; }
28
28
  catch { return false; }
29
29
  }
30
30
  type Row = { revision: number; config_json: string; secret_key_ref: string; updated_at: number };
@@ -0,0 +1,124 @@
1
+ // Short-lived management lane, not task/message text and not a secret archive.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { db } from './db';
4
+ import { getUserNetworkRole } from './auth';
5
+ import { providerScanTargets } from './daemon-provider-snapshots';
6
+ import { validScopedSecretKey } from './shared/scoped-secret-contract';
7
+ type Human = { userId?: string | null; isNode: boolean; networkId: string | null };
8
+ type Daemon = { ok: false; error: string } | { ok: true; networkId: string; daemonNodeId: string; daemonAlias: string };
9
+ type Request = { request_id: string; network_id: string; daemon_node_id: string | null; node_id: string | null;
10
+ action: 'list' | 'put' | 'delete'; key?: string; value?: string; revision?: number;
11
+ status: 'pending' | 'succeeded' | 'failed' | 'expired'; expires_at: number; snapshot?: any; error?: string };
12
+ const requests = new Map<string, Request>();
13
+ const denied = (error: string) => ({ ok: false as const, error });
14
+ function authorize(h: Human) {
15
+ return h.isNode || !h.userId ? denied('user_token_required') : !h.networkId
16
+ || !['owner','admin'].includes(getUserNetworkRole(h.userId, h.networkId) ?? '') ? denied('secret_admin_required') : null;
17
+ }
18
+ function daemonSupported(network: string, daemon: string) {
19
+ const row = db.get<{ config_snapshot: string }>('SELECT config_snapshot FROM nodes WHERE network_id=?1 AND node_id=?2', network, daemon);
20
+ try { const s = JSON.parse(row?.config_snapshot ?? '{}'); return s.role === 'host_supervisor' && s.scoped_secrets_capable === true; } catch { return false; }
21
+ }
22
+ function expire() {
23
+ for (const [id,r] of requests) {
24
+ if (r.expires_at < Date.now() && r.status === 'pending') { r.status = 'expired'; delete r.value; }
25
+ if (r.expires_at + 600_000 < Date.now()) requests.delete(id);
26
+ }
27
+ }
28
+ const timer = setInterval(expire, 10_000); timer.unref();
29
+ function nodeSupported(network: string, node: string) {
30
+ const row = db.get<{ config_snapshot: string }>('SELECT config_snapshot FROM nodes WHERE network_id=?1 AND node_id=?2', network, node);
31
+ try { return JSON.parse(row?.config_snapshot ?? '{}').node_secrets_capable === true; } catch { return false; }
32
+ }
33
+ // Independent node lane: no supervisor lookup, binding, or Daemon credential.
34
+ export function requestNodeSecrets(h: Human, node: string, action: Request['action'], key?: string, value?: string, revision?: number) {
35
+ const refusal = authorize(h); if (refusal) return refusal;
36
+ if (!nodeSupported(h.networkId!, node)) return denied('node_secrets_upgrade_required');
37
+ if (action !== 'list' && (!validScopedSecretKey(key) || !Number.isSafeInteger(revision) || revision! < 0
38
+ || (action === 'put' && (typeof value !== 'string' || !value || Buffer.byteLength(value) > 8192 || value.includes('\0'))))) return denied('invalid_secret_input');
39
+ expire();
40
+ if ([...requests.values()].some(r => r.network_id === h.networkId && r.daemon_node_id === null && r.node_id === node && r.status === 'pending')) return denied('secret_store_busy');
41
+ if (requests.size >= 1000) return denied('secret_queue_busy');
42
+ const r: Request = { request_id: `nsk_${randomUUID()}`, network_id: h.networkId!, daemon_node_id: null,
43
+ node_id: node, action, ...(action === 'list' ? {} : { key, revision, ...(action === 'put' ? { value } : {}) }),
44
+ expires_at: Date.now() + 120_000, status: 'pending' };
45
+ requests.set(r.request_id, r);
46
+ return { ok: true as const, request_id: r.request_id, status: r.status, expires_at: r.expires_at };
47
+ }
48
+ export function pullNodeSecrets(d: Daemon) {
49
+ if (!d.ok || !nodeSupported(d.networkId, d.daemonNodeId)) return denied('caller_not_a_secret_node');
50
+ expire();
51
+ return { ok: true, requests: [...requests.values()].filter(r => r.network_id === d.networkId && r.daemon_node_id === null
52
+ && r.node_id === d.daemonNodeId && r.status === 'pending').map(({ status, snapshot, error, ...input }) => input) };
53
+ }
54
+ export function ackNodeSecrets(d: Daemon, id: string, snapshot: any, failed = false) {
55
+ if (!d.ok || !nodeSupported(d.networkId, d.daemonNodeId)) return denied('caller_not_a_secret_node');
56
+ expire(); const r = requests.get(id);
57
+ if (!r || r.network_id !== d.networkId || r.daemon_node_id !== null || r.node_id !== d.daemonNodeId) return denied('request_not_found');
58
+ if (r.status !== 'pending') return { ok: r.status === 'succeeded' || r.status === 'failed', status: r.status };
59
+ if (!failed) {
60
+ if (!snapshot || Object.keys(snapshot).some(k => !['network_id','daemon_node_id','node_id','revision','activation','keys'].includes(k))
61
+ || snapshot.network_id !== r.network_id || snapshot.daemon_node_id !== null || snapshot.node_id !== r.node_id
62
+ || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0 || snapshot.activation !== 'next_start'
63
+ || !Array.isArray(snapshot.keys) || snapshot.keys.length > 128 || new Set(snapshot.keys.map((x:any)=>x?.key)).size !== snapshot.keys.length
64
+ || snapshot.keys.some((x:any)=>!x || Object.keys(x).some(k=>!['key','source'].includes(k)) || !validScopedSecretKey(x.key) || x.source !== 'node')
65
+ || (r.action !== 'list' && snapshot.revision !== r.revision! + 1)
66
+ || (r.action === 'put' && !snapshot.keys.some((x:any)=>x.key === r.key))
67
+ || (r.action === 'delete' && snapshot.keys.some((x:any)=>x.key === r.key))) return denied('invalid_secret_receipt');
68
+ r.snapshot = snapshot;
69
+ }
70
+ r.status = failed ? 'failed' : 'succeeded'; if (failed) r.error = 'secret_operation_unavailable'; delete r.value;
71
+ return { ok: true, status: r.status };
72
+ }
73
+ export function requestScopedSecrets(h: Human, daemon: string, node: string | null, action: Request['action'], key?: string, value?: string, revision?: number) {
74
+ const refusal = authorize(h); if (refusal) return refusal;
75
+ if (!daemonSupported(h.networkId!, daemon)) return denied('scoped_secrets_upgrade_required');
76
+ if (node !== null && !providerScanTargets(h.networkId!, daemon).some(t => t.node_id === node)) return denied('secret_target_not_bound');
77
+ if (action !== 'list' && (!validScopedSecretKey(key)
78
+ || !Number.isSafeInteger(revision) || revision! < 0 || (action === 'put' && (typeof value !== 'string' || !value || Buffer.byteLength(value) > 8192 || value.includes('\0'))))) return denied('invalid_secret_input');
79
+ expire();
80
+ if ([...requests.values()].some(r => r.network_id === h.networkId && r.daemon_node_id === daemon && r.status === 'pending')) return denied('secret_store_busy');
81
+ if (requests.size >= 1000) return denied('secret_queue_busy');
82
+ const r: Request = { request_id: `dsk_${randomUUID()}`, network_id: h.networkId!, daemon_node_id: daemon,
83
+ node_id: node, action, ...(action === 'list' ? {} : { key, revision, ...(action === 'put' ? { value } : {}) }),
84
+ expires_at: Date.now() + 120_000, status: 'pending' };
85
+ requests.set(r.request_id, r);
86
+ return { ok: true as const, request_id: r.request_id, status: r.status, expires_at: r.expires_at };
87
+ }
88
+ export function getScopedSecrets(h: Human, id: string) {
89
+ const refusal = authorize(h); if (refusal) return refusal; expire();
90
+ const r = requests.get(id);
91
+ if (!r || r.network_id !== h.networkId) return denied('request_not_found');
92
+ return { ok: true as const, request_id: id, daemon_node_id: r.daemon_node_id, node_id: r.node_id,
93
+ status: r.status, ...(r.snapshot ? { snapshot: r.snapshot } : {}), ...(r.error ? { error: r.error } : {}) };
94
+ }
95
+ export function pullScopedSecrets(d: Daemon) {
96
+ if (!d.ok || !daemonSupported(d.networkId, d.daemonNodeId)) return denied('caller_not_a_daemon'); expire();
97
+ const targets = providerScanTargets(d.networkId, d.daemonNodeId);
98
+ return { ok: true, requests: [...requests.values()].filter(r => r.network_id === d.networkId && r.daemon_node_id === d.daemonNodeId && r.status === 'pending')
99
+ .flatMap(r => {
100
+ if (r.node_id !== null && !targets.some(t => t.node_id === r.node_id)) { r.status = 'failed'; r.error = 'secret_target_not_bound'; delete r.value; return []; }
101
+ const { status, snapshot, error, ...input } = r;
102
+ return [{ ...input, targets }];
103
+ }) };
104
+ }
105
+ export function ackScopedSecrets(d: Daemon, id: string, snapshot: any, failed = false) {
106
+ if (!d.ok || !daemonSupported(d.networkId, d.daemonNodeId)) return denied('caller_not_a_daemon'); expire();
107
+ const r = requests.get(id);
108
+ if (!r || r.network_id !== d.networkId || r.daemon_node_id !== d.daemonNodeId) return denied('request_not_found');
109
+ if (r.status !== 'pending') return { ok: r.status === 'succeeded' || r.status === 'failed', status: r.status };
110
+ if (!failed) {
111
+ if (!snapshot || Object.keys(snapshot).some(k => !['network_id','daemon_node_id','node_id','revision','activation','keys'].includes(k))
112
+ || snapshot.network_id !== r.network_id || snapshot.daemon_node_id !== r.daemon_node_id || snapshot.node_id !== r.node_id
113
+ || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0 || snapshot.activation !== 'next_start'
114
+ || !Array.isArray(snapshot.keys) || snapshot.keys.length > 256 || new Set(snapshot.keys.map((x:any)=>x.key)).size !== snapshot.keys.length
115
+ || snapshot.keys.some((x:any) => !x || Object.keys(x).some(k=>!['key','source'].includes(k)) || !validScopedSecretKey(x.key)
116
+ || !['daemon','node'].includes(x.source) || (r.node_id === null && x.source !== 'daemon'))
117
+ || (r.action !== 'list' && snapshot.revision !== r.revision! + 1)
118
+ || (r.action === 'put' && !snapshot.keys.some((x:any)=>x.key === r.key && x.source === (r.node_id ? 'node' : 'daemon')))) return denied('invalid_secret_receipt');
119
+ if (r.node_id !== null && !providerScanTargets(r.network_id,r.daemon_node_id).some(t=>t.node_id === r.node_id)) return denied('secret_target_not_bound');
120
+ r.snapshot = snapshot;
121
+ }
122
+ r.status = failed ? 'failed' : 'succeeded'; if (failed) r.error = 'secret_operation_unavailable'; delete r.value;
123
+ return { ok: true, status: r.status };
124
+ }
@@ -40,7 +40,7 @@ export function validateDaemonProviderSnapshot(value: unknown, networkId: string
40
40
  if (a.account_id !== undefined) fail();
41
41
  if (a.credential_present !== true || typeof a.baseUrl !== "string" || a.baseUrl.length > 2048) fail();
42
42
  let u: URL; try { u = new URL(a.baseUrl); } catch { return fail(); }
43
- if (u.protocol !== "https:" || u.username || u.password || u.search || u.hash) fail();
43
+ if (!["https:", "http:"].includes(u.protocol) || u.username || u.password || u.search || u.hash) fail();
44
44
  } else fail();
45
45
  }
46
46
  }
@@ -0,0 +1,5 @@
1
+ import { isReservedEnvKey } from './reserved-env';
2
+ export const validScopedSecretKey = (key: unknown): key is string => typeof key === 'string'
3
+ && /^[A-Z_][A-Z0-9_]{0,127}$/.test(key) && !isReservedEnvKey(key)
4
+ && !['CODEX_HOME','CLAUDE_CONFIG_DIR','XDG_CONFIG_HOME','XDG_DATA_HOME','TMUX','TMUX_PANE','TEMP','TMP'].includes(key)
5
+ && !['COMMHUB_','ANET_','CLAUDE_CODE_'].some(p => key.startsWith(p));
package/src/tools.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { requestProviderSnapshot, requestProviderWrite, requestProviderCollection, requestProviderApplication, requestProviderLogin, getProviderSnapshot, pullProviderSnapshots, ackProviderSnapshot } from "./daemon-provider-snapshots.js";
3
3
  import { readHubProviders, putHubProvider, bindHubProvider } from './hub-provider-config.js';
4
+ import { requestScopedSecrets, getScopedSecrets, pullScopedSecrets, ackScopedSecrets, requestNodeSecrets, pullNodeSecrets, ackNodeSecrets } from './scoped-secrets.js';
4
5
  import { requestAdopt, getAdopt, ackAdopt, unadopt, resolveManagedDaemon, activeBinding, createdDaemon } from "./node-daemon-bindings.js";
5
6
  import { runtimeReadinessSchema } from "./runtime-readiness.js";
6
7
  import { adoptionCandidatesReportSchema } from "./adoption-candidates.js";
@@ -841,6 +842,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
841
842
  // discovery (#337 extracts this field). "host_supervisor" =
842
843
  // anet daemon. Default-stripping zod would drop this otherwise.
843
844
  role: z.string().max(64).optional().nullable(),
845
+ scoped_secrets_capable: z.boolean().optional(),
844
846
  // RFC-026 §9.3 / #338 PR3 — daemon self-declare nested under
845
847
  // `daemon_capabilities` (canonical shape per existing hub reads
846
848
  // at tools.ts:2010/2075 — PR1/PR2 placed these top-level, hub
@@ -4129,10 +4131,31 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4129
4131
  const providerHuman = (net?: string) => ({ userId: enforceUserId, isNode: callerTokenIsNetwork, networkId: getNetworkId(net) });
4130
4132
  const providerReply = (value: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(value) }] });
4131
4133
  server.tool(
4132
- "daemon_provider_snapshot", "Provider: daemon refresh/put/collect/apply/login; read=request id. login provider={action:start|read|cancel|save|accounts,session_id?,label?}. hub_read=catalog; hub_put=owner config; hub_bind=id+revision,provider={daemon_node_id,daemon_revision,confirm:true}. No login tokens.",
4133
- { action: z.enum(["refresh", "read", "put", "collect", "apply", "login", "hub_read", "hub_put", "hub_bind"]), id: z.string().min(1).max(200), network_id: z.string().max(200).optional(), revision: z.number().int().min(0).optional(), provider: z.unknown().optional(), include_discovery: z.boolean().optional() },
4134
+ "daemon_provider_snapshot", "Provider read/write/login (no tokens). hub_*=owner catalog/distribute. secret_*=Daemon env; node_secret_*=node env. provider={node_id?,key?,value?}; next full start.",
4135
+ { action: z.enum(["refresh", "read", "put", "collect", "apply", "login", "hub_read", "hub_put", "hub_bind", "secret_list", "secret_put", "secret_delete", "secret_read", "node_secret_list", "node_secret_put", "node_secret_delete"]), id: z.string().min(1).max(200), network_id: z.string().max(200).optional(), revision: z.number().int().min(0).optional(), provider: z.unknown().optional(), include_discovery: z.boolean().optional() },
4134
4136
  async ({ action, id, network_id, revision, provider, include_discovery }) => {
4135
4137
  const human = providerHuman(network_id);
4138
+ // Separate scoped-secret contract using the existing management lane.
4139
+ // No additional tools in model context; ordinary Provider actions unchanged.
4140
+ if (action === 'secret_read') return providerReply(getScopedSecrets(human, id));
4141
+ if (action === 'node_secret_list' || action === 'node_secret_put' || action === 'node_secret_delete') {
4142
+ const input = provider as { key?: string; value?: string } | undefined;
4143
+ const result = requestNodeSecrets(human, id, action === 'node_secret_list' ? 'list' : action === 'node_secret_put' ? 'put' : 'delete', input?.key, input?.value, revision);
4144
+ if (result.ok) {
4145
+ const target = db.get<{ alias: string }>('SELECT alias FROM nodes WHERE node_id=?1 AND network_id=?2', id, human.networkId!);
4146
+ if (target) pushEvent(target.alias, { type: 'node_secrets', request_id: result.request_id }, human.networkId!);
4147
+ }
4148
+ return providerReply(result);
4149
+ }
4150
+ if (action === 'secret_list' || action === 'secret_put' || action === 'secret_delete') {
4151
+ const input = provider as { node_id?: string | null; key?: string; value?: string } | undefined;
4152
+ const result = requestScopedSecrets(human, id, input?.node_id ?? null, action === 'secret_list' ? 'list' : action === 'secret_put' ? 'put' : 'delete', input?.key, input?.value, revision);
4153
+ if (result.ok) {
4154
+ const target = db.get<{ alias: string }>('SELECT alias FROM nodes WHERE node_id=?1 AND network_id=?2', id, human.networkId!);
4155
+ if (target) pushEvent(target.alias, { type: 'scoped_secrets', request_id: result.request_id }, human.networkId!);
4156
+ }
4157
+ return providerReply(result);
4158
+ }
4136
4159
  if (action === 'hub_read') return providerReply(readHubProviders(human, (provider as any)?.include_secret_refs === true));
4137
4160
  if (action === 'hub_put') return providerReply(putHubProvider(human, revision!, provider));
4138
4161
  if (action === 'hub_bind') {
@@ -4158,13 +4181,22 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4158
4181
  },
4159
4182
  );
4160
4183
  server.tool(
4161
- "pull_daemon_provider_snapshots", "Daemon Provider work; advertise only implemented secure write/collect/apply/login capabilities.", { accept_writes: z.boolean().optional(), accept_applications: z.boolean().optional(), accept_logins: z.boolean().optional(), accept_local_discovery: z.boolean().optional(), accept_collections: z.boolean().optional() },
4162
- async ({ accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections }) => providerReply(pullProviderSnapshots(resolveCallerDaemonTokenBound(), accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections)),
4184
+ "pull_daemon_provider_snapshots", "Provider/secret work; opt into implemented capabilities only.", { accept_writes: z.boolean().optional(), accept_applications: z.boolean().optional(), accept_logins: z.boolean().optional(), accept_local_discovery: z.boolean().optional(), accept_collections: z.boolean().optional(), accept_secrets: z.boolean().optional(), accept_node_secrets: z.boolean().optional() },
4185
+ async ({ accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections, accept_secrets, accept_node_secrets }) => {
4186
+ const caller = resolveCallerDaemonTokenBound();
4187
+ if (accept_node_secrets) return providerReply(pullNodeSecrets(caller));
4188
+ const result = pullProviderSnapshots(caller, accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections);
4189
+ return providerReply(result.ok && accept_secrets ? { ...result, secret_requests: pullScopedSecrets(caller) } : result);
4190
+ },
4163
4191
  );
4164
4192
  server.tool(
4165
4193
  "ack_daemon_provider_snapshot", "Target daemon acknowledges redacted Provider configuration readback; fixed failure code only.",
4166
4194
  { request_id: z.string().min(1).max(200), snapshot: z.unknown().optional(), failed: z.boolean().optional(), applied_revision: z.number().int().min(0).optional(), application: z.unknown().optional(), login: z.unknown().optional() },
4167
- async ({ request_id, snapshot, failed, applied_revision, application, login }) => providerReply(ackProviderSnapshot(resolveCallerDaemonTokenBound(), request_id, snapshot, failed, applied_revision, application, login)),
4195
+ async ({ request_id, snapshot, failed, applied_revision, application, login }) => providerReply(request_id.startsWith('nsk_')
4196
+ ? ackNodeSecrets(resolveCallerDaemonTokenBound(), request_id, snapshot, failed)
4197
+ : request_id.startsWith('dsk_')
4198
+ ? ackScopedSecrets(resolveCallerDaemonTokenBound(), request_id, snapshot, failed)
4199
+ : ackProviderSnapshot(resolveCallerDaemonTokenBound(), request_id, snapshot, failed, applied_revision, application, login)),
4168
4200
  );
4169
4201
 
4170
4202
  // Helper — map a ValidationError thrown from create-node-validate