@clawos-dev/clawd 0.2.232 → 0.2.234

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/dist/cli.cjs CHANGED
@@ -4820,6 +4820,14 @@ var init_schemas = __esm({
4820
4820
  * 或 deps.ownerDisplayName(owner)。
4821
4821
  */
4822
4822
  creatorDisplayName: external_exports.string().min(1).optional(),
4823
+ /**
4824
+ * 创建该 session 的连接者飞书 union_id(一次性 stamp,T-31 persona prompt injection)。
4825
+ * session:create / dispatch spawnB handler 从 ctx.principal.feishuUnionId(guest 场景来自
4826
+ * connect token subOwner)或 deps.ownerFeishuUnionId(owner 场景)传入;newRunner 时读这个
4827
+ * 字段派生 SubSessionMeta.callerFeishuUnionId 注入 cc system prompt「身份卡」的 union_id 行。
4828
+ * Optional:老 SessionFile / owner 未登飞书路径缺省,缺省时 prompt 不渲染该行(向后兼容)。
4829
+ */
4830
+ creatorFeishuUnionId: external_exports.string().min(1).optional(),
4823
4831
  /**
4824
4832
  * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4825
4833
  * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
@@ -41423,11 +41431,17 @@ function scopeSubPath(scope) {
41423
41431
  if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
41424
41432
  return [scope.personaId, scope.mode];
41425
41433
  }
41426
- function metaFromScope(scope, personaRoot, usersRoot, ownerPrincipalId, callerDisplayName) {
41434
+ function metaFromScope(scope, personaRoot, usersRoot, ownerPrincipalId, callerDisplayName, callerFeishuUnionId) {
41427
41435
  if (scope.kind === "default") return void 0;
41428
41436
  if (scope.mode === "owner") {
41429
41437
  if (!ownerPrincipalId || !usersRoot) {
41430
- return { idleKillEnabled: false, personaMode: "owner", callerDisplayName };
41438
+ return {
41439
+ idleKillEnabled: false,
41440
+ personaMode: "owner",
41441
+ callerDisplayName,
41442
+ callerDeviceId: ownerPrincipalId,
41443
+ callerFeishuUnionId
41444
+ };
41431
41445
  }
41432
41446
  const userWorkDir2 = deriveUserWorkDir(ownerPrincipalId, usersRoot);
41433
41447
  return {
@@ -41435,7 +41449,9 @@ function metaFromScope(scope, personaRoot, usersRoot, ownerPrincipalId, callerDi
41435
41449
  personaMode: "owner",
41436
41450
  userWorkDir: userWorkDir2,
41437
41451
  addDirs: [userWorkDir2],
41438
- callerDisplayName
41452
+ callerDisplayName,
41453
+ callerDeviceId: ownerPrincipalId,
41454
+ callerFeishuUnionId
41439
41455
  };
41440
41456
  }
41441
41457
  const userWorkDir = deriveUserWorkDir(scope.capId, usersRoot);
@@ -41444,7 +41460,9 @@ function metaFromScope(scope, personaRoot, usersRoot, ownerPrincipalId, callerDi
41444
41460
  personaMode: "guest",
41445
41461
  userWorkDir,
41446
41462
  addDirs: [userWorkDir],
41447
- callerDisplayName
41463
+ callerDisplayName,
41464
+ callerDeviceId: scope.capId,
41465
+ callerFeishuUnionId
41448
41466
  };
41449
41467
  }
41450
41468
  function scopeForFile(file, ownerPrincipalId) {
@@ -41844,30 +41862,61 @@ function buildRule(tool, input) {
41844
41862
  }
41845
41863
 
41846
41864
  // src/persona/connection-prompt.ts
41847
- var OWNER_TEMPLATE = `# \u8FDE\u63A5\u4E0A\u4E0B\u6587
41848
- \u4F60\u73B0\u5728\u4EE5 Owner \u8EAB\u4EFD\u88AB\u8FDE\u63A5\uFF0C\u5BF9\u65B9\u5C31\u662F owner \u672C\u4EBA\uFF08{ownerLabel}\uFF09\u3002
41849
- \u81EA\u6211\u4ECB\u7ECD\u65F6\u4F7F\u7528\u7B2C\u4E00/\u7B2C\u4E8C\u4EBA\u79F0\uFF0C\u4F8B\u5982\u300C\u6211\u662F\u60A8\u7684 xxx \u52A9\u624B\u300D\u3002`;
41850
- var GUEST_TEMPLATE = `# \u8FDE\u63A5\u4E0A\u4E0B\u6587
41851
- \u4F60\u73B0\u5728\u4EE5 Guest \u8EAB\u4EFD\u88AB\u8FDE\u63A5\uFF0C\u672C persona \u7684 owner \u662F\u300C{ownerLabel}\u300D\uFF0C\u5F53\u524D\u5BF9\u8BDD\u7684\u5BF9\u65B9\u662F\u300C{guestLabel}\u300D\uFF0C\u4E0D\u662F owner \u672C\u4EBA\u3002
41852
- \u81EA\u6211\u4ECB\u7ECD\u65F6\u7B2C\u4E09\u4EBA\u79F0\u6307\u4EE3 owner\u3001\u7B2C\u4E8C\u4EBA\u79F0\u79F0\u547C\u5BF9\u65B9\uFF0C\u4F8B\u5982\u300C{guestLabel}\uFF0C\u6211\u662F {ownerLabel} \u7684 xxx \u52A9\u624B\uFF0C\u4F60\u53EF\u4EE5\u901A\u8FC7\u6211\u770B\u5230\u4ED6\u7684 xxx\u300D\u3002\u4E0D\u8981\u628A\u5BF9\u65B9\u5F53\u6210 owner\uFF0C\u4E0D\u8981\u66FF owner \u505A\u51FA\u627F\u8BFA\u3002`;
41865
+ function renderIdentityLines(fields) {
41866
+ const lines = [`- \u663E\u793A\u540D\uFF1A${fields.label}`];
41867
+ if (fields.feishuUnionId) lines.push(`- \u98DE\u4E66 union_id\uFF1A${fields.feishuUnionId}`);
41868
+ if (fields.deviceId) lines.push(`- \u8BBE\u5907 id\uFF1A${fields.deviceId}`);
41869
+ if (fields.extraLastLine) lines.push(fields.extraLastLine);
41870
+ return lines.join("\n");
41871
+ }
41853
41872
  function buildConnectionPrompt(args) {
41854
- let result;
41855
- if (args.mode === "guest") {
41856
- result = GUEST_TEMPLATE.replace(/\{ownerLabel\}/g, args.ownerLabel).replace(
41857
- /\{guestLabel\}/g,
41858
- args.guestLabel
41873
+ const parts = ["# \u8FDE\u63A5\u4E0A\u4E0B\u6587"];
41874
+ if (args.mode === "owner") {
41875
+ parts.push("\u5F53\u524D\u8FDE\u63A5\u65B9\uFF08\u5C31\u662F owner \u672C\u4EBA\uFF09\uFF1A");
41876
+ parts.push(
41877
+ renderIdentityLines({
41878
+ label: args.ownerLabel,
41879
+ feishuUnionId: args.ownerFeishuUnionId,
41880
+ deviceId: args.ownerDeviceId
41881
+ })
41859
41882
  );
41860
41883
  } else {
41861
- result = OWNER_TEMPLATE.replace(/\{ownerLabel\}/g, args.ownerLabel);
41884
+ parts.push("\u5F53\u524D\u8FDE\u63A5\u65B9\uFF08\u4E0D\u662F owner \u672C\u4EBA\uFF09\uFF1A");
41885
+ parts.push(
41886
+ renderIdentityLines({
41887
+ label: args.guestLabel,
41888
+ feishuUnionId: args.guestFeishuUnionId,
41889
+ deviceId: args.guestDeviceId,
41890
+ extraLastLine: "- \u8EAB\u4EFD\uFF1AGuest\uFF08\u8FDC\u7A0B\u8FDE\u63A5\u8FDB\u6765\u7684\u8054\u7CFB\u4EBA\uFF09"
41891
+ })
41892
+ );
41893
+ parts.push("");
41894
+ parts.push("\u672C persona \u7684 owner\uFF1A");
41895
+ parts.push(
41896
+ renderIdentityLines({
41897
+ label: args.ownerLabel,
41898
+ feishuUnionId: args.ownerFeishuUnionId,
41899
+ deviceId: args.ownerDeviceId
41900
+ })
41901
+ );
41862
41902
  }
41863
41903
  if (args.workDir) {
41864
- result += `
41865
- \u4F60\u7684\u6587\u4EF6\u5DE5\u4F5C\u76EE\u5F55\u662F ${args.workDir}\uFF0C\u8FD9\u4E2A\u76EE\u5F55\u5BF9\u4F60\u53EF\u8BFB\u53EF\u5199\uFF0C\u6587\u4EF6\u7684\u8BFB\u53D6/\u521B\u5EFA/\u4FEE\u6539\u90FD\u5728\u8FD9\u91CC\u8FDB\u884C\u3002`;
41904
+ parts.push("");
41905
+ let line = `\u4F60\u7684\u6587\u4EF6\u5DE5\u4F5C\u76EE\u5F55\u662F ${args.workDir}\uFF0C\u8FD9\u4E2A\u76EE\u5F55\u5BF9\u4F60\u53EF\u8BFB\u53EF\u5199\uFF0C\u6587\u4EF6\u7684\u8BFB\u53D6/\u521B\u5EFA/\u4FEE\u6539\u90FD\u5728\u8FD9\u91CC\u8FDB\u884C\u3002`;
41866
41906
  if (args.mode === "guest") {
41867
- result += "persona \u76EE\u5F55\u5BF9\u4F60\u53EA\u8BFB\uFF0C\u4E0D\u8981\u5F80\u91CC\u5199\u3002";
41907
+ line += "persona \u76EE\u5F55\u5BF9\u4F60\u53EA\u8BFB\uFF0C\u4E0D\u8981\u5F80\u91CC\u5199\u3002";
41868
41908
  }
41909
+ parts.push(line);
41869
41910
  }
41870
- return result;
41911
+ parts.push("");
41912
+ if (args.mode === "owner") {
41913
+ parts.push("\u81EA\u6211\u4ECB\u7ECD\u65F6\u4F7F\u7528\u7B2C\u4E00/\u7B2C\u4E8C\u4EBA\u79F0\uFF0C\u4F8B\u5982\u300C\u6211\u662F\u60A8\u7684 xxx \u52A9\u624B\u300D\u3002");
41914
+ } else {
41915
+ parts.push(
41916
+ `\u81EA\u6211\u4ECB\u7ECD\u65F6\u7B2C\u4E09\u4EBA\u79F0\u6307\u4EE3 owner\u3001\u7B2C\u4E8C\u4EBA\u79F0\u79F0\u547C\u5BF9\u65B9\uFF0C\u4F8B\u5982\u300C${args.guestLabel}\uFF0C\u6211\u662F ${args.ownerLabel} \u7684 xxx \u52A9\u624B\uFF0C\u4F60\u53EF\u4EE5\u901A\u8FC7\u6211\u770B\u5230\u4ED6\u7684 xxx\u300D\u3002\u4E0D\u8981\u628A\u5BF9\u65B9\u5F53\u6210 owner\uFF0C\u4E0D\u8981\u66FF owner \u505A\u51FA\u627F\u8BFA\u3002`
41917
+ );
41918
+ }
41919
+ return parts.join("\n");
41871
41920
  }
41872
41921
 
41873
41922
  // src/persona/sharing-prompt.ts
@@ -41946,10 +41995,10 @@ var CONTACT_SSH_SYSTEM_PROMPT_HINT = `## \u8DE8\u8BBE\u5907\u6587\u4EF6\u8BBF\u9
41946
41995
  # 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
41947
41996
  \`\`\`
41948
41997
 
41949
- **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
41998
+ **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
41950
41999
 
41951
42000
  **\u6CE8\u610F**\uFF1A
41952
- - \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
42001
+ - \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
41953
42002
  - \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
41954
42003
  - \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
41955
42004
  `;
@@ -42065,13 +42114,19 @@ function buildSpawnContext(state, deps) {
42065
42114
  promptArgs = {
42066
42115
  mode: "guest",
42067
42116
  ownerLabel: deps.ownerDisplayName,
42117
+ ownerDeviceId: deps.ownerDeviceId,
42118
+ ownerFeishuUnionId: deps.ownerFeishuUnionId,
42068
42119
  guestLabel: meta.callerDisplayName,
42120
+ guestDeviceId: meta.callerDeviceId,
42121
+ guestFeishuUnionId: meta.callerFeishuUnionId,
42069
42122
  workDir: meta.userWorkDir
42070
42123
  };
42071
42124
  } else {
42072
42125
  promptArgs = {
42073
42126
  mode: "owner",
42074
42127
  ownerLabel: meta.callerDisplayName ?? deps.ownerDisplayName,
42128
+ ownerDeviceId: meta.callerDeviceId ?? deps.ownerDeviceId,
42129
+ ownerFeishuUnionId: meta.callerFeishuUnionId ?? deps.ownerFeishuUnionId,
42075
42130
  workDir: meta.userWorkDir
42076
42131
  };
42077
42132
  }
@@ -42856,6 +42911,8 @@ var SessionRunner = class {
42856
42911
  resolveContextWindow: this.hooks.resolveContextWindow,
42857
42912
  genUuid: this.hooks.genUuid ?? v4_default,
42858
42913
  ownerDisplayName: this.hooks.ownerDisplayName,
42914
+ ownerDeviceId: this.hooks.ownerDeviceId,
42915
+ ownerFeishuUnionId: this.hooks.ownerFeishuUnionId,
42859
42916
  personaRoot: this.hooks.personaRoot,
42860
42917
  // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
42861
42918
  // curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
@@ -43634,12 +43691,14 @@ var SessionManager = class {
43634
43691
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
43635
43692
  const store = this.storeFor(scope);
43636
43693
  const callerDisplayName = file.creatorDisplayName ?? (scope.kind === "persona" ? scope.mode === "owner" ? this.deps.ownerDisplayName : this.deps.contactStore?.get(scope.capId)?.displayName : void 0);
43694
+ const callerFeishuUnionId = file.creatorFeishuUnionId ?? (scope.kind === "persona" && scope.mode === "owner" ? this.deps.ownerFeishuUnionId : void 0);
43637
43695
  const subSessionMeta = metaFromScope(
43638
43696
  scope,
43639
43697
  this.deps.personaRoot ?? "",
43640
43698
  this.deps.usersRoot ?? "",
43641
43699
  this.deps.ownerPrincipalId,
43642
- callerDisplayName
43700
+ callerDisplayName,
43701
+ callerFeishuUnionId
43643
43702
  );
43644
43703
  if (subSessionMeta?.userWorkDir) {
43645
43704
  import_node_fs9.default.mkdirSync(subSessionMeta.userWorkDir, { recursive: true });
@@ -43686,6 +43745,11 @@ var SessionManager = class {
43686
43745
  // personaStore 不再透给 runner hooks(in-place mention 注入路径已删,runner 不用);
43687
43746
  // manager 自身的 sandbox 派生仍直接读 this.deps.personaStore。
43688
43747
  ownerDisplayName: this.deps.ownerDisplayName,
43748
+ // T-31 persona prompt injection:owner 设备 id(= ownerPrincipalId)+ 飞书 union_id
43749
+ // 供 reducer 组 promptArgs 时作为「本 persona 的 owner」身份卡数据源(guest session 用)
43750
+ // 或「当前连接方」身份卡的兜底源(owner session 老路径未 stamp caller 时)。
43751
+ ownerDeviceId: this.deps.ownerPrincipalId,
43752
+ ownerFeishuUnionId: this.deps.ownerFeishuUnionId,
43689
43753
  // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
43690
43754
  // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
43691
43755
  personaRoot: this.deps.personaRoot,
@@ -43801,7 +43865,7 @@ var SessionManager = class {
43801
43865
  }
43802
43866
  }
43803
43867
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
43804
- create(args, creatorPrincipalId, creatorDisplayName) {
43868
+ create(args, creatorPrincipalId, creatorDisplayName, creatorFeishuUnionId) {
43805
43869
  let cwd;
43806
43870
  if (args.ownerPersonaId) {
43807
43871
  cwd = derivePersonaSpawnCwd(
@@ -43846,6 +43910,7 @@ var SessionManager = class {
43846
43910
  ephemeral: args.ephemeral,
43847
43911
  creatorPrincipalId,
43848
43912
  creatorDisplayName,
43913
+ ...creatorFeishuUnionId ? { creatorFeishuUnionId } : {},
43849
43914
  createdAt: iso,
43850
43915
  updatedAt: iso
43851
43916
  };
@@ -44616,7 +44681,9 @@ var SessionManager = class {
44616
44681
  // creatorDisplayName = A 的 displayName,供 guest meta callerDisplayName(reducer 要求)。
44617
44682
  ...args.guestPrincipalId ? {
44618
44683
  creatorPrincipalId: args.guestPrincipalId,
44619
- ...args.guestDisplayName ? { creatorDisplayName: args.guestDisplayName } : {}
44684
+ ...args.guestDisplayName ? { creatorDisplayName: args.guestDisplayName } : {},
44685
+ // T-31: A 的飞书 union_id stamp 到 B 的 SessionFile;guest meta 由 newRunner 派生
44686
+ ...args.guestFeishuUnionId ? { creatorFeishuUnionId: args.guestFeishuUnionId } : {}
44620
44687
  } : {},
44621
44688
  // T-20:dispatch model override 落盘(reducer spawn 路径读 SessionFile.model 生成 --model flag)
44622
44689
  ...args.model ? { model: args.model } : {},
@@ -48890,6 +48957,8 @@ function buildPersonaDispatchHandlers(deps) {
48890
48957
  prompt: args.prompt,
48891
48958
  guestPrincipalId: ctx.principal.id,
48892
48959
  guestDisplayName: ctx.principal.displayName,
48960
+ // T-31: 把 A 的飞书 union_id 透传给 B(connectGuestContext 已把它挂在 principal.feishuUnionId)
48961
+ ...ctx.principal.feishuUnionId ? { guestFeishuUnionId: ctx.principal.feishuUnionId } : {},
48893
48962
  ...args.model ? { model: args.model } : {}
48894
48963
  }).then(() => logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2 })).catch((err) => {
48895
48964
  const reason = err instanceof Error ? err.message : String(err);
@@ -50662,7 +50731,7 @@ function ownerContext(ownerPrincipalId, displayName, feishuUnionId) {
50662
50731
  }
50663
50732
  function connectGuestContext(subDevice, subOwner, provider, displayName) {
50664
50733
  return {
50665
- principal: { id: subDevice, kind: "guest", displayName },
50734
+ principal: { id: subDevice, kind: "guest", displayName, feishuUnionId: subOwner },
50666
50735
  grants: PERSONAL_CAP_GRANTS.map((g2) => ({
50667
50736
  resource: { ...g2.resource },
50668
50737
  actions: [...g2.actions]
@@ -53464,9 +53533,8 @@ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
53464
53533
  # 真实请求(interactive shell 时为空)。
53465
53534
  #
53466
53535
  # 职责:
53467
- # 1. 读 ~/.clawd/contacts.json contact.exposedDirs
53468
- # 2. macOS 用 sandbox-exec + sbpl; Linux 用 bwrap
53469
- # 3. exec 沙箱 shell
53536
+ # 1. 读 ~/.clawd/contacts.json 校验 contact.sshAllowed,读 exposedDirs 作为提示
53537
+ # 2. 直接 exec shell(去沙箱后无目录级限制;exposedDirs echo 给对端参考)
53470
53538
 
53471
53539
  set -euo pipefail
53472
53540
 
@@ -53513,33 +53581,19 @@ for c in data.get('contacts', []):
53513
53581
  sys.exit(3)
53514
53582
  ")
53515
53583
 
53516
- if [ -z "$EXPOSED_JSON" ]; then
53517
- log_line ERROR jail.entered "contact ${"${DEVICE_ID}"} 不在 store 或 exposedDirs 为空"
53518
- echo "clawd-ssh-jail: contact not found or no exposed dirs" >&2
53519
- exit 1
53584
+ # exposedDirs 可能为空(sshAllowed=true 但未配目录)——去沙箱后不再因空目录拒绝,直接放行。
53585
+ if [ -n "$EXPOSED_JSON" ]; then
53586
+ log_line INFO jail.entered "exposedDirs 提示已加载,放行 exec shell"
53587
+ # 告知 SSH client 建议目录(发到 stderr)。去沙箱后这些只是建议,不做目录级强制限制;
53588
+ # exposedDirs 仅供对端参考,运行时读,A 改后下次拨号即感知。
53589
+ echo "[clawd-ssh-jail] Suggested working directories for this contact:" >&2
53590
+ while IFS= read -r d; do
53591
+ echo " - $d" >&2
53592
+ done <<< "$EXPOSED_JSON"
53593
+ echo "[clawd-ssh-jail] Note: no directory-level restriction is enforced." >&2
53594
+ else
53595
+ log_line INFO jail.entered "sshAllowed=true 且未配 exposedDirs,直接放行 exec shell"
53520
53596
  fi
53521
- log_line INFO jail.entered "exposedDirs 白名单已加载,进沙箱 exec shell"
53522
-
53523
- # 进沙箱前告知 SSH client 授权目录列表(发到 stderr)—— CC 或其他 SSH client 试错一次就能
53524
- # 学到 exposedDirs 具体值,无需 A 侧向 B 侧同步(授权数据实时的 single source of truth)。
53525
- # 每次拨号运行时读,A 改 exposedDirs 后 B 侧下一次拨号立即感知。
53526
- echo "[clawd-ssh-jail] Access allowed to these directories:" >&2
53527
- while IFS= read -r d; do
53528
- echo " - $d" >&2
53529
- done <<< "$EXPOSED_JSON"
53530
- echo "[clawd-ssh-jail] Anything outside these paths will be sandbox-denied." >&2
53531
-
53532
- # 校验路径安全(bash 侧二次防御)
53533
- while IFS= read -r line; do
53534
- case "$line" in
53535
- /*) : ;;
53536
- *) echo "clawd-ssh-jail: bad path: $line" >&2; exit 1 ;;
53537
- esac
53538
- case "$line" in
53539
- *[\"\'\`\$\;\|\&\(\)\{\}\[\]\<\>\*\?]*)
53540
- echo "clawd-ssh-jail: unsafe path: $line" >&2; exit 1 ;;
53541
- esac
53542
- done <<< "$EXPOSED_JSON"
53543
53597
 
53544
53598
  CMD="${"${SSH_ORIGINAL_COMMAND:-}"}"
53545
53599
  if [ -z "$CMD" ]; then
@@ -53548,48 +53602,11 @@ else
53548
53602
  SHELL_CMD=(bash -c "$CMD")
53549
53603
  fi
53550
53604
 
53551
- case "$(uname -s)" in
53552
- Darwin)
53553
- POLICY="(version 1)
53554
- (deny default)
53555
- (allow process*)
53556
- (allow signal (target self))
53557
- (allow sysctl-read)
53558
- (allow mach-lookup)
53559
- (allow file-read-metadata)
53560
- (allow network*)
53561
- (allow file-read* (subpath \"/usr\"))
53562
- (allow file-read* (subpath \"/bin\"))
53563
- (allow file-read* (subpath \"/sbin\"))
53564
- (allow file-read* (subpath \"/System\"))
53565
- (allow file-read* (subpath \"/Library\"))
53566
- (allow file-read* (subpath \"/etc\"))
53567
- (allow file-read* (subpath \"/private/etc\"))"
53568
- while IFS= read -r d; do
53569
- POLICY+="
53570
- (allow file-read* file-write* (subpath \"$d\"))"
53571
- done <<< "$EXPOSED_JSON"
53572
- exec sandbox-exec -p "$POLICY" "${"${SHELL_CMD[@]}"}"
53573
- ;;
53574
- Linux)
53575
- BWRAP_ARGS=(
53576
- --unshare-user --unshare-ipc --unshare-pid --unshare-uts
53577
- --die-with-parent
53578
- --proc /proc --dev /dev --tmpfs /tmp
53579
- --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /sbin /sbin
53580
- --ro-bind /lib /lib --ro-bind /etc /etc
53581
- )
53582
- if [ -d /lib64 ]; then BWRAP_ARGS+=(--ro-bind /lib64 /lib64); fi
53583
- while IFS= read -r d; do
53584
- BWRAP_ARGS+=(--bind "$d" "$d")
53585
- done <<< "$EXPOSED_JSON"
53586
- exec bwrap "${"${BWRAP_ARGS[@]}"}" "${"${SHELL_CMD[@]}"}"
53587
- ;;
53588
- *)
53589
- echo "clawd-ssh-jail: unsupported OS $(uname -s)" >&2
53590
- exit 1
53591
- ;;
53592
- esac
53605
+ # 去掉 OS 沙箱层(原本 macOS Seatbelt 策略 / Linux 命名空间隔离):SSH 进来即该用户
53606
+ # 权限内的 shell,直接 exec。沙箱只限制文件、拦不住网络/进程,隔离价值有限,且相关 API
53607
+ # 在 macOS 已 deprecated。授权即完全信任——sshAllowed 授权开关 + authorized_keys 即时
53608
+ # 踢行是唯一访问闸。要加回沙箱:从 git 历史(T-29 之前)恢复本段的 OS 分支即可。
53609
+ exec "${"${SHELL_CMD[@]}"}"
53593
53610
  `;
53594
53611
  function ensureJailScript(dataDir) {
53595
53612
  const binDir = import_node_path37.default.join(dataDir, "bin");
@@ -53636,6 +53653,8 @@ var SshdManager = class {
53636
53653
  stopping = false;
53637
53654
  exitHookInstalled = false;
53638
53655
  startupTimeoutMs;
53656
+ // sshd stderr 按行嗌探 pubkey 认证结果时的半行缓冲(防关键字被 chunk 边界切断)
53657
+ sniffBuf = "";
53639
53658
  get port() {
53640
53659
  return this.deps.port;
53641
53660
  }
@@ -53683,7 +53702,11 @@ var SshdManager = class {
53683
53702
  logStream.write(String(c));
53684
53703
  };
53685
53704
  proc.stdout?.on("data", tee);
53686
- proc.stderr?.on("data", tee);
53705
+ proc.stderr?.on("data", (c) => {
53706
+ const s = String(c);
53707
+ logStream.write(s);
53708
+ this.sniffSshdAuth(s);
53709
+ });
53687
53710
  proc.once("exit", () => logStream.end());
53688
53711
  const ready = await waitForSshdReady(proc, this.startupTimeoutMs);
53689
53712
  if (!ready.ok) {
@@ -53770,7 +53793,36 @@ ${tail}` : ready.error;
53770
53793
  } catch {
53771
53794
  }
53772
53795
  }
53796
+ // 把 sshd -e 输出里的 pubkey 认证结果转写一条到 contact-ssh.log,让 A 侧审计日志一处即可
53797
+ // 看到"隧道连上但 sshd 拒了 key"(否则只有 tunnel.connected + tunnel.disconnected,为什么
53798
+ // 被拒要翻另一个 sshd.log 对时间线)。按 \n 切行嗅探,半行留 sniffBuf 等下个 chunk 拼回。
53799
+ sniffSshdAuth(chunk) {
53800
+ const sshLog = this.deps.sshLog;
53801
+ if (!sshLog) return;
53802
+ this.sniffBuf += chunk;
53803
+ const parts = this.sniffBuf.split("\n");
53804
+ this.sniffBuf = parts.pop() ?? "";
53805
+ for (const line of parts) {
53806
+ if (/Accepted publickey/i.test(line)) {
53807
+ sshLog.info(
53808
+ "ssh.auth-ok",
53809
+ "sshd \u63A5\u53D7\u4E86 pubkey \u8BA4\u8BC1\uFF08\u5BF9\u7AEF key \u547D\u4E2D authorized_keys\uFF09",
53810
+ sshdAuthMeta(line)
53811
+ );
53812
+ } else if (/Failed publickey/i.test(line) || /Connection closed by (authenticating|invalid) user/i.test(line)) {
53813
+ sshLog.warn(
53814
+ "ssh.auth-rejected",
53815
+ "sshd \u62D2\u7EDD\u4E86 pubkey \u8BA4\u8BC1\uFF08\u5BF9\u7AEF key \u4E0D\u5728 authorized_keys / \u672A\u6388\u6743 / key \u672A\u62C9\u5230\uFF09",
53816
+ sshdAuthMeta(line)
53817
+ );
53818
+ }
53819
+ }
53820
+ }
53773
53821
  };
53822
+ function sshdAuthMeta(line) {
53823
+ const m2 = /port (\d+)/.exec(line);
53824
+ return m2 ? { clientPort: Number(m2[1]) } : {};
53825
+ }
53774
53826
  async function waitForSshdReady(proc, timeoutMs) {
53775
53827
  return new Promise((resolve6) => {
53776
53828
  let settled = false;
@@ -54161,6 +54213,11 @@ async function handleSshTunnelLocalUpgrade(req, socket, head, deps) {
54161
54213
  url: outboundUrl
54162
54214
  });
54163
54215
  deps.wss.handleUpgrade(req, socket, head, (clientWs) => {
54216
+ sshLog.info(
54217
+ "relay.client-connected",
54218
+ "relay CLI \u5DF2\u8FDE\u4E0A\u672C\u5730 daemon\uFF08client WS \u5347\u7EA7\u6210\u529F\uFF09\uFF0C\u5F00\u59CB\u62E8 peer",
54219
+ { peerDeviceId, peerDisplayName: contact.displayName }
54220
+ );
54164
54221
  const makeOut = deps.makeOutboundWs ?? ((u, o) => new import_websocket.default(u, o));
54165
54222
  const outbound = makeOut(outboundUrl, {
54166
54223
  headers: { Authorization: `Bearer ${contact.connectToken}` }
@@ -54932,10 +54989,12 @@ function buildSessionHandlers(deps) {
54932
54989
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
54933
54990
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
54934
54991
  const creatorDisplayName = ctx?.principal.displayName ?? "Owner";
54992
+ const creatorFeishuUnionId = ctx?.principal.feishuUnionId;
54935
54993
  const { response, broadcast } = manager.create(
54936
54994
  { ...args, tool },
54937
54995
  creatorPrincipalId,
54938
- creatorDisplayName
54996
+ creatorDisplayName,
54997
+ creatorFeishuUnionId
54939
54998
  );
54940
54999
  return { response: { type: "session:info", ...response }, broadcast };
54941
55000
  };
@@ -60239,6 +60298,8 @@ async function startDaemon(config) {
60239
60298
  // B 角色(跨设备):非空 → guest scope + creatorPrincipalId/creatorDisplayName = A。
60240
60299
  guestPrincipalId: args.guestPrincipalId,
60241
60300
  guestDisplayName: args.guestDisplayName,
60301
+ // T-31: 把 A 的飞书 union_id stamp 到 B SessionFile,供 cc prompt 身份卡 union_id 行
60302
+ guestFeishuUnionId: args.guestFeishuUnionId,
60242
60303
  // T-20: 可选 model override 透到 SessionFile.model
60243
60304
  ...args.model ? { model: args.model } : {}
60244
60305
  });
@@ -60620,7 +60681,9 @@ ${bar}
60620
60681
  port: config.sshdPort,
60621
60682
  logger,
60622
60683
  installProcessExitHandlers: true,
60623
- onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info)
60684
+ onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info),
60685
+ // sshd 的 pubkey 认证结果转写进 contact-ssh.log(补 A 侧「隧道连上但 sshd 拒了 key」盲区)
60686
+ sshLog
60624
60687
  });
60625
60688
  try {
60626
60689
  await sshdMgr.start();
@@ -39876,6 +39876,14 @@ var SessionFileSchema = external_exports.object({
39876
39876
  * 或 deps.ownerDisplayName(owner)。
39877
39877
  */
39878
39878
  creatorDisplayName: external_exports.string().min(1).optional(),
39879
+ /**
39880
+ * 创建该 session 的连接者飞书 union_id(一次性 stamp,T-31 persona prompt injection)。
39881
+ * session:create / dispatch spawnB handler 从 ctx.principal.feishuUnionId(guest 场景来自
39882
+ * connect token subOwner)或 deps.ownerFeishuUnionId(owner 场景)传入;newRunner 时读这个
39883
+ * 字段派生 SubSessionMeta.callerFeishuUnionId 注入 cc system prompt「身份卡」的 union_id 行。
39884
+ * Optional:老 SessionFile / owner 未登飞书路径缺省,缺省时 prompt 不渲染该行(向后兼容)。
39885
+ */
39886
+ creatorFeishuUnionId: external_exports.string().min(1).optional(),
39879
39887
  /**
39880
39888
  * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
39881
39889
  * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。