@sleep2agi/commhub-server 0.9.0-preview.127 → 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 +1 -1
- package/src/scoped-secrets.ts +45 -1
- package/src/tools.ts +18 -6
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",
|
package/src/scoped-secrets.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { providerScanTargets } from './daemon-provider-snapshots';
|
|
|
6
6
|
import { validScopedSecretKey } from './shared/scoped-secret-contract';
|
|
7
7
|
type Human = { userId?: string | null; isNode: boolean; networkId: string | null };
|
|
8
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; node_id: string | null;
|
|
9
|
+
type Request = { request_id: string; network_id: string; daemon_node_id: string | null; node_id: string | null;
|
|
10
10
|
action: 'list' | 'put' | 'delete'; key?: string; value?: string; revision?: number;
|
|
11
11
|
status: 'pending' | 'succeeded' | 'failed' | 'expired'; expires_at: number; snapshot?: any; error?: string };
|
|
12
12
|
const requests = new Map<string, Request>();
|
|
@@ -26,6 +26,50 @@ function expire() {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
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
|
+
}
|
|
29
73
|
export function requestScopedSecrets(h: Human, daemon: string, node: string | null, action: Request['action'], key?: string, value?: string, revision?: number) {
|
|
30
74
|
const refusal = authorize(h); if (refusal) return refusal;
|
|
31
75
|
if (!daemonSupported(h.networkId!, daemon)) return denied('scoped_secrets_upgrade_required');
|
package/src/tools.ts
CHANGED
|
@@ -1,7 +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 } from './scoped-secrets.js';
|
|
4
|
+
import { requestScopedSecrets, getScopedSecrets, pullScopedSecrets, ackScopedSecrets, requestNodeSecrets, pullNodeSecrets, ackNodeSecrets } from './scoped-secrets.js';
|
|
5
5
|
import { requestAdopt, getAdopt, ackAdopt, unadopt, resolveManagedDaemon, activeBinding, createdDaemon } from "./node-daemon-bindings.js";
|
|
6
6
|
import { runtimeReadinessSchema } from "./runtime-readiness.js";
|
|
7
7
|
import { adoptionCandidatesReportSchema } from "./adoption-candidates.js";
|
|
@@ -4131,13 +4131,22 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4131
4131
|
const providerHuman = (net?: string) => ({ userId: enforceUserId, isNode: callerTokenIsNetwork, networkId: getNetworkId(net) });
|
|
4132
4132
|
const providerReply = (value: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(value) }] });
|
|
4133
4133
|
server.tool(
|
|
4134
|
-
"daemon_provider_snapshot", "
|
|
4135
|
-
{ action: z.enum(["refresh", "read", "put", "collect", "apply", "login", "hub_read", "hub_put", "hub_bind", "secret_list", "secret_put", "secret_delete", "secret_read"]), 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() },
|
|
4136
4136
|
async ({ action, id, network_id, revision, provider, include_discovery }) => {
|
|
4137
4137
|
const human = providerHuman(network_id);
|
|
4138
4138
|
// Separate scoped-secret contract using the existing management lane.
|
|
4139
4139
|
// No additional tools in model context; ordinary Provider actions unchanged.
|
|
4140
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
|
+
}
|
|
4141
4150
|
if (action === 'secret_list' || action === 'secret_put' || action === 'secret_delete') {
|
|
4142
4151
|
const input = provider as { node_id?: string | null; key?: string; value?: string } | undefined;
|
|
4143
4152
|
const result = requestScopedSecrets(human, id, input?.node_id ?? null, action === 'secret_list' ? 'list' : action === 'secret_put' ? 'put' : 'delete', input?.key, input?.value, revision);
|
|
@@ -4172,9 +4181,10 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4172
4181
|
},
|
|
4173
4182
|
);
|
|
4174
4183
|
server.tool(
|
|
4175
|
-
"pull_daemon_provider_snapshots", "
|
|
4176
|
-
async ({ accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections, accept_secrets }) => {
|
|
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 }) => {
|
|
4177
4186
|
const caller = resolveCallerDaemonTokenBound();
|
|
4187
|
+
if (accept_node_secrets) return providerReply(pullNodeSecrets(caller));
|
|
4178
4188
|
const result = pullProviderSnapshots(caller, accept_writes, accept_applications, accept_logins, accept_local_discovery, accept_collections);
|
|
4179
4189
|
return providerReply(result.ok && accept_secrets ? { ...result, secret_requests: pullScopedSecrets(caller) } : result);
|
|
4180
4190
|
},
|
|
@@ -4182,7 +4192,9 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
|
|
|
4182
4192
|
server.tool(
|
|
4183
4193
|
"ack_daemon_provider_snapshot", "Target daemon acknowledges redacted Provider configuration readback; fixed failure code only.",
|
|
4184
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() },
|
|
4185
|
-
async ({ request_id, snapshot, failed, applied_revision, application, login }) => providerReply(request_id.startsWith('
|
|
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_')
|
|
4186
4198
|
? ackScopedSecrets(resolveCallerDaemonTokenBound(), request_id, snapshot, failed)
|
|
4187
4199
|
: ackProviderSnapshot(resolveCallerDaemonTokenBound(), request_id, snapshot, failed, applied_revision, application, login)),
|
|
4188
4200
|
);
|