@clawos-dev/clawd 0.2.231 → 0.2.233

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +99 -145
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -5556,11 +5556,12 @@ var init_schemas = __esm({
5556
5556
  });
5557
5557
 
5558
5558
  // ../protocol/src/frames.ts
5559
- var PROTOCOL_VERSION;
5559
+ var PROTOCOL_VERSION, DISPATCH_MCP_ID;
5560
5560
  var init_frames = __esm({
5561
5561
  "../protocol/src/frames.ts"() {
5562
5562
  "use strict";
5563
5563
  PROTOCOL_VERSION = 1;
5564
+ DISPATCH_MCP_ID = "dispatch";
5564
5565
  }
5565
5566
  });
5566
5567
 
@@ -12148,17 +12149,8 @@ function buildSpawnArgs(ctx) {
12148
12149
  if (ctx.addDirs) {
12149
12150
  for (const dir of ctx.addDirs) args.push("--add-dir", dir);
12150
12151
  }
12151
- if (ctx.dispatchMcpConfigPath) {
12152
- args.push("--mcp-config", ctx.dispatchMcpConfigPath);
12153
- }
12154
- if (ctx.ticketMcpConfigPath) {
12155
- args.push("--mcp-config", ctx.ticketMcpConfigPath);
12156
- }
12157
- if (ctx.shiftMcpConfigPath) {
12158
- args.push("--mcp-config", ctx.shiftMcpConfigPath);
12159
- }
12160
- if (ctx.inboxMcpConfigPath) {
12161
- args.push("--mcp-config", ctx.inboxMcpConfigPath);
12152
+ for (const mcp of ctx.mcpConfigs ?? []) {
12153
+ args.push("--mcp-config", mcp.path);
12162
12154
  }
12163
12155
  args.push("--disallowed-tools", "CronCreate,CronDelete,CronList,ScheduleWakeup,RemoteTrigger");
12164
12156
  if (ctx.effort) args.push("--effort", ctx.effort);
@@ -41818,6 +41810,7 @@ var import_node_os4 = __toESM(require("os"), 1);
41818
41810
  var import_node_path7 = __toESM(require("path"), 1);
41819
41811
 
41820
41812
  // src/session/reducer.ts
41813
+ init_protocol();
41821
41814
  init_runtime();
41822
41815
 
41823
41816
  // src/permission/rules.ts
@@ -41953,10 +41946,10 @@ var CONTACT_SSH_SYSTEM_PROMPT_HINT = `## \u8DE8\u8BBE\u5907\u6587\u4EF6\u8BBF\u9
41953
41946
  # execPath+cliPath\uFF0C\u4FDD\u8BC1 relay \u8DDF daemon \u7248\u672C\u540C\u6B65\u3002\u4E0D\u8981\u7528\u88F8 \`clawd\`\uFF08\u8D70 $PATH \u4F1A\u6F02\uFF09\u3002
41954
41947
  \`\`\`
41955
41948
 
41956
- **A \u6388\u6743\u54EA\u4E9B\u76EE\u5F55\u4E0D\u7528\u4F60\u63D0\u524D\u8BB0**\uFF1AA \u4FA7\u7684 sshd \u62E8\u53F7\u4F1A\u5F3A\u5236\u8D70 sandbox jail\uFF0C\u8FDB\u6C99\u7BB1\u524D\u4F1A\u628A\u6388\u6743\u76EE\u5F55\u5217\u8868\u5199\u5230 stderr\uFF08\u683C\u5F0F \`[clawd-ssh-jail] Access allowed to these directories: ...\`\uFF09\u3002\u4F60\u5373\u4F7F\u731C\u9519\u8DEF\u5F84\u4E5F\u4F1A\u88AB sandbox deny\uFF0C\u4F46 stderr \u91CC\u5C31\u81EA\u5E26\u7B54\u6848\uFF0C\u8BD5\u9519\u4E00\u6B21\u5C31\u5B66\u5230 A \u6388\u6743\u4E86\u54EA\u4E9B\u76EE\u5F55\uFF0C\u4E0B\u6B21\u76F4\u63A5\u7528\u5BF9\u7684\u8DEF\u5F84\u3002
41949
+ **A \u5EFA\u8BAE\u5728\u54EA\u4E9B\u76EE\u5F55\u5E72\u6D3B**\uFF1AA \u4FA7 jail \u8FDB shell \u524D\u4F1A\u628A\u5EFA\u8BAE\u76EE\u5F55\u5217\u8868\u5199\u5230 stderr\uFF08\u683C\u5F0F \`[clawd-ssh-jail] Suggested working directories for this contact: ...\`\uFF09\u4F5C\u4E3A\u53C2\u8003\u3002\u6CE8\u610F\uFF1A**\u6CA1\u6709\u76EE\u5F55\u7EA7\u5F3A\u5236\u9650\u5236**\u2014\u2014\u4F60\u8BBF\u95EE\u7684\u662F A \u673A\u5668\u4E0A\u8BE5 SSH \u7528\u6237\u6743\u9650\u5185\u7684\u6587\u4EF6\uFF0C\u8BF7\u53EA\u5728 A \u6388\u6743/\u9700\u8981\u7684\u8303\u56F4\u5185\u64CD\u4F5C\uFF0C\u4E0D\u8981\u8D8A\u754C\u7FFB\u770B\u65E0\u5173\u76EE\u5F55\u3002
41957
41950
 
41958
41951
  **\u6CE8\u610F**\uFF1A
41959
- - \u6388\u6743\u76EE\u5F55\u6570\u636E\u6E90\u5B9E\u65F6\uFF08jail \u6BCF\u6B21\u8BFB A \u4FA7\u6700\u65B0\u914D\u7F6E\uFF09\uFF0CA \u6539\u4E86\u6388\u6743\u540E\u4F60\u4E0B\u4E00\u6B21\u62E8\u53F7\u7ACB\u5373\u611F\u77E5
41952
+ - \u5EFA\u8BAE\u76EE\u5F55\u6570\u636E\u6E90\u5B9E\u65F6\uFF08jail \u6BCF\u6B21\u8BFB A \u4FA7\u6700\u65B0\u914D\u7F6E\uFF09\uFF0CA \u6539\u4E86 exposedDirs \u540E\u4F60\u4E0B\u4E00\u6B21\u62E8\u53F7\u7ACB\u5373\u611F\u77E5
41960
41953
  - \u6392\u969C\uFF1A\u672C\u673A debug \u65E5\u5FD7\u5728 \`~/.clawd/log/contact-ssh.log\`\uFF08\u7528\u6237\u53EF \`tail -f\` \u770B\u6BCF\u4E00\u6B65\uFF09\uFF0C\u4F60\u51FA\u9519\u65F6\u544A\u8BC9\u7528\u6237\u67E5\u8FD9\u4E2A\u6587\u4EF6
41961
41954
  - \u5982\u679C \`ls ~/.clawd/contact-ssh-keys/\` \u4E3A\u7A7A\uFF0C\u8BF4\u660E\u8FD8\u6CA1\u6709\u5BF9\u7AEF\u6388\u6743\u4F60\u6216 key \u8FD8\u6CA1\u62C9\u5230\uFF08B \u4FA7 daemon \u6BCF 60s \u4E00\u6B21\u81EA\u52A8\u62C9\uFF09\uFF0C\u5982\u5B9E\u544A\u8BC9\u7528\u6237
41962
41955
  `;
@@ -42045,10 +42038,7 @@ function buildSpawnContext(state, deps) {
42045
42038
  if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
42046
42039
  const personaId = file.ownerPersonaId;
42047
42040
  if (personaId) env.CLAWD_PERSONA_ID = personaId;
42048
- const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
42049
- const ticketMcpConfigPath2 = deps.getTicketMcpConfigPath?.() ?? null;
42050
- const shiftMcpConfigPath2 = deps.getShiftMcpConfigPath?.() ?? null;
42051
- const inboxMcpConfigPath2 = deps.getInboxMcpConfigPath?.() ?? null;
42041
+ const mcpConfigs = deps.getMcpConfigs?.() ?? [];
42052
42042
  const ctx = {
42053
42043
  cwd: file.cwd,
42054
42044
  toolSessionId: file.toolSessionId,
@@ -42059,12 +42049,7 @@ function buildSpawnContext(state, deps) {
42059
42049
  // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
42060
42050
  // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
42061
42051
  personaRoot: deps.personaRoot,
42062
- // Persona dispatch:所有 session(A B)都挂同一份 dispatch.mcp.json,让两边的 cc
42063
- // 都能看到 personaDispatch / personaDispatchComplete tool;按 env 区分实际行为。
42064
- dispatchMcpConfigPath: dispatchMcpConfigPath2 ?? void 0,
42065
- ticketMcpConfigPath: ticketMcpConfigPath2 ?? void 0,
42066
- shiftMcpConfigPath: shiftMcpConfigPath2 ?? void 0,
42067
- inboxMcpConfigPath: inboxMcpConfigPath2 ?? void 0,
42052
+ ...mcpConfigs.length > 0 ? { mcpConfigs } : {},
42068
42053
  // shift v1:把 SessionFile.ownerPersonaId 透传到 SpawnContext;buildSpawnArgs 等下游
42069
42054
  // 不直接读这个字段(只读 env),但保留显式字段方便 daemon 内部诊断/日志。
42070
42055
  personaId
@@ -42092,7 +42077,7 @@ function buildSpawnContext(state, deps) {
42092
42077
  }
42093
42078
  ctx.extraSystemPrompt = buildConnectionPrompt(promptArgs);
42094
42079
  }
42095
- if (dispatchMcpConfigPath2) {
42080
+ if (mcpConfigs.some((c) => c.id === DISPATCH_MCP_ID)) {
42096
42081
  ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + DISPATCH_SYSTEM_PROMPT_HINT;
42097
42082
  }
42098
42083
  if (daemonUrl) {
@@ -42875,13 +42860,9 @@ var SessionRunner = class {
42875
42860
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
42876
42861
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
42877
42862
  getDaemonUrl: this.hooks.getDaemonUrl,
42878
- // Persona dispatch:透传 dispatch.mcp.json 路径闭包 + B→dispatchId 反查闭包。
42879
- // 见 reducer.buildSpawnContext CLAWD_DISPATCH_ID env / 派生 SpawnContext.dispatchMcpConfigPath
42880
- getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
42881
- getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
42882
- getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
42883
- // Ticket MCP:透传 ticket.mcp.json 路径闭包(所有 persona session 都挂,无 gating)
42884
- getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
42863
+ // daemon MCP server 列表闭包透传;见 reducer.buildSpawnContext 派生
42864
+ // SpawnContext.mcpConfigs + DISPATCH_MCP_ID 判断 dispatch 教学注入
42865
+ getMcpConfigs: this.hooks.getMcpConfigs,
42885
42866
  lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
42886
42867
  // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
42887
42868
  mode: this.hooks.mode,
@@ -43711,12 +43692,9 @@ var SessionManager = class {
43711
43692
  // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
43712
43693
  // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
43713
43694
  getDaemonUrl: this.deps.getDaemonUrl,
43714
- // Persona dispatch (Task 7): dispatch.mcp.json 路径 + B sessionId → dispatchId 反查
43715
- // 闭包透传给 reducer,让 cc spawn 加 --mcp-config flag + B session CLAWD_DISPATCH_ID env.
43716
- getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
43717
- getTicketMcpConfigPath: this.deps.ticketMcpConfigPath ? () => this.deps.ticketMcpConfigPath ?? null : void 0,
43718
- getShiftMcpConfigPath: this.deps.shiftMcpConfigPath ? () => this.deps.shiftMcpConfigPath ?? null : void 0,
43719
- getInboxMcpConfigPath: this.deps.inboxMcpConfigPath ? () => this.deps.inboxMcpConfigPath ?? null : void 0,
43695
+ // daemon MCP server 列表闭包透传给 reducer,cc spawn 逐项加 --mcp-config flag;
43696
+ // B session CLAWD_DISPATCH_ID env 由下方 lookupDispatchByBSessionId 反查闭包负责。
43697
+ getMcpConfigs: () => this.deps.mcpConfigs ?? [],
43720
43698
  lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
43721
43699
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
43722
43700
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
@@ -48169,17 +48147,8 @@ function buildTuiSpawnArgs(ctx, isResume = false) {
48169
48147
  if (ctx.addDirs) {
48170
48148
  for (const dir of ctx.addDirs) args.push("--add-dir", dir);
48171
48149
  }
48172
- if (ctx.dispatchMcpConfigPath) {
48173
- args.push("--mcp-config", ctx.dispatchMcpConfigPath);
48174
- }
48175
- if (ctx.ticketMcpConfigPath) {
48176
- args.push("--mcp-config", ctx.ticketMcpConfigPath);
48177
- }
48178
- if (ctx.shiftMcpConfigPath) {
48179
- args.push("--mcp-config", ctx.shiftMcpConfigPath);
48180
- }
48181
- if (ctx.inboxMcpConfigPath) {
48182
- args.push("--mcp-config", ctx.inboxMcpConfigPath);
48150
+ for (const mcp of ctx.mcpConfigs ?? []) {
48151
+ args.push("--mcp-config", mcp.path);
48183
48152
  }
48184
48153
  args.push("--disallowed-tools", "CronCreate,CronDelete,CronList,ScheduleWakeup,RemoteTrigger");
48185
48154
  if (ctx.effort) args.push("--effort", ctx.effort);
@@ -53495,9 +53464,8 @@ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
53495
53464
  # 真实请求(interactive shell 时为空)。
53496
53465
  #
53497
53466
  # 职责:
53498
- # 1. 读 ~/.clawd/contacts.json contact.exposedDirs
53499
- # 2. macOS 用 sandbox-exec + sbpl; Linux 用 bwrap
53500
- # 3. exec 沙箱 shell
53467
+ # 1. 读 ~/.clawd/contacts.json 校验 contact.sshAllowed,读 exposedDirs 作为提示
53468
+ # 2. 直接 exec shell(去沙箱后无目录级限制;exposedDirs echo 给对端参考)
53501
53469
 
53502
53470
  set -euo pipefail
53503
53471
 
@@ -53544,33 +53512,19 @@ for c in data.get('contacts', []):
53544
53512
  sys.exit(3)
53545
53513
  ")
53546
53514
 
53547
- if [ -z "$EXPOSED_JSON" ]; then
53548
- log_line ERROR jail.entered "contact ${"${DEVICE_ID}"} 不在 store 或 exposedDirs 为空"
53549
- echo "clawd-ssh-jail: contact not found or no exposed dirs" >&2
53550
- exit 1
53515
+ # exposedDirs 可能为空(sshAllowed=true 但未配目录)——去沙箱后不再因空目录拒绝,直接放行。
53516
+ if [ -n "$EXPOSED_JSON" ]; then
53517
+ log_line INFO jail.entered "exposedDirs 提示已加载,放行 exec shell"
53518
+ # 告知 SSH client 建议目录(发到 stderr)。去沙箱后这些只是建议,不做目录级强制限制;
53519
+ # exposedDirs 仅供对端参考,运行时读,A 改后下次拨号即感知。
53520
+ echo "[clawd-ssh-jail] Suggested working directories for this contact:" >&2
53521
+ while IFS= read -r d; do
53522
+ echo " - $d" >&2
53523
+ done <<< "$EXPOSED_JSON"
53524
+ echo "[clawd-ssh-jail] Note: no directory-level restriction is enforced." >&2
53525
+ else
53526
+ log_line INFO jail.entered "sshAllowed=true 且未配 exposedDirs,直接放行 exec shell"
53551
53527
  fi
53552
- log_line INFO jail.entered "exposedDirs 白名单已加载,进沙箱 exec shell"
53553
-
53554
- # 进沙箱前告知 SSH client 授权目录列表(发到 stderr)—— CC 或其他 SSH client 试错一次就能
53555
- # 学到 exposedDirs 具体值,无需 A 侧向 B 侧同步(授权数据实时的 single source of truth)。
53556
- # 每次拨号运行时读,A 改 exposedDirs 后 B 侧下一次拨号立即感知。
53557
- echo "[clawd-ssh-jail] Access allowed to these directories:" >&2
53558
- while IFS= read -r d; do
53559
- echo " - $d" >&2
53560
- done <<< "$EXPOSED_JSON"
53561
- echo "[clawd-ssh-jail] Anything outside these paths will be sandbox-denied." >&2
53562
-
53563
- # 校验路径安全(bash 侧二次防御)
53564
- while IFS= read -r line; do
53565
- case "$line" in
53566
- /*) : ;;
53567
- *) echo "clawd-ssh-jail: bad path: $line" >&2; exit 1 ;;
53568
- esac
53569
- case "$line" in
53570
- *[\"\'\`\$\;\|\&\(\)\{\}\[\]\<\>\*\?]*)
53571
- echo "clawd-ssh-jail: unsafe path: $line" >&2; exit 1 ;;
53572
- esac
53573
- done <<< "$EXPOSED_JSON"
53574
53528
 
53575
53529
  CMD="${"${SSH_ORIGINAL_COMMAND:-}"}"
53576
53530
  if [ -z "$CMD" ]; then
@@ -53579,48 +53533,11 @@ else
53579
53533
  SHELL_CMD=(bash -c "$CMD")
53580
53534
  fi
53581
53535
 
53582
- case "$(uname -s)" in
53583
- Darwin)
53584
- POLICY="(version 1)
53585
- (deny default)
53586
- (allow process*)
53587
- (allow signal (target self))
53588
- (allow sysctl-read)
53589
- (allow mach-lookup)
53590
- (allow file-read-metadata)
53591
- (allow network*)
53592
- (allow file-read* (subpath \"/usr\"))
53593
- (allow file-read* (subpath \"/bin\"))
53594
- (allow file-read* (subpath \"/sbin\"))
53595
- (allow file-read* (subpath \"/System\"))
53596
- (allow file-read* (subpath \"/Library\"))
53597
- (allow file-read* (subpath \"/etc\"))
53598
- (allow file-read* (subpath \"/private/etc\"))"
53599
- while IFS= read -r d; do
53600
- POLICY+="
53601
- (allow file-read* file-write* (subpath \"$d\"))"
53602
- done <<< "$EXPOSED_JSON"
53603
- exec sandbox-exec -p "$POLICY" "${"${SHELL_CMD[@]}"}"
53604
- ;;
53605
- Linux)
53606
- BWRAP_ARGS=(
53607
- --unshare-user --unshare-ipc --unshare-pid --unshare-uts
53608
- --die-with-parent
53609
- --proc /proc --dev /dev --tmpfs /tmp
53610
- --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /sbin /sbin
53611
- --ro-bind /lib /lib --ro-bind /etc /etc
53612
- )
53613
- if [ -d /lib64 ]; then BWRAP_ARGS+=(--ro-bind /lib64 /lib64); fi
53614
- while IFS= read -r d; do
53615
- BWRAP_ARGS+=(--bind "$d" "$d")
53616
- done <<< "$EXPOSED_JSON"
53617
- exec bwrap "${"${BWRAP_ARGS[@]}"}" "${"${SHELL_CMD[@]}"}"
53618
- ;;
53619
- *)
53620
- echo "clawd-ssh-jail: unsupported OS $(uname -s)" >&2
53621
- exit 1
53622
- ;;
53623
- esac
53536
+ # 去掉 OS 沙箱层(原本 macOS Seatbelt 策略 / Linux 命名空间隔离):SSH 进来即该用户
53537
+ # 权限内的 shell,直接 exec。沙箱只限制文件、拦不住网络/进程,隔离价值有限,且相关 API
53538
+ # 在 macOS 已 deprecated。授权即完全信任——sshAllowed 授权开关 + authorized_keys 即时
53539
+ # 踢行是唯一访问闸。要加回沙箱:从 git 历史(T-29 之前)恢复本段的 OS 分支即可。
53540
+ exec "${"${SHELL_CMD[@]}"}"
53624
53541
  `;
53625
53542
  function ensureJailScript(dataDir) {
53626
53543
  const binDir = import_node_path37.default.join(dataDir, "bin");
@@ -53667,6 +53584,8 @@ var SshdManager = class {
53667
53584
  stopping = false;
53668
53585
  exitHookInstalled = false;
53669
53586
  startupTimeoutMs;
53587
+ // sshd stderr 按行嗌探 pubkey 认证结果时的半行缓冲(防关键字被 chunk 边界切断)
53588
+ sniffBuf = "";
53670
53589
  get port() {
53671
53590
  return this.deps.port;
53672
53591
  }
@@ -53714,7 +53633,11 @@ var SshdManager = class {
53714
53633
  logStream.write(String(c));
53715
53634
  };
53716
53635
  proc.stdout?.on("data", tee);
53717
- proc.stderr?.on("data", tee);
53636
+ proc.stderr?.on("data", (c) => {
53637
+ const s = String(c);
53638
+ logStream.write(s);
53639
+ this.sniffSshdAuth(s);
53640
+ });
53718
53641
  proc.once("exit", () => logStream.end());
53719
53642
  const ready = await waitForSshdReady(proc, this.startupTimeoutMs);
53720
53643
  if (!ready.ok) {
@@ -53801,7 +53724,36 @@ ${tail}` : ready.error;
53801
53724
  } catch {
53802
53725
  }
53803
53726
  }
53727
+ // 把 sshd -e 输出里的 pubkey 认证结果转写一条到 contact-ssh.log,让 A 侧审计日志一处即可
53728
+ // 看到"隧道连上但 sshd 拒了 key"(否则只有 tunnel.connected + tunnel.disconnected,为什么
53729
+ // 被拒要翻另一个 sshd.log 对时间线)。按 \n 切行嗅探,半行留 sniffBuf 等下个 chunk 拼回。
53730
+ sniffSshdAuth(chunk) {
53731
+ const sshLog = this.deps.sshLog;
53732
+ if (!sshLog) return;
53733
+ this.sniffBuf += chunk;
53734
+ const parts = this.sniffBuf.split("\n");
53735
+ this.sniffBuf = parts.pop() ?? "";
53736
+ for (const line of parts) {
53737
+ if (/Accepted publickey/i.test(line)) {
53738
+ sshLog.info(
53739
+ "ssh.auth-ok",
53740
+ "sshd \u63A5\u53D7\u4E86 pubkey \u8BA4\u8BC1\uFF08\u5BF9\u7AEF key \u547D\u4E2D authorized_keys\uFF09",
53741
+ sshdAuthMeta(line)
53742
+ );
53743
+ } else if (/Failed publickey/i.test(line) || /Connection closed by (authenticating|invalid) user/i.test(line)) {
53744
+ sshLog.warn(
53745
+ "ssh.auth-rejected",
53746
+ "sshd \u62D2\u7EDD\u4E86 pubkey \u8BA4\u8BC1\uFF08\u5BF9\u7AEF key \u4E0D\u5728 authorized_keys / \u672A\u6388\u6743 / key \u672A\u62C9\u5230\uFF09",
53747
+ sshdAuthMeta(line)
53748
+ );
53749
+ }
53750
+ }
53751
+ }
53804
53752
  };
53753
+ function sshdAuthMeta(line) {
53754
+ const m2 = /port (\d+)/.exec(line);
53755
+ return m2 ? { clientPort: Number(m2[1]) } : {};
53756
+ }
53805
53757
  async function waitForSshdReady(proc, timeoutMs) {
53806
53758
  return new Promise((resolve6) => {
53807
53759
  let settled = false;
@@ -54192,6 +54144,11 @@ async function handleSshTunnelLocalUpgrade(req, socket, head, deps) {
54192
54144
  url: outboundUrl
54193
54145
  });
54194
54146
  deps.wss.handleUpgrade(req, socket, head, (clientWs) => {
54147
+ sshLog.info(
54148
+ "relay.client-connected",
54149
+ "relay CLI \u5DF2\u8FDE\u4E0A\u672C\u5730 daemon\uFF08client WS \u5347\u7EA7\u6210\u529F\uFF09\uFF0C\u5F00\u59CB\u62E8 peer",
54150
+ { peerDeviceId, peerDisplayName: contact.displayName }
54151
+ );
54195
54152
  const makeOut = deps.makeOutboundWs ?? ((u, o) => new import_websocket.default(u, o));
54196
54153
  const outbound = makeOut(outboundUrl, {
54197
54154
  headers: { Authorization: `Bearer ${contact.connectToken}` }
@@ -59689,6 +59646,7 @@ async function startDaemon(config) {
59689
59646
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
59690
59647
  const personaDispatchManager = new PersonaDispatchManager({ genId: () => v4_default() });
59691
59648
  const here = typeof __dirname === "string" ? __dirname : import_node_path64.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
59649
+ const mcpConfigs = [];
59692
59650
  const dispatchServerCandidates = [
59693
59651
  import_node_path64.default.join(here, "dispatch", "mcp-server.cjs"),
59694
59652
  // 生产 dist/index → dist/dispatch/mcp-server.cjs
@@ -59696,16 +59654,16 @@ async function startDaemon(config) {
59696
59654
  // dev tsx src/index → ../dist/dispatch/mcp-server.cjs
59697
59655
  ];
59698
59656
  const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59699
- let dispatchMcpConfigPath2;
59700
59657
  if (dispatchServerScriptPath) {
59701
59658
  const dispatchLogPath = import_node_path64.default.join(logDir, "dispatch-mcp-server.log");
59702
- dispatchMcpConfigPath2 = writeDispatchMcpConfig({
59659
+ const dispatchCfgPath = writeDispatchMcpConfig({
59703
59660
  dataDir: config.dataDir,
59704
59661
  serverScriptPath: dispatchServerScriptPath,
59705
59662
  logPath: dispatchLogPath
59706
59663
  });
59664
+ mcpConfigs.push({ id: DISPATCH_MCP_ID, path: dispatchCfgPath });
59707
59665
  logger.info("dispatch.mcp.json written", {
59708
- path: dispatchMcpConfigPath2,
59666
+ path: dispatchCfgPath,
59709
59667
  server: dispatchServerScriptPath,
59710
59668
  serverLog: dispatchLogPath
59711
59669
  });
@@ -59719,18 +59677,18 @@ async function startDaemon(config) {
59719
59677
  import_node_path64.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
59720
59678
  ];
59721
59679
  const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59722
- let ticketMcpConfigPath2;
59723
59680
  if (ticketServerScriptPath) {
59724
59681
  const ticketLogPath = import_node_path64.default.join(logDir, "ticket-mcp-server.log");
59725
- ticketMcpConfigPath2 = writeTicketMcpConfig({
59682
+ const ticketCfgPath = writeTicketMcpConfig({
59726
59683
  dataDir: config.dataDir,
59727
59684
  serverScriptPath: ticketServerScriptPath,
59728
59685
  apiBaseUrl: config.clawosApi ?? DEFAULT_CLAWOS_API,
59729
59686
  apiKey: resolveClawdTicketsApiKey(),
59730
59687
  logPath: ticketLogPath
59731
59688
  });
59689
+ mcpConfigs.push({ id: "ticket", path: ticketCfgPath });
59732
59690
  logger.info("ticket.mcp.json written", {
59733
- path: ticketMcpConfigPath2,
59691
+ path: ticketCfgPath,
59734
59692
  server: ticketServerScriptPath,
59735
59693
  serverLog: ticketLogPath
59736
59694
  });
@@ -59744,16 +59702,16 @@ async function startDaemon(config) {
59744
59702
  import_node_path64.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
59745
59703
  ];
59746
59704
  const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59747
- let shiftMcpConfigPath2;
59748
59705
  if (shiftServerScriptPath) {
59749
59706
  const shiftLogPath = import_node_path64.default.join(logDir, "shift-mcp-server.log");
59750
- shiftMcpConfigPath2 = await writeShiftMcpConfig({
59707
+ const shiftCfgPath = await writeShiftMcpConfig({
59751
59708
  dataDir: config.dataDir,
59752
59709
  serverScriptPath: shiftServerScriptPath,
59753
59710
  logPath: shiftLogPath
59754
59711
  });
59712
+ mcpConfigs.push({ id: "shift", path: shiftCfgPath });
59755
59713
  logger.info("shift.mcp.json written", {
59756
- path: shiftMcpConfigPath2,
59714
+ path: shiftCfgPath,
59757
59715
  server: shiftServerScriptPath,
59758
59716
  serverLog: shiftLogPath
59759
59717
  });
@@ -59768,16 +59726,16 @@ async function startDaemon(config) {
59768
59726
  import_node_path64.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
59769
59727
  ];
59770
59728
  const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59771
- let inboxMcpConfigPath2;
59772
59729
  if (inboxServerScriptPath) {
59773
59730
  const inboxLogPath = import_node_path64.default.join(logDir, "inbox-mcp-server.log");
59774
- inboxMcpConfigPath2 = await writeInboxMcpConfig({
59731
+ const inboxCfgPath = await writeInboxMcpConfig({
59775
59732
  dataDir: config.dataDir,
59776
59733
  serverScriptPath: inboxServerScriptPath,
59777
59734
  logPath: inboxLogPath
59778
59735
  });
59736
+ mcpConfigs.push({ id: "inbox", path: inboxCfgPath });
59779
59737
  logger.info("inbox.mcp.json written", {
59780
- path: inboxMcpConfigPath2,
59738
+ path: inboxCfgPath,
59781
59739
  server: inboxServerScriptPath,
59782
59740
  serverLog: inboxLogPath
59783
59741
  });
@@ -59823,18 +59781,12 @@ async function startDaemon(config) {
59823
59781
  // 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
59824
59782
  // 的 isLoopback 兜底已确保安全。
59825
59783
  getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
59826
- // Persona dispatch (Task 8): manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
59827
- // - personaDispatchManager: manager.createDispatchedSession 用它 registerBSession;
59828
- // reducer 通过闭包反查 dispatchId 注 CLAWD_DISPATCH_ID env。
59829
- // - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
59784
+ // Persona dispatch (Task 8): manager.createDispatchedSession personaDispatchManager
59785
+ // registerBSession;reducer 通过闭包反查 dispatchId CLAWD_DISPATCH_ID env。
59830
59786
  personaDispatchManager,
59831
- dispatchMcpConfigPath: dispatchMcpConfigPath2,
59832
- ticketMcpConfigPath: ticketMcpConfigPath2,
59833
- // shift v1:所有 persona session 都挂 shift.mcp.json(不做 persona gating)。
59834
- // 缺省 = shift-mcp-server.cjs 没 build 出来,cc 看不到 shift tool 但 scheduler 仍跑。
59835
- shiftMcpConfigPath: shiftMcpConfigPath2,
59836
- // sendDm MCP tool:所有 persona session 都挂 inbox.mcp.json(无 gating)。
59837
- inboxMcpConfigPath: inboxMcpConfigPath2,
59787
+ // daemon 级 MCP server 登记表(dispatch / ticket / shift / inbox / ...,上方各 writer
59788
+ // 块写盘成功即登记);reducer 透传到 SpawnContext.mcpConfigs,cc spawn 逐项 --mcp-config。
59789
+ mcpConfigs,
59838
59790
  broadcastFrame: (frame, target) => {
59839
59791
  if (target === "all") {
59840
59792
  transport?.broadcastAll(frame);
@@ -60656,7 +60608,9 @@ ${bar}
60656
60608
  port: config.sshdPort,
60657
60609
  logger,
60658
60610
  installProcessExitHandlers: true,
60659
- onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info)
60611
+ onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info),
60612
+ // sshd 的 pubkey 认证结果转写进 contact-ssh.log(补 A 侧「隧道连上但 sshd 拒了 key」盲区)
60613
+ sshLog
60660
60614
  });
60661
60615
  try {
60662
60616
  await sshdMgr.start();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.231",
3
+ "version": "0.2.233",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",