@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.
|
|
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
|
|
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 (
|
|
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
|
|
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", "
|
|
4162
|
-
async ({ 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(
|
|
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
|