@sleep2agi/commhub-server 0.9.0-preview.105 → 0.9.0-preview.107

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/src/tools.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { requestAdopt, getAdopt, ackAdopt, unadopt, resolveManagedDaemon, activeBinding, createdDaemon } from "./node-daemon-bindings.js";
3
+ import { runtimeReadinessSchema } from "./runtime-readiness.js";
2
4
  import { parseDbTimestampMs } from "./db-timestamp.js";
3
5
  import { z } from "zod/v4";
4
- import { nodeHealthSchema, recordNodeHealth } from "./node-health-store.js";
6
+ import { nodeHealthSchema, normalizeNodeHealth, recordNodeHealth } from "./node-health-store.js";
5
7
  import { assertNodeHealthy } from "./node-health-guard.js";
6
8
  import { noteModelAuthHealth } from "./model-auth-notify.js";
7
9
  import { numberedAliasInClause, parseAliasFilter } from "./alias-filter.js";
@@ -30,6 +32,7 @@ import {
30
32
  validateEnvRefs,
31
33
  FLAG_KEYS,
32
34
  validateFlagValue,
35
+ validateFlagsForRuntime,
33
36
  ValidationError,
34
37
  } from "./create-node-validate.js";
35
38
  import {
@@ -805,6 +808,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
805
808
  // 应当 hub 先合)。同一份 schema 里 side_thread_capability / external_schedules
806
809
  // 是 `.strict()` 的,后果不同 —— 见 `host:` 上方那段方框注释。
807
810
  daemon_capabilities: z.object({
811
+ adopt_capable: z.boolean().optional(),
808
812
  runtimes_supported: z.array(z.string().max(64)).max(16).optional(),
809
813
  allowed_secret_keys: z.array(z.string().max(64)).max(64).optional(),
810
814
  max_concurrent_children: z.number().int().min(1).max(1000).optional(),
@@ -879,6 +883,8 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
879
883
  // `.catch(undefined)`:同上一格的立场 —— 一个诊断/展示字段不许拒掉整份 report。
880
884
  // 读取侧再按形状消毒(create-node-validate.ts daemonDefaultWorkdirRoot)。
881
885
  default_workdir_root: z.string().max(1024).optional().catch(undefined),
886
+ // #622 —— 逐 runtime 实测就绪度。收得宽、坏了只丢这一格(见 runtime-readiness.ts)。
887
+ runtime_readiness: runtimeReadinessSchema,
882
888
  }).optional(),
883
889
  }).optional().describe("RFC-024 — masked node config snapshot"),
884
890
  // app#225 follow-up — the reporting process answers the `rules_file`
@@ -1152,7 +1158,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
1152
1158
  // #448 — only the node token bound to this alias may speak for its health
1153
1159
  // (same rule as the *_capable flags above).
1154
1160
  const acceptedHealth = nodeHealth && callerTokenIsNetwork && callerAlias && callerAlias === effectiveAlias
1155
- ? nodeHealth
1161
+ ? normalizeNodeHealth(nodeHealth) // #594 — strict codex_login shape (the wire schema is loose on purpose)
1156
1162
  : undefined;
1157
1163
  if (acceptedHealth) {
1158
1164
  recordNodeHealth(sessionNetId, effectiveAlias, acceptedHealth);
@@ -4252,6 +4258,7 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4252
4258
  if (!(FLAG_KEYS as readonly string[]).includes(k)) throw new ValidationError("flag_key_unknown", { field: k });
4253
4259
  validateFlagValue(k, v);
4254
4260
  }
4261
+ validateFlagsForRuntime(node_spec.runtime, node_spec.flags);
4255
4262
  } catch (e) {
4256
4263
  return validationFailReply(e);
4257
4264
  }
@@ -4653,55 +4660,22 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4653
4660
  delete_config: boolean;
4654
4661
  confirm_alias?: string;
4655
4662
  };
4656
- // RFC-027 PR2 prereq — auto-resolve daemon_node_id from child_node_id.
4657
- // The dashboard (and most callers of stop_node / delete_node) shouldn't
4658
- // need to track the daemon→child mapping themselves; the hub has it on
4659
- // node_create_requests at child creation time. Returns null when no
4660
- // creation record exists (orphan node row OR pre-RFC-026 node) — caller
4661
- // must then pass daemon_node_id explicitly. node_id derivation:
4662
- // `node_${request_id.replace(/^cr_/,"")}` (see create-node-daemon.ts
4663
- // and PR1 BLOCKER-1 fix), so we reverse it: `cr_${node_id.slice(5)}`.
4663
+ // Board #625: creation authority first, then an acknowledged adoption binding.
4664
+ // Supplying a daemon id explicitly is routing, never proof of authority.
4664
4665
  const resolveDaemonForChild = (child_node_id: string): string | null => {
4665
- if (!child_node_id.startsWith("node_")) return null;
4666
- const requestId = `cr_${child_node_id.slice(5)}`;
4667
- const row = db.get<{ daemon_node_id: string }>(
4668
- `SELECT daemon_node_id FROM node_create_requests WHERE request_id = ?1`,
4669
- requestId,
4670
- );
4671
- return row?.daemon_node_id ?? null;
4666
+ return resolveManagedDaemon(child_node_id);
4672
4667
  };
4673
4668
 
4674
- /**
4675
- * 为什么这里要分两种「解析不到 daemon」(#196 / app 仓)。
4676
- *
4677
- * `resolveDaemonForChild` 对两件完全不同的事都返回 `null`:
4678
- * ① id 不以 `node_` 开头 —— 这个节点**根本不是 daemon 创建的**,
4679
- * 是有人在某台机器上直接 `anet node start` 起来的。hub 上没有任何
4680
- * daemon 可以代它执行停止,**这条路径对它在概念上就不成立**。
4681
- * ② id 以 `node_` 开头,但 `node_create_requests` 里查不到那一行 ——
4682
- * 它可能确实是 daemon 建的,只是记录缺失,这时「显式传 daemon_node_id」
4683
- * 是一条真的走得通的路。
4684
- *
4685
- * 🔴 原先两种情况打印同一句 `pass daemon_node_id explicitly`。对 ① 来说
4686
- * **这句建议的前提是假的** —— 没有 daemon 可传,用户按它做只会走进死路。
4687
- * 2026-08-28 对生产 hub 实测:218 个节点里 **207 个**是 ① 这种。
4688
- *
4689
- * 信息在源头就存在(那个 `startsWith("node_")` 判断),只是被丢在了返回值里。
4690
- */
4669
+ // Preserve old error codes for clients; both now require real managed authority.
4691
4670
  const explainUnresolvableDaemon = (child_node_id: string) =>
4692
4671
  child_node_id.startsWith("node_")
4693
4672
  ? {
4694
4673
  ok: false, error: "daemon_not_resolvable",
4695
- message: "no node_create_requests row found for this child_node_id; pass daemon_node_id explicitly",
4674
+ message: "no creation authority or active adoption binding; adopt this node before remote stop/start",
4696
4675
  }
4697
4676
  : {
4698
4677
  ok: false, error: "not_daemon_managed",
4699
- message:
4700
- `node ${child_node_id} was not created by a daemon (it was started by hand with ` +
4701
- `\`anet node start\` on some machine), so no daemon on the Hub can stop it ` +
4702
- `(naming a daemon_node_id does not change that). ` +
4703
- `Run \`anet node stop <alias>\` on that machine instead. ` +
4704
- `Daemon-created children have ids beginning with \`node_\`; this one does not.`,
4678
+ message: "node has no creation authority or active adoption binding; adopt it or run `anet node stop <alias>` on its host",
4705
4679
  };
4706
4680
 
4707
4681
  const dispatchStopOrDelete = (args: DispatchArgs, clientNetId?: string | null) => {
@@ -4816,27 +4790,14 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
4816
4790
  return { ok: false, error: "daemon_cross_tenant", message: "daemon and child are in different networks" };
4817
4791
  }
4818
4792
 
4819
- // #1448 finding-4 — daemon 必须是该 child 的**权威创建者**,镜像 start_node 的
4820
- // daemon_child_mismatch。此前 stop/delete 接受调用方传入的 daemon_node_id,只校验
4821
- // 它存在 + 同网,不验它真的创建了这个 child。同网调用方传错 daemon_node_id → 门铃
4822
- // 路由到错 daemon。🔴 f3(#1453) 合入后更糟:错 daemon 无该 child 的 map entry → 走
4823
- // 收敛分支 sweep(错机器 pgrep 空) + ack stopped → hub 假收敛 stopped、child 仍在
4824
- // 正确机器上跑(旧 noop 至少留下可见的卡死态)。网络 scope 只挡跨租户,网内是 footgun。
4825
- //
4826
- // 用「确定性不匹配」判据:authority 已知(child 有 create 记录)且不等 → 拒。不对
4827
- // authority==null(create 记录被裁剪的旧 child)加拒,避免给它们的 stop/delete 引入
4828
- // 新失败面——handler 的 daemon_node_id??resolveDaemonForChild 已兜住无法解析的情形。
4793
+ // Board #625 closes the missing-create-record exception, regardless of id prefix.
4829
4794
  const authoritativeDaemonId = resolveDaemonForChild(args.child_node_id);
4830
- // #571 — 手工起的节点(id 不以 `node_` 开头)**没有**任何 daemon 链接。此前
4831
- // authority==null 时放行调用方点名的任意同网 daemon → 门铃发给那台 daemon,
4832
- // 它本地没这个 child 的记录,会走「按 alias 全机 pgrep + SIGTERM」的收敛分支,
4833
- // 杀掉那台机器上所有同 alias 的 agent-node(不分工作目录/HOME/网络)。
4834
- // 对 hand-started 节点这条路在概念上就不成立(见 explainUnresolvableDaemon ①),
4835
- // 所以不管调用方传没传 daemon_node_id 一律拒。`node_` 开头但缺 create 记录的
4836
- // (②,显式传 daemon 是文档化的出路)保持原行为。
4837
- if (!authoritativeDaemonId && !args.child_node_id.startsWith("node_")) {
4795
+ if (!authoritativeDaemonId) {
4838
4796
  return explainUnresolvableDaemon(args.child_node_id);
4839
4797
  }
4798
+ if (args.action === "delete" && !createdDaemon(args.child_node_id) && activeBinding(args.child_node_id)) {
4799
+ return { ok: false, error: "adopted_node_delete_unsupported" };
4800
+ }
4840
4801
  if (authoritativeDaemonId && authoritativeDaemonId !== args.daemon_node_id) {
4841
4802
  return { ok: false, error: "daemon_child_mismatch", message: "daemon_node_id is not this child's authoritative creator daemon" };
4842
4803
  }
@@ -5460,15 +5421,49 @@ export function registerTools(server: McpServer, clientIP?: string, enforceNetwo
5460
5421
  AND COALESCE(n.lifecycle_state, 'active') NOT IN ('stopped', 'stop_failed')`,
5461
5422
  callerDaemon.daemonNodeId, callerDaemon.networkId,
5462
5423
  );
5463
- const children = rows.map(r => ({
5424
+ const children: Array<{ child_node_id: string; alias: string; lifecycle_state: string; managed?: "adopted" }> = rows.map(r => ({
5464
5425
  child_node_id: r.child_node_id,
5465
5426
  alias: r.child_name,
5466
5427
  lifecycle_state: r.lifecycle_state ?? "active",
5467
5428
  }));
5429
+ const adopted = db.all<{ node_id: string; alias: string; lifecycle_state: string | null }>(
5430
+ `SELECT n.node_id,n.alias,n.lifecycle_state FROM node_daemon_bindings b
5431
+ JOIN nodes n ON n.node_id=b.node_id AND n.network_id=b.network_id
5432
+ WHERE b.daemon_node_id=?1 AND b.network_id=?2 AND b.status='active'`,
5433
+ callerDaemon.daemonNodeId, callerDaemon.networkId,
5434
+ );
5435
+ for (const n of adopted) children.push({ child_node_id: n.node_id, alias: n.alias, lifecycle_state: n.lifecycle_state ?? "active", managed: "adopted" });
5468
5436
  return { content: [{ type: "text" as const, text: JSON.stringify({ ok: true, count: children.length, children }) }] };
5469
5437
  },
5470
5438
  );
5471
5439
 
5440
+ const adoptionHuman = () => ({ userId: enforceUserId, networkId: enforceNetworkId, isNode: callerTokenIsNetwork, canWrite });
5441
+ const adoptionReply = (result: unknown) => ({ content: [{ type: "text" as const, text: JSON.stringify(result) }] });
5442
+ server.tool(
5443
+ "request_adopt_node",
5444
+ "Human owner/admin requests adoption. Only the target capable daemon may activate the binding after local verification; no process is signalled.",
5445
+ { node_id: z.string().min(1).max(200), daemon_node_id: z.string().min(1).max(200), workdir: z.string().min(1).max(4096) },
5446
+ async args => adoptionReply(requestAdopt(adoptionHuman(), args)),
5447
+ );
5448
+ server.tool(
5449
+ "get_adopt_request",
5450
+ "Target daemon pulls a pending adoption request for local verification.",
5451
+ { request_id: z.string().min(1).max(200) },
5452
+ async ({ request_id }) => adoptionReply(getAdopt(resolveCallerDaemonTokenBound(), request_id)),
5453
+ );
5454
+ server.tool(
5455
+ "ack_adopt_request",
5456
+ "Target daemon confirms local registration or refuses adoption. Revoked requests cannot reactivate.",
5457
+ { request_id: z.string().min(1).max(200), status: z.enum(["adopted", "refused"]), error: z.string().max(1000).optional() },
5458
+ async args => adoptionReply(ackAdopt(resolveCallerDaemonTokenBound(), args)),
5459
+ );
5460
+ server.tool(
5461
+ "unadopt_node",
5462
+ "Human owner/admin revokes a pending or active binding without stopping the node.",
5463
+ { node_id: z.string().min(1).max(200) },
5464
+ async ({ node_id }) => adoptionReply(unadopt(adoptionHuman(), node_id)),
5465
+ );
5466
+
5472
5467
  // ── RFC-028 P1 — Provider & Model Registry + connectivity probe ──
5473
5468
  // Background timers (probe GC + sweeper) idempotent; safe to call
5474
5469
  // per registerTools.