@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sleep2agi/commhub-server",
3
- "version": "0.9.0-preview.127",
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",
@@ -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", "Daemon Provider actions; read=id. Login provider={action,session_id?,label?}, no tokens. hub_*=owner catalog/distribute. secret_*=env persistence, 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"]), 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", "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(), accept_secrets: z.boolean().optional() },
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('dsk_')
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
  );