@clawos-dev/clawd 0.2.234 → 0.2.236

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
@@ -161,11 +161,10 @@ var init_methods = __esm({
161
161
  "contact:list",
162
162
  "contact:pin",
163
163
  "contact:remove",
164
- // ---- contact:setSshAccess / contact:sshKey:issue (SSH 反向访问,PR: contact-ssh-sandbox) ----
165
- // owner UI 授权 contact 通过 daemon 专用 sshd 反向 SSH 进本机;guest daemon 用 sshKey:issue 拉 privkey.
166
- // 详见 protocol/src/contact-ssh.ts.
164
+ // ---- contact:setSshAccess (SSH 反向访问,PR: contact-ssh-sandbox) ----
165
+ // owner UI 授权 contact 通过 daemon 专用 sshd 反向 SSH 进本机;key 分发走建联握手
166
+ // pubkey 互换(spec 2026-07-15,无签发 RPC)。详见 protocol/src/contact-ssh.ts.
167
167
  "contact:setSshAccess",
168
- "contact:sshKey:issue",
169
168
  // ---- visitor:* (web 访客 · persona web 分享,spec 2026-06-24-persona-web-share-design) ----
170
169
  // owner-only:列出登录访问过本机 public persona 的 web 访客(零安装、飞书登录、daemon 自签
171
170
  // visitor token)。存储 ~/.clawd/visitors.json(VisitorStore),登录(exchange)即 upsert;
@@ -745,8 +744,8 @@ var init_parseUtil = __esm({
745
744
  init_errors2();
746
745
  init_en();
747
746
  makeIssue = (params) => {
748
- const { data, path: path77, errorMaps, issueData } = params;
749
- const fullPath = [...path77, ...issueData.path || []];
747
+ const { data, path: path76, errorMaps, issueData } = params;
748
+ const fullPath = [...path76, ...issueData.path || []];
750
749
  const fullIssue = {
751
750
  ...issueData,
752
751
  path: fullPath
@@ -1057,11 +1056,11 @@ var init_types = __esm({
1057
1056
  init_parseUtil();
1058
1057
  init_util();
1059
1058
  ParseInputLazyPath = class {
1060
- constructor(parent, value, path77, key) {
1059
+ constructor(parent, value, path76, key) {
1061
1060
  this._cachedPath = [];
1062
1061
  this.parent = parent;
1063
1062
  this.data = value;
1064
- this._path = path77;
1063
+ this._path = path76;
1065
1064
  this._key = key;
1066
1065
  }
1067
1066
  get path() {
@@ -5326,7 +5325,14 @@ var init_schemas = __esm({
5326
5325
  * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5327
5326
  * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5328
5327
  */
5329
- selfUrl: external_exports.string().min(1).optional()
5328
+ selfUrl: external_exports.string().min(1).optional(),
5329
+ /**
5330
+ * ssh identity 互换(spec 2026-07-15):连接方自报本机 SSH identity pubkey
5331
+ * (一行 `ssh-ed25519 AAAA... comment`)。跟 selfUrl 同理不是身份——pubkey 是公开
5332
+ * 材料,报假只会让自己连不上(拨号还要对应 privkey 签名验证),被连方 auto-reverse
5333
+ * 落 contact.sshPublicKey。缺省 = 老客户端 / identity 未生成。
5334
+ */
5335
+ selfSshPublicKey: external_exports.string().min(1).optional()
5330
5336
  });
5331
5337
  AuthOkFrameSchema = external_exports.object({
5332
5338
  type: external_exports.literal("auth:ok"),
@@ -5649,7 +5655,12 @@ var init_capability = __esm({
5649
5655
  id: external_exports.string().min(1),
5650
5656
  displayName: external_exports.string()
5651
5657
  }).strict()
5652
- )
5658
+ ),
5659
+ /**
5660
+ * 被连方设备 SSH identity pubkey(spec 2026-07-15 ssh identity 互换):
5661
+ * 连接方 device:connect 读走落 contact.sshPublicKey。identity 未生成时省略。
5662
+ */
5663
+ sshPublicKey: external_exports.string().min(1).optional()
5653
5664
  }).strict();
5654
5665
  PERSONAL_CAP_GRANTS = Object.freeze([
5655
5666
  Object.freeze({
@@ -5745,8 +5756,18 @@ var init_contact = __esm({
5745
5756
  */
5746
5757
  sshAllowed: external_exports.boolean().default(false),
5747
5758
  /**
5748
- * 授权访问的目录白名单(绝对路径)。空数组 + sshAllowed=true 意味 SSH 通道能拨到但无处可读,
5749
- * jail 会拒绝 shell 启动。由 clawd-ssh-jail 运行时读,生成 sbpl / bwrap policy 白名单。
5759
+ * 对端设备 SSH identity pubkey(一行 `ssh-ed25519 AAAA... comment`,spec 2026-07-15)。
5760
+ * 建联握手双向交换写入(连入方 auth 帧自报 / 被连方 whoami:ok 下发),
5761
+ * setSshAccess=true 时由 rebuildAuthorizedKeys 写进 authorized_keys。
5762
+ * pubkey 公开可自报无伪造风险——拨号还要 privkey 签名验证。
5763
+ * 缺省 = 老 contact / 对方老版本未重连,授权时写不出 authorized_keys 行。
5764
+ */
5765
+ sshPublicKey: external_exports.string().optional(),
5766
+ /**
5767
+ * @deprecated T-29 去沙箱后废弃。原为授权访问的目录白名单(clawd-ssh-jail 生成 sbpl/bwrap
5768
+ * policy 用);去掉 OS 沙箱后不再有目录级限制,SSH 授权即 on/off(见 sshAllowed)。
5769
+ * 字段仅为兼容老 contacts.json 保留(default [] 保证 strict parse 不炸),不再有代码读写它,
5770
+ * 也不再进 setSshAccess RPC / ContactSettingsDrawer / jail。勿新增消费方。
5750
5771
  */
5751
5772
  exposedDirs: external_exports.array(external_exports.string()).default([])
5752
5773
  }).strict();
@@ -5788,35 +5809,24 @@ var init_contact = __esm({
5788
5809
  });
5789
5810
 
5790
5811
  // ../protocol/src/contact-ssh.ts
5791
- var ContactSetSshAccessArgsSchema, ContactSetSshAccessOkSchema, ContactSshKeyIssueArgsSchema, ContactSshKeyIssueOkSchema, ContactSshAccessUpdatedFrameSchema;
5812
+ var ContactSetSshAccessArgsSchema, ContactSetSshAccessOkSchema, ContactSshAccessUpdatedFrameSchema;
5792
5813
  var init_contact_ssh = __esm({
5793
5814
  "../protocol/src/contact-ssh.ts"() {
5794
5815
  "use strict";
5795
5816
  init_zod();
5796
5817
  ContactSetSshAccessArgsSchema = external_exports.object({
5797
5818
  deviceId: external_exports.string().min(1),
5798
- sshAllowed: external_exports.boolean(),
5799
- exposedDirs: external_exports.array(external_exports.string())
5819
+ sshAllowed: external_exports.boolean()
5800
5820
  }).strict();
5801
5821
  ContactSetSshAccessOkSchema = external_exports.object({
5802
5822
  type: external_exports.literal("contact:setSshAccess:ok"),
5803
5823
  deviceId: external_exports.string().min(1),
5804
- sshAllowed: external_exports.boolean(),
5805
- exposedDirs: external_exports.array(external_exports.string())
5806
- }).strict();
5807
- ContactSshKeyIssueArgsSchema = external_exports.object({
5808
- deviceId: external_exports.string().min(1)
5809
- }).strict();
5810
- ContactSshKeyIssueOkSchema = external_exports.object({
5811
- type: external_exports.literal("contact:sshKey:issue:ok"),
5812
- privateKeyPem: external_exports.string().min(1),
5813
- publicKeyLine: external_exports.string().min(1)
5824
+ sshAllowed: external_exports.boolean()
5814
5825
  }).strict();
5815
5826
  ContactSshAccessUpdatedFrameSchema = external_exports.object({
5816
5827
  type: external_exports.literal("contact:ssh-access-updated"),
5817
5828
  deviceId: external_exports.string().min(1),
5818
- sshAllowed: external_exports.boolean(),
5819
- exposedDirs: external_exports.array(external_exports.string())
5829
+ sshAllowed: external_exports.boolean()
5820
5830
  }).strict();
5821
5831
  }
5822
5832
  });
@@ -6507,8 +6517,8 @@ var require_req = __commonJS({
6507
6517
  if (req.originalUrl) {
6508
6518
  _req.url = req.originalUrl;
6509
6519
  } else {
6510
- const path77 = req.path;
6511
- _req.url = typeof path77 === "string" ? path77 : req.url ? req.url.path || req.url : void 0;
6520
+ const path76 = req.path;
6521
+ _req.url = typeof path76 === "string" ? path76 : req.url ? req.url.path || req.url : void 0;
6512
6522
  }
6513
6523
  if (req.query) {
6514
6524
  _req.query = req.query;
@@ -6673,14 +6683,14 @@ var require_redact = __commonJS({
6673
6683
  }
6674
6684
  return obj;
6675
6685
  }
6676
- function parsePath(path77) {
6686
+ function parsePath(path76) {
6677
6687
  const parts = [];
6678
6688
  let current = "";
6679
6689
  let inBrackets = false;
6680
6690
  let inQuotes = false;
6681
6691
  let quoteChar = "";
6682
- for (let i = 0; i < path77.length; i++) {
6683
- const char = path77[i];
6692
+ for (let i = 0; i < path76.length; i++) {
6693
+ const char = path76[i];
6684
6694
  if (!inBrackets && char === ".") {
6685
6695
  if (current) {
6686
6696
  parts.push(current);
@@ -6811,10 +6821,10 @@ var require_redact = __commonJS({
6811
6821
  return current;
6812
6822
  }
6813
6823
  function redactPaths(obj, paths, censor, remove = false) {
6814
- for (const path77 of paths) {
6815
- const parts = parsePath(path77);
6824
+ for (const path76 of paths) {
6825
+ const parts = parsePath(path76);
6816
6826
  if (parts.includes("*")) {
6817
- redactWildcardPath(obj, parts, censor, path77, remove);
6827
+ redactWildcardPath(obj, parts, censor, path76, remove);
6818
6828
  } else {
6819
6829
  if (remove) {
6820
6830
  removeKey(obj, parts);
@@ -6899,8 +6909,8 @@ var require_redact = __commonJS({
6899
6909
  }
6900
6910
  } else {
6901
6911
  if (afterWildcard.includes("*")) {
6902
- const wrappedCensor = typeof censor === "function" ? (value, path77) => {
6903
- const fullPath = [...pathArray.slice(0, pathLength), ...path77];
6912
+ const wrappedCensor = typeof censor === "function" ? (value, path76) => {
6913
+ const fullPath = [...pathArray.slice(0, pathLength), ...path76];
6904
6914
  return censor(value, fullPath);
6905
6915
  } : censor;
6906
6916
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6935,8 +6945,8 @@ var require_redact = __commonJS({
6935
6945
  return null;
6936
6946
  }
6937
6947
  const pathStructure = /* @__PURE__ */ new Map();
6938
- for (const path77 of pathsToClone) {
6939
- const parts = parsePath(path77);
6948
+ for (const path76 of pathsToClone) {
6949
+ const parts = parsePath(path76);
6940
6950
  let current = pathStructure;
6941
6951
  for (let i = 0; i < parts.length; i++) {
6942
6952
  const part = parts[i];
@@ -6988,24 +6998,24 @@ var require_redact = __commonJS({
6988
6998
  }
6989
6999
  return cloneSelectively(obj, pathStructure);
6990
7000
  }
6991
- function validatePath(path77) {
6992
- if (typeof path77 !== "string") {
7001
+ function validatePath(path76) {
7002
+ if (typeof path76 !== "string") {
6993
7003
  throw new Error("Paths must be (non-empty) strings");
6994
7004
  }
6995
- if (path77 === "") {
7005
+ if (path76 === "") {
6996
7006
  throw new Error("Invalid redaction path ()");
6997
7007
  }
6998
- if (path77.includes("..")) {
6999
- throw new Error(`Invalid redaction path (${path77})`);
7008
+ if (path76.includes("..")) {
7009
+ throw new Error(`Invalid redaction path (${path76})`);
7000
7010
  }
7001
- if (path77.includes(",")) {
7002
- throw new Error(`Invalid redaction path (${path77})`);
7011
+ if (path76.includes(",")) {
7012
+ throw new Error(`Invalid redaction path (${path76})`);
7003
7013
  }
7004
7014
  let bracketCount = 0;
7005
7015
  let inQuotes = false;
7006
7016
  let quoteChar = "";
7007
- for (let i = 0; i < path77.length; i++) {
7008
- const char = path77[i];
7017
+ for (let i = 0; i < path76.length; i++) {
7018
+ const char = path76[i];
7009
7019
  if ((char === '"' || char === "'") && bracketCount > 0) {
7010
7020
  if (!inQuotes) {
7011
7021
  inQuotes = true;
@@ -7019,20 +7029,20 @@ var require_redact = __commonJS({
7019
7029
  } else if (char === "]" && !inQuotes) {
7020
7030
  bracketCount--;
7021
7031
  if (bracketCount < 0) {
7022
- throw new Error(`Invalid redaction path (${path77})`);
7032
+ throw new Error(`Invalid redaction path (${path76})`);
7023
7033
  }
7024
7034
  }
7025
7035
  }
7026
7036
  if (bracketCount !== 0) {
7027
- throw new Error(`Invalid redaction path (${path77})`);
7037
+ throw new Error(`Invalid redaction path (${path76})`);
7028
7038
  }
7029
7039
  }
7030
7040
  function validatePaths(paths) {
7031
7041
  if (!Array.isArray(paths)) {
7032
7042
  throw new TypeError("paths must be an array");
7033
7043
  }
7034
- for (const path77 of paths) {
7035
- validatePath(path77);
7044
+ for (const path76 of paths) {
7045
+ validatePath(path76);
7036
7046
  }
7037
7047
  }
7038
7048
  function slowRedact(options = {}) {
@@ -7200,8 +7210,8 @@ var require_redaction = __commonJS({
7200
7210
  if (shape[k2] === null) {
7201
7211
  o[k2] = (value) => topCensor(value, [k2]);
7202
7212
  } else {
7203
- const wrappedCensor = typeof censor === "function" ? (value, path77) => {
7204
- return censor(value, [k2, ...path77]);
7213
+ const wrappedCensor = typeof censor === "function" ? (value, path76) => {
7214
+ return censor(value, [k2, ...path76]);
7205
7215
  } : censor;
7206
7216
  o[k2] = Redact({
7207
7217
  paths: shape[k2],
@@ -7419,10 +7429,10 @@ var require_atomic_sleep = __commonJS({
7419
7429
  var require_sonic_boom = __commonJS({
7420
7430
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7421
7431
  "use strict";
7422
- var fs71 = require("fs");
7432
+ var fs70 = require("fs");
7423
7433
  var EventEmitter3 = require("events");
7424
7434
  var inherits = require("util").inherits;
7425
- var path77 = require("path");
7435
+ var path76 = require("path");
7426
7436
  var sleep2 = require_atomic_sleep();
7427
7437
  var assert = require("assert");
7428
7438
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7476,20 +7486,20 @@ var require_sonic_boom = __commonJS({
7476
7486
  const mode = sonic.mode;
7477
7487
  if (sonic.sync) {
7478
7488
  try {
7479
- if (sonic.mkdir) fs71.mkdirSync(path77.dirname(file), { recursive: true });
7480
- const fd = fs71.openSync(file, flags, mode);
7489
+ if (sonic.mkdir) fs70.mkdirSync(path76.dirname(file), { recursive: true });
7490
+ const fd = fs70.openSync(file, flags, mode);
7481
7491
  fileOpened(null, fd);
7482
7492
  } catch (err) {
7483
7493
  fileOpened(err);
7484
7494
  throw err;
7485
7495
  }
7486
7496
  } else if (sonic.mkdir) {
7487
- fs71.mkdir(path77.dirname(file), { recursive: true }, (err) => {
7497
+ fs70.mkdir(path76.dirname(file), { recursive: true }, (err) => {
7488
7498
  if (err) return fileOpened(err);
7489
- fs71.open(file, flags, mode, fileOpened);
7499
+ fs70.open(file, flags, mode, fileOpened);
7490
7500
  });
7491
7501
  } else {
7492
- fs71.open(file, flags, mode, fileOpened);
7502
+ fs70.open(file, flags, mode, fileOpened);
7493
7503
  }
7494
7504
  }
7495
7505
  function SonicBoom(opts) {
@@ -7530,8 +7540,8 @@ var require_sonic_boom = __commonJS({
7530
7540
  this.flush = flushBuffer;
7531
7541
  this.flushSync = flushBufferSync;
7532
7542
  this._actualWrite = actualWriteBuffer;
7533
- fsWriteSync = () => fs71.writeSync(this.fd, this._writingBuf);
7534
- fsWrite = () => fs71.write(this.fd, this._writingBuf, this.release);
7543
+ fsWriteSync = () => fs70.writeSync(this.fd, this._writingBuf);
7544
+ fsWrite = () => fs70.write(this.fd, this._writingBuf, this.release);
7535
7545
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7536
7546
  this._writingBuf = "";
7537
7547
  this.write = write;
@@ -7540,15 +7550,15 @@ var require_sonic_boom = __commonJS({
7540
7550
  this._actualWrite = actualWrite;
7541
7551
  fsWriteSync = () => {
7542
7552
  if (Buffer.isBuffer(this._writingBuf)) {
7543
- return fs71.writeSync(this.fd, this._writingBuf);
7553
+ return fs70.writeSync(this.fd, this._writingBuf);
7544
7554
  }
7545
- return fs71.writeSync(this.fd, this._writingBuf, "utf8");
7555
+ return fs70.writeSync(this.fd, this._writingBuf, "utf8");
7546
7556
  };
7547
7557
  fsWrite = () => {
7548
7558
  if (Buffer.isBuffer(this._writingBuf)) {
7549
- return fs71.write(this.fd, this._writingBuf, this.release);
7559
+ return fs70.write(this.fd, this._writingBuf, this.release);
7550
7560
  }
7551
- return fs71.write(this.fd, this._writingBuf, "utf8", this.release);
7561
+ return fs70.write(this.fd, this._writingBuf, "utf8", this.release);
7552
7562
  };
7553
7563
  } else {
7554
7564
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7605,7 +7615,7 @@ var require_sonic_boom = __commonJS({
7605
7615
  }
7606
7616
  }
7607
7617
  if (this._fsync) {
7608
- fs71.fsyncSync(this.fd);
7618
+ fs70.fsyncSync(this.fd);
7609
7619
  }
7610
7620
  const len = this._len;
7611
7621
  if (this._reopening) {
@@ -7719,7 +7729,7 @@ var require_sonic_boom = __commonJS({
7719
7729
  const onDrain = () => {
7720
7730
  if (!this._fsync) {
7721
7731
  try {
7722
- fs71.fsync(this.fd, (err) => {
7732
+ fs70.fsync(this.fd, (err) => {
7723
7733
  this._flushPending = false;
7724
7734
  cb(err);
7725
7735
  });
@@ -7821,7 +7831,7 @@ var require_sonic_boom = __commonJS({
7821
7831
  const fd = this.fd;
7822
7832
  this.once("ready", () => {
7823
7833
  if (fd !== this.fd) {
7824
- fs71.close(fd, (err) => {
7834
+ fs70.close(fd, (err) => {
7825
7835
  if (err) {
7826
7836
  return this.emit("error", err);
7827
7837
  }
@@ -7870,7 +7880,7 @@ var require_sonic_boom = __commonJS({
7870
7880
  buf = this._bufs[0];
7871
7881
  }
7872
7882
  try {
7873
- const n = Buffer.isBuffer(buf) ? fs71.writeSync(this.fd, buf) : fs71.writeSync(this.fd, buf, "utf8");
7883
+ const n = Buffer.isBuffer(buf) ? fs70.writeSync(this.fd, buf) : fs70.writeSync(this.fd, buf, "utf8");
7874
7884
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7875
7885
  buf = releasedBufObj.writingBuf;
7876
7886
  this._len = releasedBufObj.len;
@@ -7886,7 +7896,7 @@ var require_sonic_boom = __commonJS({
7886
7896
  }
7887
7897
  }
7888
7898
  try {
7889
- fs71.fsyncSync(this.fd);
7899
+ fs70.fsyncSync(this.fd);
7890
7900
  } catch {
7891
7901
  }
7892
7902
  }
@@ -7907,7 +7917,7 @@ var require_sonic_boom = __commonJS({
7907
7917
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7908
7918
  }
7909
7919
  try {
7910
- const n = fs71.writeSync(this.fd, buf);
7920
+ const n = fs70.writeSync(this.fd, buf);
7911
7921
  buf = buf.subarray(n);
7912
7922
  this._len = Math.max(this._len - n, 0);
7913
7923
  if (buf.length <= 0) {
@@ -7935,13 +7945,13 @@ var require_sonic_boom = __commonJS({
7935
7945
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7936
7946
  if (this.sync) {
7937
7947
  try {
7938
- const written = Buffer.isBuffer(this._writingBuf) ? fs71.writeSync(this.fd, this._writingBuf) : fs71.writeSync(this.fd, this._writingBuf, "utf8");
7948
+ const written = Buffer.isBuffer(this._writingBuf) ? fs70.writeSync(this.fd, this._writingBuf) : fs70.writeSync(this.fd, this._writingBuf, "utf8");
7939
7949
  release(null, written);
7940
7950
  } catch (err) {
7941
7951
  release(err);
7942
7952
  }
7943
7953
  } else {
7944
- fs71.write(this.fd, this._writingBuf, release);
7954
+ fs70.write(this.fd, this._writingBuf, release);
7945
7955
  }
7946
7956
  }
7947
7957
  function actualWriteBuffer() {
@@ -7950,7 +7960,7 @@ var require_sonic_boom = __commonJS({
7950
7960
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7951
7961
  if (this.sync) {
7952
7962
  try {
7953
- const written = fs71.writeSync(this.fd, this._writingBuf);
7963
+ const written = fs70.writeSync(this.fd, this._writingBuf);
7954
7964
  release(null, written);
7955
7965
  } catch (err) {
7956
7966
  release(err);
@@ -7959,7 +7969,7 @@ var require_sonic_boom = __commonJS({
7959
7969
  if (kCopyBuffer) {
7960
7970
  this._writingBuf = Buffer.from(this._writingBuf);
7961
7971
  }
7962
- fs71.write(this.fd, this._writingBuf, release);
7972
+ fs70.write(this.fd, this._writingBuf, release);
7963
7973
  }
7964
7974
  }
7965
7975
  function actualClose(sonic) {
@@ -7975,12 +7985,12 @@ var require_sonic_boom = __commonJS({
7975
7985
  sonic._lens = [];
7976
7986
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7977
7987
  try {
7978
- fs71.fsync(sonic.fd, closeWrapped);
7988
+ fs70.fsync(sonic.fd, closeWrapped);
7979
7989
  } catch {
7980
7990
  }
7981
7991
  function closeWrapped() {
7982
7992
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7983
- fs71.close(sonic.fd, done);
7993
+ fs70.close(sonic.fd, done);
7984
7994
  } else {
7985
7995
  done();
7986
7996
  }
@@ -11115,11 +11125,11 @@ var init_lib = __esm({
11115
11125
  }
11116
11126
  }
11117
11127
  },
11118
- addToPath: function addToPath(path77, added, removed, oldPosInc, options) {
11119
- var last = path77.lastComponent;
11128
+ addToPath: function addToPath(path76, added, removed, oldPosInc, options) {
11129
+ var last = path76.lastComponent;
11120
11130
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
11121
11131
  return {
11122
- oldPos: path77.oldPos + oldPosInc,
11132
+ oldPos: path76.oldPos + oldPosInc,
11123
11133
  lastComponent: {
11124
11134
  count: last.count + 1,
11125
11135
  added,
@@ -11129,7 +11139,7 @@ var init_lib = __esm({
11129
11139
  };
11130
11140
  } else {
11131
11141
  return {
11132
- oldPos: path77.oldPos + oldPosInc,
11142
+ oldPos: path76.oldPos + oldPosInc,
11133
11143
  lastComponent: {
11134
11144
  count: 1,
11135
11145
  added,
@@ -11634,10 +11644,10 @@ function attachmentToHistoryMessage(o, ts) {
11634
11644
  const memories = raw.map((m2) => {
11635
11645
  if (!m2 || typeof m2 !== "object") return null;
11636
11646
  const rec3 = m2;
11637
- const path77 = typeof rec3.path === "string" ? rec3.path : null;
11647
+ const path76 = typeof rec3.path === "string" ? rec3.path : null;
11638
11648
  const content = typeof rec3.content === "string" ? rec3.content : null;
11639
- if (!path77 || content == null) return null;
11640
- const entry = { path: path77, content };
11649
+ if (!path76 || content == null) return null;
11650
+ const entry = { path: path76, content };
11641
11651
  if (typeof rec3.mtimeMs === "number") entry.mtimeMs = rec3.mtimeMs;
11642
11652
  return entry;
11643
11653
  }).filter((m2) => m2 !== null);
@@ -12440,10 +12450,10 @@ function parseAttachment(obj) {
12440
12450
  const memories = raw.map((m2) => {
12441
12451
  if (!m2 || typeof m2 !== "object") return null;
12442
12452
  const rec3 = m2;
12443
- const path77 = typeof rec3.path === "string" ? rec3.path : null;
12453
+ const path76 = typeof rec3.path === "string" ? rec3.path : null;
12444
12454
  const content = typeof rec3.content === "string" ? rec3.content : null;
12445
- if (!path77 || content == null) return null;
12446
- const out = { path: path77, content };
12455
+ if (!path76 || content == null) return null;
12456
+ const out = { path: path76, content };
12447
12457
  if (typeof rec3.mtimeMs === "number") out.mtimeMs = rec3.mtimeMs;
12448
12458
  return out;
12449
12459
  }).filter((m2) => m2 !== null);
@@ -33429,8 +33439,8 @@ var require_utils = __commonJS({
33429
33439
  var result = transform[inputType][outputType](input);
33430
33440
  return result;
33431
33441
  };
33432
- exports2.resolve = function(path77) {
33433
- var parts = path77.split("/");
33442
+ exports2.resolve = function(path76) {
33443
+ var parts = path76.split("/");
33434
33444
  var result = [];
33435
33445
  for (var index = 0; index < parts.length; index++) {
33436
33446
  var part = parts[index];
@@ -39283,18 +39293,18 @@ var require_object = __commonJS({
39283
39293
  var object = new ZipObject(name, zipObjectContent, o);
39284
39294
  this.files[name] = object;
39285
39295
  };
39286
- var parentFolder = function(path77) {
39287
- if (path77.slice(-1) === "/") {
39288
- path77 = path77.substring(0, path77.length - 1);
39296
+ var parentFolder = function(path76) {
39297
+ if (path76.slice(-1) === "/") {
39298
+ path76 = path76.substring(0, path76.length - 1);
39289
39299
  }
39290
- var lastSlash = path77.lastIndexOf("/");
39291
- return lastSlash > 0 ? path77.substring(0, lastSlash) : "";
39300
+ var lastSlash = path76.lastIndexOf("/");
39301
+ return lastSlash > 0 ? path76.substring(0, lastSlash) : "";
39292
39302
  };
39293
- var forceTrailingSlash = function(path77) {
39294
- if (path77.slice(-1) !== "/") {
39295
- path77 += "/";
39303
+ var forceTrailingSlash = function(path76) {
39304
+ if (path76.slice(-1) !== "/") {
39305
+ path76 += "/";
39296
39306
  }
39297
- return path77;
39307
+ return path76;
39298
39308
  };
39299
39309
  var folderAdd = function(name, createFolders) {
39300
39310
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -40296,7 +40306,7 @@ var require_lib3 = __commonJS({
40296
40306
  // src/run-case/recorder.ts
40297
40307
  function startRunCaseRecorder(opts) {
40298
40308
  const now = opts.now ?? Date.now;
40299
- const dir = import_node_path65.default.dirname(opts.recordPath);
40309
+ const dir = import_node_path64.default.dirname(opts.recordPath);
40300
40310
  let stream = null;
40301
40311
  let closing = false;
40302
40312
  let closedSettled = false;
@@ -40310,8 +40320,8 @@ function startRunCaseRecorder(opts) {
40310
40320
  });
40311
40321
  const ensureStream = () => {
40312
40322
  if (stream) return stream;
40313
- import_node_fs53.default.mkdirSync(dir, { recursive: true });
40314
- stream = import_node_fs53.default.createWriteStream(opts.recordPath, { flags: "a" });
40323
+ import_node_fs52.default.mkdirSync(dir, { recursive: true });
40324
+ stream = import_node_fs52.default.createWriteStream(opts.recordPath, { flags: "a" });
40315
40325
  stream.on("close", () => closedResolve());
40316
40326
  return stream;
40317
40327
  };
@@ -40336,12 +40346,12 @@ function startRunCaseRecorder(opts) {
40336
40346
  };
40337
40347
  return { tap, close, closed };
40338
40348
  }
40339
- var import_node_fs53, import_node_path65;
40349
+ var import_node_fs52, import_node_path64;
40340
40350
  var init_recorder = __esm({
40341
40351
  "src/run-case/recorder.ts"() {
40342
40352
  "use strict";
40343
- import_node_fs53 = __toESM(require("fs"), 1);
40344
- import_node_path65 = __toESM(require("path"), 1);
40353
+ import_node_fs52 = __toESM(require("fs"), 1);
40354
+ import_node_path64 = __toESM(require("path"), 1);
40345
40355
  }
40346
40356
  });
40347
40357
 
@@ -40384,7 +40394,7 @@ var init_wire = __esm({
40384
40394
  // src/run-case/controller.ts
40385
40395
  async function runController(opts) {
40386
40396
  const now = opts.now ?? Date.now;
40387
- const cwd = opts.cwd ?? (0, import_node_fs54.mkdtempSync)(import_node_path66.default.join(import_node_os22.default.tmpdir(), "clawd-runcase-"));
40397
+ const cwd = opts.cwd ?? (0, import_node_fs53.mkdtempSync)(import_node_path65.default.join(import_node_os22.default.tmpdir(), "clawd-runcase-"));
40388
40398
  const ownsCwd = opts.cwd === void 0;
40389
40399
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
40390
40400
  const spawnCtx = { cwd };
@@ -40545,19 +40555,19 @@ async function runController(opts) {
40545
40555
  if (sigintHandler) process.off("SIGINT", sigintHandler);
40546
40556
  if (ownsCwd) {
40547
40557
  try {
40548
- (0, import_node_fs54.rmSync)(cwd, { recursive: true, force: true });
40558
+ (0, import_node_fs53.rmSync)(cwd, { recursive: true, force: true });
40549
40559
  } catch {
40550
40560
  }
40551
40561
  }
40552
40562
  return exitCode ?? 0;
40553
40563
  }
40554
- var import_node_fs54, import_node_os22, import_node_path66;
40564
+ var import_node_fs53, import_node_os22, import_node_path65;
40555
40565
  var init_controller = __esm({
40556
40566
  "src/run-case/controller.ts"() {
40557
40567
  "use strict";
40558
- import_node_fs54 = require("fs");
40568
+ import_node_fs53 = require("fs");
40559
40569
  import_node_os22 = __toESM(require("os"), 1);
40560
- import_node_path66 = __toESM(require("path"), 1);
40570
+ import_node_path65 = __toESM(require("path"), 1);
40561
40571
  init_claude();
40562
40572
  init_stdout_splitter();
40563
40573
  init_permission_stdio();
@@ -40761,7 +40771,7 @@ Options:
40761
40771
 
40762
40772
  Example:
40763
40773
  ssh -o ProxyCommand='~/.clawd/bin/clawd ssh-relay <peer-device-id>' \\
40764
- -i ~/.clawd/contact-ssh-keys/<peer-device-id>.ed25519 \\
40774
+ -i ~/.clawd/ssh-identity/id_ed25519 \\
40765
40775
  $USER@127.0.0.1
40766
40776
  `;
40767
40777
  }
@@ -40951,8 +40961,8 @@ Env (advanced):
40951
40961
  `;
40952
40962
 
40953
40963
  // src/index.ts
40954
- var import_node_path64 = __toESM(require("path"), 1);
40955
- var import_node_fs52 = __toESM(require("fs"), 1);
40964
+ var import_node_path63 = __toESM(require("path"), 1);
40965
+ var import_node_fs51 = __toESM(require("fs"), 1);
40956
40966
  var import_node_os21 = __toESM(require("os"), 1);
40957
40967
 
40958
40968
  // ../node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/stringify.js
@@ -41703,7 +41713,7 @@ function composeGuestSandbox(base, userWorkDir, spawnCwd) {
41703
41713
  fsv.allowRead = unionArr(fsv.allowRead, [userWorkDir]);
41704
41714
  fsv.allowWrite = unionArr(fsv.allowWrite, [userWorkDir]);
41705
41715
  fsv.allowRead = unionArr(fsv.allowRead, [
41706
- "~/.clawd/contact-ssh-keys",
41716
+ "~/.clawd/ssh-identity",
41707
41717
  "~/.clawd/bin",
41708
41718
  "~/Library/Application Support/@clawos/clawd-desktop/ota-bundles",
41709
41719
  "~/.clawd/config.json",
@@ -41979,28 +41989,27 @@ deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731
41979
41989
  // src/sshd/contact-ssh-prompt.ts
41980
41990
  var CONTACT_SSH_SYSTEM_PROMPT_HINT = `## \u8DE8\u8BBE\u5907\u6587\u4EF6\u8BBF\u95EE\uFF08Contact SSH\uFF09
41981
41991
 
41982
- \u7528\u6237\u53EF\u80FD\u6709\u591A\u53F0 clawd \u8BBE\u5907\u4E92\u76F8\u6388\u6743\u3002\u4F60\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u8BBF\u95EE\u5DF2\u6388\u6743\u5BF9\u7AEF\uFF08\u4E0B\u79F0 A\uFF09\u7684\u65B9\u6CD5\uFF1A
41992
+ \u7528\u6237\u53EF\u80FD\u6709\u591A\u53F0 clawd \u8BBE\u5907\u4E92\u76F8\u6388\u6743\u3002\u4F60\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u8BBF\u95EE\u5DF2\u6388\u6743\u5BF9\u7AEF\uFF08\u4E0B\u79F0 A\uFF0CdeviceId \u7531\u7528\u6237/\u4EFB\u52A1\u4E0A\u4E0B\u6587\u7ED9\u51FA\uFF09\u7684\u65B9\u6CD5\uFF1A
41983
41993
 
41984
- 1. \u5217\u51FA\u53EF\u62E8\u7684\u5BF9\u7AEF deviceId\uFF1A\`ls ~/.clawd/contact-ssh-keys/\`
41985
- \uFF08\u6BCF\u4E2A \`<deviceId>.ed25519\` \u5C31\u662F\u4E00\u628A privkey\uFF0C\u53BB\u6389\u540E\u7F00\u5C31\u662F deviceId\uFF09
41986
- 2. \u7528 SSH \u62E8\u53F7\uFF08\u628A \`<A>\` \u6362\u6210\u5B9E\u9645 deviceId\uFF09\uFF1A
41987
- \`\`\`bash
41988
- ssh -o ProxyCommand='~/.clawd/bin/clawd ssh-relay <A>' \\
41989
- -i ~/.clawd/contact-ssh-keys/<A>.ed25519 \\
41990
- -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \\
41991
- $USER@127.0.0.1 <command>
41992
- # \u4F8B\u5982\uFF1A... $USER@127.0.0.1 cat /Users/xxx/some/file.md
41993
- # ... $USER@127.0.0.1 ls -la /Users/xxx/proj
41994
- # \u8BF4\u660E\uFF1A\`~/.clawd/bin/clawd\` \u662F daemon \u542F\u52A8\u65F6 seed \u7684 shim\uFF0C\u786C\u7F16\u7801\u5F53\u524D daemon \u7684
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
41996
- \`\`\`
41994
+ \u7528 SSH \u62E8\u53F7\uFF08\u628A \`<A>\` \u6362\u6210\u5BF9\u7AEF deviceId\uFF09\uFF1A
41995
+ \`\`\`bash
41996
+ ssh -o ProxyCommand='~/.clawd/bin/clawd ssh-relay <A>' \\
41997
+ -i ~/.clawd/ssh-identity/id_ed25519 \\
41998
+ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \\
41999
+ $USER@127.0.0.1 <command>
42000
+ # \u4F8B\u5982\uFF1A... $USER@127.0.0.1 cat /Users/xxx/some/file.md
42001
+ # ... $USER@127.0.0.1 ls -la /Users/xxx/proj
42002
+ # \u8BF4\u660E\uFF1A\`-i\` \u7528\u672C\u8BBE\u5907\u552F\u4E00\u7684 SSH identity key\uFF08\u6240\u6709\u5BF9\u7AEF\u590D\u7528\u4E00\u628A\uFF09\u3002
42003
+ # \`~/.clawd/bin/clawd\` \u662F daemon \u542F\u52A8\u65F6 seed \u7684 shim\uFF0C\u786C\u7F16\u7801\u5F53\u524D daemon \u7684
42004
+ # 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
42005
+ \`\`\`
41997
42006
 
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
42007
+ **\u4F60\u80FD\u8BBF\u95EE\u4EC0\u4E48**\uFF1A**\u6CA1\u6709\u76EE\u5F55\u7EA7\u9650\u5236**\u2014\u2014\u4F60\u4EE5 A \u673A\u5668\u4E0A\u8BE5 SSH \u7528\u6237\u7684\u6743\u9650\u8BBF\u95EE\u6587\u4EF6\uFF08\u7B49\u540C\u4E8E A \u7ED9\u4F60\u5F00\u4E86\u4E00\u4E2A shell\uFF09\u3002\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
41999
42008
 
42000
42009
  **\u6CE8\u610F**\uFF1A
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
42010
+ - \u6388\u6743\u662F\u5B9E\u65F6\u7684\uFF1AA \u64A4\u9500 sshAllowed \u540E\u4F60\u4E0B\u4E00\u6B21\u62E8\u53F7\u7ACB\u5373\u5931\u6548\uFF08authorized_keys \u7ACB\u5373\u5237\uFF09
42011
+ - \u62E8\u53F7\u88AB\u62D2\uFF08Permission denied\uFF09\u901A\u5E38\u662F A \u5C1A\u672A\u6388\u6743\u672C\u8BBE\u5907\uFF0C\u6216\u53CC\u65B9\u8001\u7248\u672C\u5EFA\u8054\u3001A \u7684 contact \u91CC\u6CA1\u6709\u672C\u673A pubkey\uFF08\u4EFB\u4E00\u65B9\u91CD\u65B0 device:connect \u4E00\u6B21\u5373\u8865\uFF09\uFF0C\u5982\u5B9E\u544A\u8BC9\u7528\u6237
42002
42012
  - \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
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
42004
42013
  `;
42005
42014
 
42006
42015
  // src/session/reducer.ts
@@ -46755,8 +46764,8 @@ function turnStartInput(text) {
46755
46764
  const items = [];
46756
46765
  let leftover = text;
46757
46766
  for (const m2 of text.matchAll(SKILL_RE)) {
46758
- const [marker, name, path77] = m2;
46759
- items.push({ type: "skill", name, path: path77 });
46767
+ const [marker, name, path76] = m2;
46768
+ items.push({ type: "skill", name, path: path76 });
46760
46769
  leftover = leftover.replace(marker, "");
46761
46770
  }
46762
46771
  for (const m2 of text.matchAll(ATTACHMENT_RE2)) {
@@ -49109,43 +49118,6 @@ async function forwardInboxPostToPeer(args) {
49109
49118
  }
49110
49119
  return { ok: true };
49111
49120
  }
49112
- async function forwardContactSshKeyIssueToPeer(args) {
49113
- const f = args.fetchImpl ?? fetch;
49114
- const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
49115
- const url = `${base}/rpc/contact:sshKey:issue`;
49116
- let res;
49117
- try {
49118
- res = await f(url, {
49119
- method: "POST",
49120
- headers: {
49121
- "content-type": "application/json",
49122
- authorization: `Bearer ${args.contact.connectToken}`
49123
- },
49124
- body: JSON.stringify({ deviceId: args.selfDeviceIdForRequest })
49125
- });
49126
- } catch (err) {
49127
- return {
49128
- ok: false,
49129
- code: "NETWORK",
49130
- message: err instanceof Error ? err.message : String(err)
49131
- };
49132
- }
49133
- let json = null;
49134
- try {
49135
- json = await res.json();
49136
- } catch {
49137
- return { ok: false, code: "PROTOCOL", message: `peer non-JSON (HTTP ${res.status})` };
49138
- }
49139
- if (!json) {
49140
- return { ok: false, code: "PROTOCOL", message: `peer returned null (HTTP ${res.status})` };
49141
- }
49142
- if (json.ok === false) {
49143
- const j = json;
49144
- return { ok: false, code: j.error ?? "UNKNOWN", message: j.message ?? "" };
49145
- }
49146
- const r = json.result;
49147
- return { ok: true, privateKeyPem: r.privateKeyPem, publicKeyLine: r.publicKeyLine };
49148
- }
49149
49121
 
49150
49122
  // src/tools/codex-history.ts
49151
49123
  var import_node_child_process5 = require("child_process");
@@ -49333,13 +49305,13 @@ function mapSkillsListResponse(res) {
49333
49305
  const r = s ?? {};
49334
49306
  const name = str3(r.name);
49335
49307
  if (!name) continue;
49336
- const path77 = str3(r.path);
49308
+ const path76 = str3(r.path);
49337
49309
  const description = str3(r.description);
49338
49310
  const isPlugin = name.includes(":");
49339
49311
  out.push({
49340
49312
  name,
49341
49313
  source: isPlugin ? "plugin" : "project",
49342
- ...path77 ? { path: path77 } : {},
49314
+ ...path76 ? { path: path76 } : {},
49343
49315
  ...description ? { description } : {},
49344
49316
  ...isPlugin ? { plugin: name.split(":")[0] } : {}
49345
49317
  });
@@ -50625,7 +50597,11 @@ var AuthGate = class {
50625
50597
  st.pending = true;
50626
50598
  let r;
50627
50599
  try {
50628
- r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
50600
+ r = await this.opts.authenticate(
50601
+ parsed.data.token,
50602
+ parsed.data.selfUrl,
50603
+ parsed.data.selfSshPublicKey
50604
+ );
50629
50605
  } finally {
50630
50606
  st.pending = false;
50631
50607
  }
@@ -51213,9 +51189,8 @@ var ContactStore = class {
51213
51189
  return true;
51214
51190
  }
51215
51191
  /**
51216
- * 更新单条 contact 的 SSH 授权(PR: contact-ssh-sandbox)。对齐 setPin pattern:
51217
- * store 只做原始 mutation,不做业务校验(如"sshAllowed=false 时清空 exposedDirs")——
51218
- * 那是 handler / UI 的责任。数组语义是完全替换(不 append)。
51192
+ * 更新单条 contact 的 SSH 授权开关(对齐 setPin pattern)。T-29 去沙箱后只改 sshAllowed;
51193
+ * exposedDirs 已废弃,本方法不再触碰它(保留 contact 原值不动)。
51219
51194
  * @returns 是否命中:deviceId 不存在返 false;命中即 flush.
51220
51195
  */
51221
51196
  setSshAccess(deviceId, opts) {
@@ -51223,8 +51198,7 @@ var ContactStore = class {
51223
51198
  if (!existing) return false;
51224
51199
  this.contacts.set(deviceId, {
51225
51200
  ...existing,
51226
- sshAllowed: opts.sshAllowed,
51227
- exposedDirs: opts.exposedDirs
51201
+ sshAllowed: opts.sshAllowed
51228
51202
  });
51229
51203
  this.flush();
51230
51204
  return true;
@@ -51272,7 +51246,10 @@ async function connectRemote(args) {
51272
51246
  // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
51273
51247
  // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
51274
51248
  // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
51275
- ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
51249
+ ...args.selfUrl ? { selfUrl: args.selfUrl } : {},
51250
+ // ssh identity 互换(spec 2026-07-15):自报本机 identity pubkey;缺省省略字段
51251
+ // (对方 schema selfSshPublicKey 是 .min(1).optional(),不发空串)。
51252
+ ...args.selfSshPublicKey ? { selfSshPublicKey: args.selfSshPublicKey } : {}
51276
51253
  })
51277
51254
  );
51278
51255
  await new Promise((resolve6, reject) => {
@@ -51347,7 +51324,9 @@ async function connectRemote(args) {
51347
51324
  ownerId: f.owner.ownerId,
51348
51325
  provider: f.owner.provider,
51349
51326
  displayName: f.owner.displayName,
51350
- grants: f.capability.grants
51327
+ grants: f.capability.grants,
51328
+ // ssh identity 互换(spec 2026-07-15):对方老版本不带 → 省略(不落 undefined 占位)
51329
+ ...f.sshPublicKey ? { sshPublicKey: f.sshPublicKey } : {}
51351
51330
  };
51352
51331
  },
51353
51332
  close: () => {
@@ -51368,6 +51347,8 @@ async function autoReverseContact(args) {
51368
51347
  resource: { ...g2.resource },
51369
51348
  actions: [...g2.actions]
51370
51349
  }));
51350
+ const existing = args.store.get(args.deviceId) ?? void 0;
51351
+ const sshPublicKey = args.sshPublicKey ?? existing?.sshPublicKey;
51371
51352
  const base = {
51372
51353
  deviceId: args.deviceId,
51373
51354
  ownerId: args.ownerId,
@@ -51377,12 +51358,17 @@ async function autoReverseContact(args) {
51377
51358
  connectToken: "",
51378
51359
  grants,
51379
51360
  addedAt: now(),
51380
- pinnedAt: null,
51381
- sshAllowed: false,
51361
+ pinnedAt: existing?.pinnedAt ?? null,
51362
+ sshAllowed: existing?.sshAllowed ?? false,
51363
+ // ssh identity 互换(spec 2026-07-15):缺省不落 undefined 占位(JSON 持久化干净)
51364
+ ...sshPublicKey ? { sshPublicKey } : {},
51382
51365
  exposedDirs: []
51383
51366
  };
51384
51367
  args.store.upsert(base);
51385
51368
  args.broadcast({ type: "contact:added", contact: base });
51369
+ if (base.sshAllowed && sshPublicKey && sshPublicKey !== existing?.sshPublicKey) {
51370
+ args.rebuildAuthorizedKeys?.();
51371
+ }
51386
51372
  if (!args.exchangeToken) return;
51387
51373
  let token;
51388
51374
  try {
@@ -53533,8 +53519,8 @@ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
53533
53519
  # 真实请求(interactive shell 时为空)。
53534
53520
  #
53535
53521
  # 职责:
53536
- # 1. 读 ~/.clawd/contacts.json 校验 contact.sshAllowed,读 exposedDirs 作为提示
53537
- # 2. 直接 exec shell(去沙箱后无目录级限制;exposedDirs 仅 echo 给对端参考)
53522
+ # 1. 读 ~/.clawd/contacts.json 校验 contact.sshAllowed(不通过即拒)
53523
+ # 2. 直接 exec shell(去沙箱后无目录级限制)
53538
53524
 
53539
53525
  set -euo pipefail
53540
53526
 
@@ -53561,13 +53547,15 @@ log_line INFO jail.entered "clawd-ssh-jail 起来 device=${"${DEVICE_ID}"} cmd=$
53561
53547
 
53562
53548
  CONTACTS="${"${HOME}"}/.clawd/contacts.json"
53563
53549
  if [ ! -f "$CONTACTS" ]; then
53564
- log_line ERROR jail.entered "contacts.json 不存在,无法查 exposedDirs"
53550
+ log_line ERROR jail.entered "contacts.json 不存在,无法校验 sshAllowed"
53565
53551
  echo "clawd-ssh-jail: contacts.json missing" >&2
53566
53552
  exit 1
53567
53553
  fi
53568
53554
 
53569
- # contact 的 exposedDirs (mac/linux 都自带 python3)
53570
- EXPOSED_JSON=$(python3 -c "
53555
+ # 校验该 contact 的 sshAllowed(mac/linux 都自带 python3)。去沙箱后不再读目录白名单:
53556
+ # sshAllowed=false DENIED exit 2;deviceId 未命中 → NOT_FOUND exit 3;命中且允许 → exit 0。
53557
+ # set -e 下 python 非 0 退出即终止脚本(不进 exec),只有 exit 0 才继续 exec shell。
53558
+ python3 -c "
53571
53559
  import json, sys
53572
53560
  with open('$CONTACTS') as f:
53573
53561
  data = json.load(f)
@@ -53575,25 +53563,11 @@ for c in data.get('contacts', []):
53575
53563
  if c.get('deviceId') == '$DEVICE_ID':
53576
53564
  if not c.get('sshAllowed'):
53577
53565
  print('DENIED', file=sys.stderr); sys.exit(2)
53578
- for d in c.get('exposedDirs', []):
53579
- print(d)
53580
53566
  sys.exit(0)
53581
- sys.exit(3)
53582
- ")
53583
-
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"
53596
- fi
53567
+ print('NOT_FOUND', file=sys.stderr); sys.exit(3)
53568
+ "
53569
+
53570
+ log_line INFO jail.entered "sshAllowed 校验通过,放行 exec shell"
53597
53571
 
53598
53572
  CMD="${"${SSH_ORIGINAL_COMMAND:-}"}"
53599
53573
  if [ -z "$CMD" ]; then
@@ -53877,10 +53851,10 @@ function rebuildAuthorizedKeys(store, sshdDir) {
53877
53851
  if (!c.sshAllowed) continue;
53878
53852
  const safe = /^[A-Za-z0-9_.-]+$/.test(c.deviceId);
53879
53853
  if (!safe) continue;
53880
- const pubkey = readIssuedPubkey(sshdDir, c.deviceId);
53881
- if (!pubkey) continue;
53854
+ const pubkey = c.sshPublicKey?.trim();
53855
+ if (!pubkey || /[\r\n"]/.test(pubkey)) continue;
53882
53856
  const bin = jailBinPath();
53883
- lines.push(`command="${bin} ${c.deviceId}",restrict ${pubkey.trim()}`);
53857
+ lines.push(`command="${bin} ${c.deviceId}",restrict ${pubkey}`);
53884
53858
  lines.push(`# contact:${c.deviceId}`);
53885
53859
  }
53886
53860
  const body = lines.join("\n") + "\n";
@@ -53888,25 +53862,59 @@ function rebuildAuthorizedKeys(store, sshdDir) {
53888
53862
  import_node_fs40.default.writeFileSync(tmp, body, { mode: 384 });
53889
53863
  import_node_fs40.default.renameSync(tmp, target);
53890
53864
  }
53891
- function readIssuedPubkey(sshdDir, deviceId) {
53892
- const safeId = deviceId.replace(/[\/\\]/g, "_");
53893
- const p2 = import_node_path40.default.join(sshdDir, "keys", `${safeId}.ed25519.pub`);
53865
+
53866
+ // src/sshd/ssh-identity.ts
53867
+ var import_node_fs41 = __toESM(require("fs"), 1);
53868
+ var import_node_path41 = __toESM(require("path"), 1);
53869
+ var import_node_child_process12 = require("child_process");
53870
+ var IDENTITY_DIR = "ssh-identity";
53871
+ var KEY_FILE = "id_ed25519";
53872
+ function identityPaths(dataDir) {
53873
+ const privPath = import_node_path41.default.join(dataDir, IDENTITY_DIR, KEY_FILE);
53874
+ return { privPath, pubPath: `${privPath}.pub` };
53875
+ }
53876
+ async function ensureSshIdentity(dataDir, opts = {}) {
53877
+ const { privPath, pubPath } = identityPaths(dataDir);
53878
+ import_node_fs41.default.mkdirSync(import_node_path41.default.dirname(privPath), { recursive: true, mode: 448 });
53879
+ if (import_node_fs41.default.existsSync(privPath) && import_node_fs41.default.existsSync(pubPath)) {
53880
+ return { privateKeyPath: privPath, publicKeyLine: import_node_fs41.default.readFileSync(pubPath, "utf8").trim() };
53881
+ }
53882
+ const bin = opts.keygenBin ?? "/usr/bin/ssh-keygen";
53883
+ await new Promise((resolve6, reject) => {
53884
+ const p2 = (opts.spawnImpl ?? import_node_child_process12.spawn)(
53885
+ bin,
53886
+ ["-t", "ed25519", "-f", privPath, "-N", "", "-q", "-C", "clawd-ssh-identity"],
53887
+ { stdio: "ignore" }
53888
+ );
53889
+ p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
53890
+ p2.on("error", reject);
53891
+ });
53892
+ try {
53893
+ import_node_fs41.default.chmodSync(privPath, 384);
53894
+ } catch {
53895
+ }
53896
+ try {
53897
+ import_node_fs41.default.chmodSync(pubPath, 420);
53898
+ } catch {
53899
+ }
53900
+ return { privateKeyPath: privPath, publicKeyLine: import_node_fs41.default.readFileSync(pubPath, "utf8").trim() };
53901
+ }
53902
+ function readSshIdentityPublicKey(dataDir) {
53894
53903
  try {
53895
- return import_node_fs40.default.readFileSync(p2, "utf8");
53904
+ return import_node_fs41.default.readFileSync(identityPaths(dataDir).pubPath, "utf8").trim();
53896
53905
  } catch {
53897
53906
  return null;
53898
53907
  }
53899
53908
  }
53900
53909
 
53901
- // src/sshd/contact-key-puller.ts
53902
- var import_node_fs42 = __toESM(require("fs"), 1);
53903
- var import_node_path42 = __toESM(require("path"), 1);
53910
+ // src/sshd/ssh-tunnel-relay.ts
53911
+ var import_node_net2 = __toESM(require("net"), 1);
53904
53912
 
53905
53913
  // src/sshd/contact-ssh-log.ts
53906
- var import_node_fs41 = __toESM(require("fs"), 1);
53907
- var import_node_path41 = __toESM(require("path"), 1);
53914
+ var import_node_fs42 = __toESM(require("fs"), 1);
53915
+ var import_node_path42 = __toESM(require("path"), 1);
53908
53916
  function createContactSshLog(dataDir) {
53909
- const file = import_node_path41.default.join(dataDir, "log", "contact-ssh.log");
53917
+ const file = import_node_path42.default.join(dataDir, "log", "contact-ssh.log");
53910
53918
  function append(level, tag, message, meta) {
53911
53919
  const time = (/* @__PURE__ */ new Date()).toISOString();
53912
53920
  let line = `[${time}] [${level}] [${tag}] ${message}`;
@@ -53919,8 +53927,8 @@ function createContactSshLog(dataDir) {
53919
53927
  }
53920
53928
  line += "\n";
53921
53929
  try {
53922
- import_node_fs41.default.mkdirSync(import_node_path41.default.dirname(file), { recursive: true });
53923
- import_node_fs41.default.appendFileSync(file, line, { mode: 384 });
53930
+ import_node_fs42.default.mkdirSync(import_node_path42.default.dirname(file), { recursive: true });
53931
+ import_node_fs42.default.appendFileSync(file, line, { mode: 384 });
53924
53932
  } catch {
53925
53933
  }
53926
53934
  }
@@ -53939,118 +53947,7 @@ var nullContactSshLog = {
53939
53947
  }
53940
53948
  };
53941
53949
 
53942
- // src/sshd/contact-key-puller.ts
53943
- var CONTACT_KEYS_DIR = "contact-ssh-keys";
53944
- function safeContactKeyPath(dataDir, deviceId) {
53945
- const safeId = deviceId.replace(/[\/\\]/g, "_");
53946
- return import_node_path42.default.join(dataDir, CONTACT_KEYS_DIR, `${safeId}.ed25519`);
53947
- }
53948
- async function pullContactSshKeyOnce(deps) {
53949
- const forward = deps.forwardImpl ?? ((c) => forwardContactSshKeyIssueToPeer({
53950
- contact: { remoteUrl: c.remoteUrl, connectToken: c.connectToken },
53951
- selfDeviceIdForRequest: c.deviceId
53952
- }));
53953
- const contacts = deps.store.list().filter((c) => c.connectToken.length > 0);
53954
- const results = await Promise.all(
53955
- contacts.map(async (c) => {
53956
- try {
53957
- const r = await forward(c);
53958
- return { contact: c, result: r };
53959
- } catch (err) {
53960
- return {
53961
- contact: c,
53962
- result: {
53963
- ok: false,
53964
- code: "NETWORK",
53965
- message: err instanceof Error ? err.message : String(err)
53966
- }
53967
- };
53968
- }
53969
- })
53970
- );
53971
- const errors = [];
53972
- let pulled = 0;
53973
- const sshLog = deps.sshLog ?? nullContactSshLog;
53974
- for (const { contact, result } of results) {
53975
- if (result.ok) {
53976
- writeKeyFile(deps.dataDir, contact.deviceId, result.privateKeyPem);
53977
- pulled++;
53978
- deps.logger?.info("contact-key-puller: pulled", { deviceId: contact.deviceId });
53979
- sshLog.info("key.pull.success", "B \u4FA7\u4ECE A \u62C9\u5230 privkey \u5E76\u843D\u76D8", {
53980
- peerDeviceId: contact.deviceId,
53981
- peerDisplayName: contact.displayName
53982
- });
53983
- } else if (result.code === "UNAUTHORIZED" || result.code === "FORBIDDEN") {
53984
- const hadStale = removeKeyFile(deps.dataDir, contact.deviceId);
53985
- sshLog.info("key.pull.rejected", "A \u4FA7\u672A\u6388\u6743\u6211 SSH\uFF08\u6B63\u5E38\u72B6\u6001\uFF1B\u8F6E\u8BE2\u7EE7\u7EED\uFF09", {
53986
- peerDeviceId: contact.deviceId,
53987
- peerDisplayName: contact.displayName,
53988
- clearedStalePrivkey: hadStale
53989
- });
53990
- } else {
53991
- errors.push({
53992
- deviceId: contact.deviceId,
53993
- code: result.code,
53994
- message: result.message
53995
- });
53996
- deps.logger?.warn("contact-key-puller: pull failed", {
53997
- deviceId: contact.deviceId,
53998
- code: result.code,
53999
- message: result.message
54000
- });
54001
- sshLog.warn("key.pull.error", "\u62C9 privkey \u65F6\u7F51\u7EDC/\u534F\u8BAE\u9519\u8BEF", {
54002
- peerDeviceId: contact.deviceId,
54003
- code: result.code,
54004
- message: result.message
54005
- });
54006
- }
54007
- }
54008
- return { pulled, errors };
54009
- }
54010
- function writeKeyFile(dataDir, deviceId, pem) {
54011
- const p2 = safeContactKeyPath(dataDir, deviceId);
54012
- import_node_fs42.default.mkdirSync(import_node_path42.default.dirname(p2), { recursive: true, mode: 448 });
54013
- import_node_fs42.default.writeFileSync(p2, pem, { mode: 384 });
54014
- }
54015
- function removeKeyFile(dataDir, deviceId) {
54016
- try {
54017
- import_node_fs42.default.unlinkSync(safeContactKeyPath(dataDir, deviceId));
54018
- return true;
54019
- } catch {
54020
- return false;
54021
- }
54022
- }
54023
- var ContactKeyPuller = class {
54024
- constructor(deps) {
54025
- this.deps = deps;
54026
- }
54027
- deps;
54028
- timer = null;
54029
- async start() {
54030
- const interval = this.deps.intervalMs ?? 6e4;
54031
- void this.tick();
54032
- this.timer = setInterval(() => void this.tick(), interval);
54033
- this.timer.unref();
54034
- }
54035
- stop() {
54036
- if (this.timer) {
54037
- clearInterval(this.timer);
54038
- this.timer = null;
54039
- }
54040
- }
54041
- async tick() {
54042
- try {
54043
- await pullContactSshKeyOnce(this.deps);
54044
- } catch (err) {
54045
- this.deps.logger?.warn("contact-key-puller: tick failed", {
54046
- err: err instanceof Error ? err.message : String(err)
54047
- });
54048
- }
54049
- }
54050
- };
54051
-
54052
53950
  // src/sshd/ssh-tunnel-relay.ts
54053
- var import_node_net2 = __toESM(require("net"), 1);
54054
53951
  async function handleSshTunnelUpgrade(req, socket, head, deps) {
54055
53952
  const sshLog = deps.sshLog ?? nullContactSshLog;
54056
53953
  const clientAddr = (req.socket && "remoteAddress" in req.socket ? req.socket.remoteAddress : null) ?? "unknown";
@@ -54641,9 +54538,9 @@ var CentralClientError = class extends Error {
54641
54538
  code;
54642
54539
  cause;
54643
54540
  };
54644
- async function centralRequest(opts, path77, init) {
54541
+ async function centralRequest(opts, path76, init) {
54645
54542
  const f = opts.fetchImpl ?? globalThis.fetch;
54646
- const url = `${opts.api.replace(/\/+$/, "")}${path77}`;
54543
+ const url = `${opts.api.replace(/\/+$/, "")}${path76}`;
54647
54544
  const ctrl = new AbortController();
54648
54545
  const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
54649
54546
  let res;
@@ -55580,11 +55477,11 @@ init_protocol();
55580
55477
  init_protocol();
55581
55478
 
55582
55479
  // src/workspace/git.ts
55583
- var import_node_child_process12 = require("child_process");
55480
+ var import_node_child_process13 = require("child_process");
55584
55481
  var import_node_fs47 = __toESM(require("fs"), 1);
55585
55482
  var import_node_path48 = __toESM(require("path"), 1);
55586
55483
  var import_node_util = require("util");
55587
- var pexec = (0, import_node_util.promisify)(import_node_child_process12.execFile);
55484
+ var pexec = (0, import_node_util.promisify)(import_node_child_process13.execFile);
55588
55485
  function normalizePath(p2) {
55589
55486
  const resolved = import_node_path48.default.resolve(p2);
55590
55487
  try {
@@ -55997,51 +55894,6 @@ function buildContactHandlers(deps) {
55997
55894
 
55998
55895
  // src/handlers/contact-ssh.ts
55999
55896
  init_protocol();
56000
-
56001
- // src/sshd/key-issue.ts
56002
- var import_node_fs48 = __toESM(require("fs"), 1);
56003
- var import_node_path49 = __toESM(require("path"), 1);
56004
- var import_node_child_process13 = require("child_process");
56005
- function safeDeviceId(deviceId) {
56006
- return deviceId.replace(/[\/\\]/g, "_");
56007
- }
56008
- async function issueContactSshKey(deviceId, sshdDir, opts = {}) {
56009
- const safeId = safeDeviceId(deviceId);
56010
- const keysDir = import_node_path49.default.join(sshdDir, "keys");
56011
- import_node_fs48.default.mkdirSync(keysDir, { recursive: true, mode: 448 });
56012
- const privPath = import_node_path49.default.join(keysDir, `${safeId}.ed25519`);
56013
- const pubPath = `${privPath}.pub`;
56014
- if (import_node_fs48.default.existsSync(privPath) && import_node_fs48.default.existsSync(pubPath)) {
56015
- return {
56016
- privateKeyPem: import_node_fs48.default.readFileSync(privPath, "utf8"),
56017
- publicKeyLine: import_node_fs48.default.readFileSync(pubPath, "utf8").trim()
56018
- };
56019
- }
56020
- const bin = opts.keygenBin ?? "/usr/bin/ssh-keygen";
56021
- await new Promise((resolve6, reject) => {
56022
- const p2 = (opts.spawnImpl ?? import_node_child_process13.spawn)(
56023
- bin,
56024
- ["-t", "ed25519", "-f", privPath, "-N", "", "-q", "-C", `clawd-contact-${safeId}`],
56025
- { stdio: "ignore" }
56026
- );
56027
- p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
56028
- p2.on("error", reject);
56029
- });
56030
- try {
56031
- import_node_fs48.default.chmodSync(privPath, 384);
56032
- } catch {
56033
- }
56034
- try {
56035
- import_node_fs48.default.chmodSync(pubPath, 420);
56036
- } catch {
56037
- }
56038
- return {
56039
- privateKeyPem: import_node_fs48.default.readFileSync(privPath, "utf8"),
56040
- publicKeyLine: import_node_fs48.default.readFileSync(pubPath, "utf8").trim()
56041
- };
56042
- }
56043
-
56044
- // src/handlers/contact-ssh.ts
56045
55897
  function ensureOwner2(ctx) {
56046
55898
  if (!ctx || ctx.principal.kind !== "owner") {
56047
55899
  throw new ClawdError(
@@ -56050,15 +55902,6 @@ function ensureOwner2(ctx) {
56050
55902
  );
56051
55903
  }
56052
55904
  }
56053
- function ensureGuest(ctx) {
56054
- if (!ctx || ctx.principal.kind !== "guest") {
56055
- throw new ClawdError(
56056
- ERROR_CODES.UNAUTHORIZED,
56057
- "UNAUTHORIZED: contact:sshKey:issue requires guest ctx"
56058
- );
56059
- }
56060
- return ctx.principal.id;
56061
- }
56062
55905
  function buildContactSshHandlers(deps) {
56063
55906
  const sshLog = deps.sshLog ?? nullContactSshLog;
56064
55907
  const setSshAccess = async (frame, _client, ctx) => {
@@ -56066,8 +55909,7 @@ function buildContactSshHandlers(deps) {
56066
55909
  const { type: _t, requestId: _r, ...rest } = frame;
56067
55910
  const args = ContactSetSshAccessArgsSchema.parse(rest);
56068
55911
  const hit = deps.store.setSshAccess(args.deviceId, {
56069
- sshAllowed: args.sshAllowed,
56070
- exposedDirs: args.exposedDirs
55912
+ sshAllowed: args.sshAllowed
56071
55913
  });
56072
55914
  if (!hit) {
56073
55915
  sshLog.warn("authz.setSshAccess", "owner \u5C1D\u8BD5\u6539 SSH \u6388\u6743\u4F46 contact \u4E0D\u5728 store", {
@@ -56081,65 +55923,23 @@ function buildContactSshHandlers(deps) {
56081
55923
  rebuildAuthorizedKeys(deps.store, deps.sshdDir);
56082
55924
  sshLog.info("authz.setSshAccess", "owner \u6539\u4E86 SSH \u6388\u6743 \u2192 authorized_keys \u5DF2\u91CD\u5EFA", {
56083
55925
  peerDeviceId: args.deviceId,
56084
- sshAllowed: args.sshAllowed,
56085
- exposedDirs: args.exposedDirs
55926
+ sshAllowed: args.sshAllowed
56086
55927
  });
56087
55928
  deps.broadcast({
56088
55929
  type: "contact:ssh-access-updated",
56089
55930
  deviceId: args.deviceId,
56090
- sshAllowed: args.sshAllowed,
56091
- exposedDirs: args.exposedDirs
55931
+ sshAllowed: args.sshAllowed
56092
55932
  });
56093
55933
  return {
56094
55934
  response: {
56095
55935
  type: "contact:setSshAccess:ok",
56096
55936
  deviceId: args.deviceId,
56097
- sshAllowed: args.sshAllowed,
56098
- exposedDirs: args.exposedDirs
56099
- }
56100
- };
56101
- };
56102
- const sshKeyIssue = async (frame, _client, ctx) => {
56103
- const callerDeviceId = ensureGuest(ctx);
56104
- const { type: _t, requestId: _r, ...rest } = frame;
56105
- ContactSshKeyIssueArgsSchema.parse(rest);
56106
- const contact = deps.store.get(callerDeviceId);
56107
- if (!contact || !contact.sshAllowed) {
56108
- sshLog.info(
56109
- "key.issue.rejected",
56110
- contact ? "guest \u8BF7\u6C42 SSH key \u4F46 owner \u672A\u6388\u6743" : "guest \u8BF7\u6C42 SSH key \u4F46\u4E0D\u5728\u6211\u7684 contactStore",
56111
- {
56112
- peerDeviceId: callerDeviceId,
56113
- peerDisplayName: contact?.displayName
56114
- }
56115
- );
56116
- throw new ClawdError(
56117
- ERROR_CODES.UNAUTHORIZED,
56118
- `UNAUTHORIZED: contact ${callerDeviceId} not authorized for SSH`
56119
- );
56120
- }
56121
- const { privateKeyPem, publicKeyLine } = await issueContactSshKey(callerDeviceId, deps.sshdDir);
56122
- rebuildAuthorizedKeys(deps.store, deps.sshdDir);
56123
- sshLog.info(
56124
- "key.issue.success",
56125
- "A \u4FA7\u53D1 privkey \u7ED9 guest\uFF08\u5E42\u7B49\uFF09\uFF0Cauthorized_keys \u5DF2\u91CD\u5EFA",
56126
- {
56127
- peerDeviceId: callerDeviceId,
56128
- peerDisplayName: contact.displayName,
56129
- publicKeyFingerprint: publicKeyLine.slice(0, 32) + "..."
56130
- }
56131
- );
56132
- return {
56133
- response: {
56134
- type: "contact:sshKey:issue:ok",
56135
- privateKeyPem,
56136
- publicKeyLine
55937
+ sshAllowed: args.sshAllowed
56137
55938
  }
56138
55939
  };
56139
55940
  };
56140
55941
  return {
56141
- "contact:setSshAccess": setSshAccess,
56142
- "contact:sshKey:issue": sshKeyIssue
55942
+ "contact:setSshAccess": setSshAccess
56143
55943
  };
56144
55944
  }
56145
55945
 
@@ -56191,12 +55991,14 @@ function buildWhoamiHandler(deps) {
56191
55991
  grantedPersonas.push({ id: file.personaId, displayName: file.label });
56192
55992
  }
56193
55993
  }
55994
+ const sshPublicKey = deps.sshIdentityPublicKey();
56194
55995
  return {
56195
55996
  response: {
56196
55997
  type: "whoami:ok",
56197
55998
  owner,
56198
55999
  capability,
56199
- grantedPersonas
56000
+ grantedPersonas,
56001
+ ...sshPublicKey ? { sshPublicKey } : {}
56200
56002
  }
56201
56003
  };
56202
56004
  };
@@ -56272,7 +56074,9 @@ function buildDeviceHandlers(deps) {
56272
56074
  // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
56273
56075
  // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
56274
56076
  // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
56275
- selfUrl: deps.selfUrl() ?? void 0
56077
+ selfUrl: deps.selfUrl() ?? void 0,
56078
+ // ssh identity 互换(spec 2026-07-15):自报本机 pubkey;未生成 → 省略
56079
+ selfSshPublicKey: deps.selfSshPublicKey() ?? void 0
56276
56080
  });
56277
56081
  } catch (e) {
56278
56082
  throw new ClawdError(
@@ -56282,6 +56086,8 @@ function buildDeviceHandlers(deps) {
56282
56086
  }
56283
56087
  try {
56284
56088
  const wh = await conn.whoami();
56089
+ const existing = deps.store.get(args.deviceId) ?? void 0;
56090
+ const sshPublicKey = wh.sshPublicKey ?? existing?.sshPublicKey;
56285
56091
  const contact = {
56286
56092
  deviceId: args.deviceId,
56287
56093
  ownerId: wh.ownerId,
@@ -56296,12 +56102,18 @@ function buildDeviceHandlers(deps) {
56296
56102
  connectToken: exchanged.token,
56297
56103
  grants: wh.grants,
56298
56104
  addedAt: now(),
56299
- pinnedAt: null,
56300
- sshAllowed: false,
56105
+ pinnedAt: existing?.pinnedAt ?? null,
56106
+ sshAllowed: existing?.sshAllowed ?? false,
56107
+ // ssh identity 互换(spec 2026-07-15):对方 whoami:ok 下发的 pubkey 落库;
56108
+ // 对方老版本不带 → 保留既有值 / 不落 undefined 占位
56109
+ ...sshPublicKey ? { sshPublicKey } : {},
56301
56110
  exposedDirs: []
56302
56111
  };
56303
56112
  deps.store.upsert(contact);
56304
56113
  deps.broadcast({ type: "contact:added", contact });
56114
+ if (contact.sshAllowed && sshPublicKey && sshPublicKey !== existing?.sshPublicKey) {
56115
+ deps.rebuildAuthorizedKeys?.();
56116
+ }
56305
56117
  return {
56306
56118
  response: {
56307
56119
  type: "device:connect:ok",
@@ -56454,7 +56266,7 @@ function buildPersonaHandlers(deps) {
56454
56266
  }
56455
56267
 
56456
56268
  // src/handlers/attachment.ts
56457
- var import_node_path50 = __toESM(require("path"), 1);
56269
+ var import_node_path49 = __toESM(require("path"), 1);
56458
56270
  init_protocol();
56459
56271
  init_protocol();
56460
56272
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -56534,12 +56346,12 @@ function buildAttachmentHandlers(deps) {
56534
56346
  `session ${args.sessionId} scope unresolved`
56535
56347
  );
56536
56348
  }
56537
- const cwdAbs = import_node_path50.default.resolve(sessionFile.cwd);
56538
- const candidateAbs = import_node_path50.default.isAbsolute(args.relPath) ? import_node_path50.default.resolve(args.relPath) : import_node_path50.default.resolve(cwdAbs, args.relPath);
56349
+ const cwdAbs = import_node_path49.default.resolve(sessionFile.cwd);
56350
+ const candidateAbs = import_node_path49.default.isAbsolute(args.relPath) ? import_node_path49.default.resolve(args.relPath) : import_node_path49.default.resolve(cwdAbs, args.relPath);
56539
56351
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.signUrl", "group-acl");
56540
56352
  const entries = deps.groupFileStore.list(scope, args.sessionId);
56541
56353
  const entry = entries.find((e) => {
56542
- const storedAbs = import_node_path50.default.isAbsolute(e.relPath) ? import_node_path50.default.resolve(e.relPath) : import_node_path50.default.resolve(cwdAbs, e.relPath);
56354
+ const storedAbs = import_node_path49.default.isAbsolute(e.relPath) ? import_node_path49.default.resolve(e.relPath) : import_node_path49.default.resolve(cwdAbs, e.relPath);
56543
56355
  return storedAbs === candidateAbs && !e.stale;
56544
56356
  });
56545
56357
  if (!entry) {
@@ -56564,7 +56376,7 @@ function buildAttachmentHandlers(deps) {
56564
56376
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
56565
56377
  const f = deps.sessionStore.read(sessionId);
56566
56378
  if (!f) return;
56567
- assertGuestAttachmentPath(ctx, import_node_path50.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
56379
+ assertGuestAttachmentPath(ctx, import_node_path49.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
56568
56380
  }
56569
56381
  const groupAdd = async (frame, _client, ctx) => {
56570
56382
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -56579,8 +56391,8 @@ function buildAttachmentHandlers(deps) {
56579
56391
  if (!scope) {
56580
56392
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
56581
56393
  }
56582
- const cwdAbs = import_node_path50.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
56583
- const candidateAbs = import_node_path50.default.isAbsolute(args.relPath) ? import_node_path50.default.resolve(args.relPath) : import_node_path50.default.resolve(cwdAbs, args.relPath);
56394
+ const cwdAbs = import_node_path49.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
56395
+ const candidateAbs = import_node_path49.default.isAbsolute(args.relPath) ? import_node_path49.default.resolve(args.relPath) : import_node_path49.default.resolve(cwdAbs, args.relPath);
56584
56396
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.groupAdd", "cwd-subtree");
56585
56397
  const from = ctx?.principal.kind === "owner" ? "owner" : "agent";
56586
56398
  const size = 0;
@@ -56639,19 +56451,19 @@ function buildAttachmentHandlers(deps) {
56639
56451
 
56640
56452
  // src/handlers/extension.ts
56641
56453
  var import_promises8 = __toESM(require("fs/promises"), 1);
56642
- var import_node_path55 = __toESM(require("path"), 1);
56454
+ var import_node_path54 = __toESM(require("path"), 1);
56643
56455
  init_protocol();
56644
56456
 
56645
56457
  // src/extension/bundle-zip.ts
56646
56458
  var import_promises5 = __toESM(require("fs/promises"), 1);
56647
- var import_node_path51 = __toESM(require("path"), 1);
56459
+ var import_node_path50 = __toESM(require("path"), 1);
56648
56460
  var import_node_crypto14 = __toESM(require("crypto"), 1);
56649
56461
  var import_jszip2 = __toESM(require_lib3(), 1);
56650
56462
  async function bundleExtensionDir(dir) {
56651
56463
  const entries = await listFilesSorted(dir);
56652
56464
  const zip = new import_jszip2.default();
56653
56465
  for (const rel of entries) {
56654
- const abs = import_node_path51.default.join(dir, rel);
56466
+ const abs = import_node_path50.default.join(dir, rel);
56655
56467
  const content = await import_promises5.default.readFile(abs);
56656
56468
  zip.file(rel, content, { date: FIXED_DATE });
56657
56469
  }
@@ -56672,7 +56484,7 @@ async function listFilesSorted(rootDir) {
56672
56484
  return out;
56673
56485
  }
56674
56486
  async function walk(absRoot, relPrefix, out) {
56675
- const dirAbs = import_node_path51.default.join(absRoot, relPrefix);
56487
+ const dirAbs = import_node_path50.default.join(absRoot, relPrefix);
56676
56488
  const entries = await import_promises5.default.readdir(dirAbs, { withFileTypes: true });
56677
56489
  for (const e of entries) {
56678
56490
  if (IGNORE_BASENAMES.has(e.name)) continue;
@@ -56726,25 +56538,25 @@ function computePublishCheck(args) {
56726
56538
 
56727
56539
  // src/extension/install-flow.ts
56728
56540
  var import_promises6 = __toESM(require("fs/promises"), 1);
56729
- var import_node_path53 = __toESM(require("path"), 1);
56541
+ var import_node_path52 = __toESM(require("path"), 1);
56730
56542
  var import_node_os19 = __toESM(require("os"), 1);
56731
56543
  var import_node_crypto15 = __toESM(require("crypto"), 1);
56732
56544
  var import_jszip3 = __toESM(require_lib3(), 1);
56733
56545
 
56734
56546
  // src/extension/paths.ts
56735
56547
  var import_node_os18 = __toESM(require("os"), 1);
56736
- var import_node_path52 = __toESM(require("path"), 1);
56548
+ var import_node_path51 = __toESM(require("path"), 1);
56737
56549
  function clawdHomeRoot(override) {
56738
- return override ?? process.env.CLAWD_HOME ?? import_node_path52.default.join(import_node_os18.default.homedir(), ".clawd");
56550
+ return override ?? process.env.CLAWD_HOME ?? import_node_path51.default.join(import_node_os18.default.homedir(), ".clawd");
56739
56551
  }
56740
56552
  function extensionsRoot(override) {
56741
- return import_node_path52.default.join(clawdHomeRoot(override), "extensions");
56553
+ return import_node_path51.default.join(clawdHomeRoot(override), "extensions");
56742
56554
  }
56743
56555
  function publishedChannelsFile(override) {
56744
- return import_node_path52.default.join(clawdHomeRoot(override), "extensions-published.json");
56556
+ return import_node_path51.default.join(clawdHomeRoot(override), "extensions-published.json");
56745
56557
  }
56746
56558
  function bundleCacheRoot(override) {
56747
- return import_node_path52.default.join(clawdHomeRoot(override), "extension-bundles");
56559
+ return import_node_path51.default.join(clawdHomeRoot(override), "extension-bundles");
56748
56560
  }
56749
56561
 
56750
56562
  // src/extension/install-flow.ts
@@ -56771,7 +56583,7 @@ async function installFromChannel(args, deps) {
56771
56583
  throw new InstallError("ZIP_INVALID", `failed to load zip: ${e.message}`);
56772
56584
  }
56773
56585
  for (const name of Object.keys(zip.files)) {
56774
- if (name.includes("..") || name.startsWith("/") || import_node_path53.default.isAbsolute(name)) {
56586
+ if (name.includes("..") || name.startsWith("/") || import_node_path52.default.isAbsolute(name)) {
56775
56587
  throw new InstallError("ZIP_INVALID", `unsafe zip entry: ${name}`);
56776
56588
  }
56777
56589
  }
@@ -56803,7 +56615,7 @@ async function installFromChannel(args, deps) {
56803
56615
  );
56804
56616
  }
56805
56617
  const localExtId = namespacedExtId(ownerSlug, channelRef.ownerPrincipalId);
56806
- const destDir = import_node_path53.default.join(deps.extensionsRoot, localExtId);
56618
+ const destDir = import_node_path52.default.join(deps.extensionsRoot, localExtId);
56807
56619
  let destExists = false;
56808
56620
  try {
56809
56621
  await import_promises6.default.access(destDir);
@@ -56817,16 +56629,16 @@ async function installFromChannel(args, deps) {
56817
56629
  );
56818
56630
  }
56819
56631
  const stage = await import_promises6.default.mkdtemp(
56820
- import_node_path53.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
56632
+ import_node_path52.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
56821
56633
  );
56822
56634
  try {
56823
56635
  for (const [name, entry] of Object.entries(zip.files)) {
56824
- const dest = import_node_path53.default.join(stage, name);
56636
+ const dest = import_node_path52.default.join(stage, name);
56825
56637
  if (entry.dir) {
56826
56638
  await import_promises6.default.mkdir(dest, { recursive: true });
56827
56639
  continue;
56828
56640
  }
56829
- await import_promises6.default.mkdir(import_node_path53.default.dirname(dest), { recursive: true });
56641
+ await import_promises6.default.mkdir(import_node_path52.default.dirname(dest), { recursive: true });
56830
56642
  if (name === "manifest.json") {
56831
56643
  const rewritten = { ...parsed.data, id: localExtId };
56832
56644
  await import_promises6.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -56847,7 +56659,7 @@ async function installFromChannel(args, deps) {
56847
56659
 
56848
56660
  // src/extension/update-flow.ts
56849
56661
  var import_promises7 = __toESM(require("fs/promises"), 1);
56850
- var import_node_path54 = __toESM(require("path"), 1);
56662
+ var import_node_path53 = __toESM(require("path"), 1);
56851
56663
  var import_node_os20 = __toESM(require("os"), 1);
56852
56664
  var import_node_crypto16 = __toESM(require("crypto"), 1);
56853
56665
  var import_jszip4 = __toESM(require_lib3(), 1);
@@ -56864,11 +56676,11 @@ async function updateFromChannel(args, deps) {
56864
56676
  channelRef.extId,
56865
56677
  channelRef.ownerPrincipalId
56866
56678
  );
56867
- const liveDir = import_node_path54.default.join(deps.extensionsRoot, localExtId);
56679
+ const liveDir = import_node_path53.default.join(deps.extensionsRoot, localExtId);
56868
56680
  const prevDir = `${liveDir}.prev`;
56869
56681
  let existingVersion;
56870
56682
  try {
56871
- const raw = await import_promises7.default.readFile(import_node_path54.default.join(liveDir, "manifest.json"), "utf8");
56683
+ const raw = await import_promises7.default.readFile(import_node_path53.default.join(liveDir, "manifest.json"), "utf8");
56872
56684
  const parsed2 = ExtensionManifestSchema.safeParse(JSON.parse(raw));
56873
56685
  if (!parsed2.success) {
56874
56686
  throw new UpdateError(
@@ -56901,7 +56713,7 @@ async function updateFromChannel(args, deps) {
56901
56713
  throw new UpdateError("ZIP_INVALID", `failed to load zip: ${e.message}`);
56902
56714
  }
56903
56715
  for (const name of Object.keys(zip.files)) {
56904
- if (name.includes("..") || name.startsWith("/") || import_node_path54.default.isAbsolute(name)) {
56716
+ if (name.includes("..") || name.startsWith("/") || import_node_path53.default.isAbsolute(name)) {
56905
56717
  throw new UpdateError("ZIP_INVALID", `unsafe zip entry: ${name}`);
56906
56718
  }
56907
56719
  }
@@ -56936,16 +56748,16 @@ async function updateFromChannel(args, deps) {
56936
56748
  await import_promises7.default.rm(prevDir, { recursive: true, force: true });
56937
56749
  await import_promises7.default.rename(liveDir, prevDir);
56938
56750
  const stage = await import_promises7.default.mkdtemp(
56939
- import_node_path54.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
56751
+ import_node_path53.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
56940
56752
  );
56941
56753
  try {
56942
56754
  for (const [name, entry] of Object.entries(zip.files)) {
56943
- const dest = import_node_path54.default.join(stage, name);
56755
+ const dest = import_node_path53.default.join(stage, name);
56944
56756
  if (entry.dir) {
56945
56757
  await import_promises7.default.mkdir(dest, { recursive: true });
56946
56758
  continue;
56947
56759
  }
56948
- await import_promises7.default.mkdir(import_node_path54.default.dirname(dest), { recursive: true });
56760
+ await import_promises7.default.mkdir(import_node_path53.default.dirname(dest), { recursive: true });
56949
56761
  if (name === "manifest.json") {
56950
56762
  const rewritten = { ...parsed.data, id: localExtId };
56951
56763
  await import_promises7.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -57038,7 +56850,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
57038
56850
  );
57039
56851
  }
57040
56852
  }
57041
- const manifestPath = import_node_path55.default.join(root, extId, "manifest.json");
56853
+ const manifestPath = import_node_path54.default.join(root, extId, "manifest.json");
57042
56854
  const manifest = await readManifest(root, extId);
57043
56855
  const next = { ...manifest, version: newVersion };
57044
56856
  const tmp = `${manifestPath}.tmp`;
@@ -57046,7 +56858,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
57046
56858
  await import_promises8.default.rename(tmp, manifestPath);
57047
56859
  }
57048
56860
  async function readManifest(root, extId) {
57049
- const file = import_node_path55.default.join(root, extId, "manifest.json");
56861
+ const file = import_node_path54.default.join(root, extId, "manifest.json");
57050
56862
  let raw;
57051
56863
  try {
57052
56864
  raw = await import_promises8.default.readFile(file, "utf8");
@@ -57137,7 +56949,7 @@ function buildExtensionHandlers(deps) {
57137
56949
  };
57138
56950
  async function buildSnapshotMeta(extId) {
57139
56951
  const manifest = await readManifest(deps.root, extId);
57140
- const { sha256, buffer } = await bundleExtensionDir(import_node_path55.default.join(deps.root, extId));
56952
+ const { sha256, buffer } = await bundleExtensionDir(import_node_path54.default.join(deps.root, extId));
57141
56953
  return { manifest, contentHash: sha256, buffer };
57142
56954
  }
57143
56955
  const publish = async (frame, _client, ctx) => {
@@ -57318,9 +57130,9 @@ function buildExtensionHandlers(deps) {
57318
57130
  }
57319
57131
 
57320
57132
  // src/app-builder/project-store.ts
57321
- var import_node_fs49 = require("fs");
57133
+ var import_node_fs48 = require("fs");
57322
57134
  var import_node_child_process14 = require("child_process");
57323
- var import_node_path56 = require("path");
57135
+ var import_node_path55 = require("path");
57324
57136
  init_protocol();
57325
57137
  var PROJECTS_DIR = "projects";
57326
57138
  var META_FILE = ".clawd-project.json";
@@ -57334,19 +57146,19 @@ var ProjectStore = class {
57334
57146
  root;
57335
57147
  /** projects/<name>/.clawd-project.json 路径 */
57336
57148
  metaPath(name) {
57337
- return (0, import_node_path56.join)(this.projectsRoot(), name, META_FILE);
57149
+ return (0, import_node_path55.join)(this.projectsRoot(), name, META_FILE);
57338
57150
  }
57339
57151
  /** projects/<name>/ 目录路径(cwd 用) */
57340
57152
  projectDir(name) {
57341
- return (0, import_node_path56.join)(this.projectsRoot(), name);
57153
+ return (0, import_node_path55.join)(this.projectsRoot(), name);
57342
57154
  }
57343
57155
  projectsRoot() {
57344
- return (0, import_node_path56.join)(this.root, PROJECTS_DIR);
57156
+ return (0, import_node_path55.join)(this.root, PROJECTS_DIR);
57345
57157
  }
57346
57158
  async list() {
57347
57159
  let entries;
57348
57160
  try {
57349
- entries = await import_node_fs49.promises.readdir(this.projectsRoot());
57161
+ entries = await import_node_fs48.promises.readdir(this.projectsRoot());
57350
57162
  } catch (err) {
57351
57163
  if (err.code === "ENOENT") return [];
57352
57164
  throw err;
@@ -57354,7 +57166,7 @@ var ProjectStore = class {
57354
57166
  const out = [];
57355
57167
  for (const name of entries) {
57356
57168
  try {
57357
- const raw = await import_node_fs49.promises.readFile(this.metaPath(name), "utf8");
57169
+ const raw = await import_node_fs48.promises.readFile(this.metaPath(name), "utf8");
57358
57170
  const json = JSON.parse(raw);
57359
57171
  let migrated = false;
57360
57172
  if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
@@ -57365,7 +57177,7 @@ var ProjectStore = class {
57365
57177
  if (parsed.success) {
57366
57178
  out.push(parsed.data);
57367
57179
  if (migrated) {
57368
- void import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
57180
+ void import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
57369
57181
  });
57370
57182
  }
57371
57183
  }
@@ -57409,8 +57221,8 @@ var ProjectStore = class {
57409
57221
  throw new Error(`invalid name "${name}": ${validated.error.message}`);
57410
57222
  }
57411
57223
  const dir = this.projectDir(name);
57412
- await import_node_fs49.promises.mkdir(dir, { recursive: true });
57413
- await import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
57224
+ await import_node_fs48.promises.mkdir(dir, { recursive: true });
57225
+ await import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
57414
57226
  return meta;
57415
57227
  }
57416
57228
  /**
@@ -57453,7 +57265,7 @@ var ProjectStore = class {
57453
57265
  }
57454
57266
  async delete(name) {
57455
57267
  const dir = this.projectDir(name);
57456
- await import_node_fs49.promises.rm(dir, { recursive: true, force: true });
57268
+ await import_node_fs48.promises.rm(dir, { recursive: true, force: true });
57457
57269
  }
57458
57270
  async updatePort(name, newPort) {
57459
57271
  if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
@@ -57469,7 +57281,7 @@ var ProjectStore = class {
57469
57281
  throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
57470
57282
  }
57471
57283
  const updated = { ...target, port: newPort };
57472
- await import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
57284
+ await import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
57473
57285
  return updated;
57474
57286
  }
57475
57287
  /**
@@ -57486,7 +57298,7 @@ var ProjectStore = class {
57486
57298
  if (!validated.success) {
57487
57299
  throw new Error(`invalid prodUrl "${url}": ${validated.error.message}`);
57488
57300
  }
57489
- await import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57301
+ await import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57490
57302
  return validated.data;
57491
57303
  }
57492
57304
  /**
@@ -57507,7 +57319,7 @@ var ProjectStore = class {
57507
57319
  if (!validated.success) {
57508
57320
  throw new Error(`invalid publishJob: ${validated.error.message}`);
57509
57321
  }
57510
- await import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57322
+ await import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57511
57323
  return validated.data;
57512
57324
  }
57513
57325
  /** 清掉 .clawd-project.json.publishJob 字段。其他字段保持原样。 */
@@ -57522,7 +57334,7 @@ var ProjectStore = class {
57522
57334
  if (!validated.success) {
57523
57335
  throw new Error(`failed to clear publishJob: ${validated.error.message}`);
57524
57336
  }
57525
- await import_node_fs49.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57337
+ await import_node_fs48.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
57526
57338
  return validated.data;
57527
57339
  }
57528
57340
  };
@@ -57643,8 +57455,8 @@ var PublishJobRegistry = class {
57643
57455
 
57644
57456
  // src/app-builder/publish-job-runner.ts
57645
57457
  var import_node_child_process16 = require("child_process");
57646
- var import_node_fs50 = require("fs");
57647
- var import_node_path57 = require("path");
57458
+ var import_node_fs49 = require("fs");
57459
+ var import_node_path56 = require("path");
57648
57460
 
57649
57461
  // src/app-builder/publish-stage-parser.ts
57650
57462
  var STAGE_RE = /^\s*::stage::(build|deploy|verify)\s*$/;
@@ -57676,10 +57488,10 @@ async function startPublishJob(deps, args) {
57676
57488
  return { jobId: registry2.get(args.name).jobId, status: "already-publishing" };
57677
57489
  }
57678
57490
  const projDir = projectDir(args.name);
57679
- const logPath = (0, import_node_path57.join)(projDir, ".publish.log");
57491
+ const logPath = (0, import_node_path56.join)(projDir, ".publish.log");
57680
57492
  let logStream = null;
57681
57493
  try {
57682
- logStream = (0, import_node_fs50.createWriteStream)(logPath, { flags: "w" });
57494
+ logStream = (0, import_node_fs49.createWriteStream)(logPath, { flags: "w" });
57683
57495
  } catch {
57684
57496
  logStream = null;
57685
57497
  }
@@ -57936,8 +57748,8 @@ async function recoverInterruptedJobs(deps) {
57936
57748
 
57937
57749
  // src/handlers/app-builder.ts
57938
57750
  init_protocol();
57939
- var import_node_path58 = require("path");
57940
- var import_node_fs51 = require("fs");
57751
+ var import_node_path57 = require("path");
57752
+ var import_node_fs50 = require("fs");
57941
57753
  var APP_BUILDER_PERSONAS = ["persona-app-builder", "persona-dataclaw-builder"];
57942
57754
  var DEV_SERVER_READY_TIMEOUT_MS = 3e4;
57943
57755
  async function recoverInterruptedPublishJobs(store, logger) {
@@ -58018,7 +57830,7 @@ function buildAppBuilderHandlers(deps) {
58018
57830
  async function listAllUsersProjects() {
58019
57831
  if (!deps.usersRoot || !deps.getStore) return [];
58020
57832
  const getStore = deps.getStore;
58021
- const userIds = await import_node_fs51.promises.readdir(deps.usersRoot).catch(() => []);
57833
+ const userIds = await import_node_fs50.promises.readdir(deps.usersRoot).catch(() => []);
58022
57834
  const perUser = await Promise.all(
58023
57835
  userIds.map((uid) => getStore(uid).list().catch(() => []))
58024
57836
  );
@@ -58094,8 +57906,8 @@ function buildAppBuilderHandlers(deps) {
58094
57906
  const project = await userStore.create(f.name, reservedPorts);
58095
57907
  try {
58096
57908
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(session.ownerPersonaId ?? "") : deps.personaRoot;
58097
- const templateSrcDir = (0, import_node_path58.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
58098
- const scaffoldScript = (0, import_node_path58.join)(deps.deployKitRoot, "scripts", "new-extension.sh");
57909
+ const templateSrcDir = (0, import_node_path57.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
57910
+ const scaffoldScript = (0, import_node_path57.join)(deps.deployKitRoot, "scripts", "new-extension.sh");
58099
57911
  const scaffoldResult = await userStore.scaffold(project.name, templateSrcDir, scaffoldScript);
58100
57912
  deps.logger?.info("app-builder.scaffold.done", {
58101
57913
  name: project.name,
@@ -58316,7 +58128,7 @@ function buildAppBuilderHandlers(deps) {
58316
58128
  await userStore.clearPublishJob(args.name);
58317
58129
  }
58318
58130
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(boundSession.ownerPersonaId ?? "") : deps.personaRoot;
58319
- const scriptPath = (0, import_node_path58.join)(deps.deployKitRoot, "scripts", "publish.sh");
58131
+ const scriptPath = (0, import_node_path57.join)(deps.deployKitRoot, "scripts", "publish.sh");
58320
58132
  deps.logger?.info("app-builder.publish.start", {
58321
58133
  name: args.name,
58322
58134
  sessionId: boundSession.sessionId,
@@ -58492,7 +58304,7 @@ function buildVisitorHandlers(deps) {
58492
58304
 
58493
58305
  // src/extension/registry.ts
58494
58306
  var import_promises9 = __toESM(require("fs/promises"), 1);
58495
- var import_node_path59 = __toESM(require("path"), 1);
58307
+ var import_node_path58 = __toESM(require("path"), 1);
58496
58308
  async function loadAll(root) {
58497
58309
  let entries;
58498
58310
  try {
@@ -58505,13 +58317,13 @@ async function loadAll(root) {
58505
58317
  for (const ent of entries) {
58506
58318
  if (!ent.isDirectory()) continue;
58507
58319
  if (ent.name.startsWith(".")) continue;
58508
- records.push(await loadOne(import_node_path59.default.join(root, ent.name), ent.name));
58320
+ records.push(await loadOne(import_node_path58.default.join(root, ent.name), ent.name));
58509
58321
  }
58510
58322
  records.sort((a, b2) => a.extId < b2.extId ? -1 : a.extId > b2.extId ? 1 : 0);
58511
58323
  return records;
58512
58324
  }
58513
58325
  async function loadOne(dir, dirName) {
58514
- const manifestPath = import_node_path59.default.join(dir, "manifest.json");
58326
+ const manifestPath = import_node_path58.default.join(dir, "manifest.json");
58515
58327
  let raw;
58516
58328
  try {
58517
58329
  raw = await import_promises9.default.readFile(manifestPath, "utf8");
@@ -58556,7 +58368,7 @@ async function loadOne(dir, dirName) {
58556
58368
 
58557
58369
  // src/extension/uninstall.ts
58558
58370
  var import_promises10 = __toESM(require("fs/promises"), 1);
58559
- var import_node_path60 = __toESM(require("path"), 1);
58371
+ var import_node_path59 = __toESM(require("path"), 1);
58560
58372
  var UninstallError = class extends Error {
58561
58373
  constructor(code, message) {
58562
58374
  super(message);
@@ -58565,7 +58377,7 @@ var UninstallError = class extends Error {
58565
58377
  code;
58566
58378
  };
58567
58379
  async function uninstall(deps) {
58568
- const dir = import_node_path60.default.join(deps.root, deps.extId);
58380
+ const dir = import_node_path59.default.join(deps.root, deps.extId);
58569
58381
  try {
58570
58382
  await import_promises10.default.access(dir);
58571
58383
  } catch {
@@ -58632,7 +58444,8 @@ function buildMethodHandlers(deps) {
58632
58444
  ownerId: deps.ownerId,
58633
58445
  ownerProvider: deps.ownerProvider,
58634
58446
  personaStore: deps.personaStore,
58635
- capabilityManager: deps.capabilityManager
58447
+ capabilityManager: deps.capabilityManager,
58448
+ sshIdentityPublicKey: deps.sshIdentityPublicKey
58636
58449
  }),
58637
58450
  ...buildFeishuAuthHandlers(deps.feishuAuth),
58638
58451
  ...buildDeviceHandlers(deps.feishuDevice),
@@ -58965,11 +58778,8 @@ var METHOD_GRANT_MAP = {
58965
58778
  "contact:pin": ADMIN_ANY,
58966
58779
  "contact:remove": ADMIN_ANY,
58967
58780
  // contact:setSshAccess (owner UI 配 SSH 授权):ADMIN_ANY
58968
- // contact:sshKey:issue (guest daemon 拉自己的 privkey):public — handler 内校
58969
- // ctx.principal.kind==='guest' + store.get(callerId).sshAllowed
58970
58781
  // (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
58971
58782
  "contact:setSshAccess": ADMIN_ANY,
58972
- "contact:sshKey:issue": { kind: "public" },
58973
58783
  // ---- visitor:* (访客名单,owner-only) ----
58974
58784
  // owner 看完整访客名单(含没开会话的);guest 不可调(handler 内再 assertOwner 兜底)。
58975
58785
  "visitor:list": ADMIN_ANY,
@@ -59155,7 +58965,7 @@ async function dispatchRpc(method, frame, client, ctx, deps) {
59155
58965
 
59156
58966
  // src/extension/runtime.ts
59157
58967
  var import_node_child_process18 = require("child_process");
59158
- var import_node_path61 = __toESM(require("path"), 1);
58968
+ var import_node_path60 = __toESM(require("path"), 1);
59159
58969
  var import_promises11 = require("timers/promises");
59160
58970
 
59161
58971
  // src/extension/port-allocator.ts
@@ -59256,7 +59066,7 @@ var Runtime = class {
59256
59066
  /\$CLAWOS_EXT_PORT/g,
59257
59067
  String(port)
59258
59068
  );
59259
- const dir = import_node_path61.default.join(this.root, extId);
59069
+ const dir = import_node_path60.default.join(this.root, extId);
59260
59070
  const env = {
59261
59071
  ...process.env,
59262
59072
  CLAWOS_EXT_PORT: String(port),
@@ -59368,7 +59178,7 @@ ${handle.stderrTail}`
59368
59178
 
59369
59179
  // src/extension/published-channels.ts
59370
59180
  var import_promises12 = __toESM(require("fs/promises"), 1);
59371
- var import_node_path62 = __toESM(require("path"), 1);
59181
+ var import_node_path61 = __toESM(require("path"), 1);
59372
59182
  init_zod();
59373
59183
  var PublishedChannelsError = class extends Error {
59374
59184
  constructor(code, message) {
@@ -59467,7 +59277,7 @@ var PublishedChannelStore = class {
59467
59277
  )
59468
59278
  };
59469
59279
  const tmp = `${this.filePath}.tmp`;
59470
- await import_promises12.default.mkdir(import_node_path62.default.dirname(this.filePath), { recursive: true });
59280
+ await import_promises12.default.mkdir(import_node_path61.default.dirname(this.filePath), { recursive: true });
59471
59281
  await import_promises12.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
59472
59282
  await import_promises12.default.rename(tmp, this.filePath);
59473
59283
  }
@@ -59475,7 +59285,7 @@ var PublishedChannelStore = class {
59475
59285
 
59476
59286
  // src/extension/bundle-cache.ts
59477
59287
  var import_promises13 = __toESM(require("fs/promises"), 1);
59478
- var import_node_path63 = __toESM(require("path"), 1);
59288
+ var import_node_path62 = __toESM(require("path"), 1);
59479
59289
  var BundleCache = class {
59480
59290
  constructor(rootDir) {
59481
59291
  this.rootDir = rootDir;
@@ -59484,14 +59294,14 @@ var BundleCache = class {
59484
59294
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
59485
59295
  async write(snapshotHash, buffer) {
59486
59296
  await import_promises13.default.mkdir(this.rootDir, { recursive: true });
59487
- const file = import_node_path63.default.join(this.rootDir, `${snapshotHash}.zip`);
59297
+ const file = import_node_path62.default.join(this.rootDir, `${snapshotHash}.zip`);
59488
59298
  const tmp = `${file}.tmp`;
59489
59299
  await import_promises13.default.writeFile(tmp, buffer, { mode: 384 });
59490
59300
  await import_promises13.default.rename(tmp, file);
59491
59301
  }
59492
59302
  /** Returns the bundle bytes, or null when the file doesn't exist. */
59493
59303
  async read(snapshotHash) {
59494
- const file = import_node_path63.default.join(this.rootDir, `${snapshotHash}.zip`);
59304
+ const file = import_node_path62.default.join(this.rootDir, `${snapshotHash}.zip`);
59495
59305
  try {
59496
59306
  return await import_promises13.default.readFile(file);
59497
59307
  } catch (e) {
@@ -59501,7 +59311,7 @@ var BundleCache = class {
59501
59311
  }
59502
59312
  /** Idempotent — missing file is not an error. */
59503
59313
  async delete(snapshotHash) {
59504
- const file = import_node_path63.default.join(this.rootDir, `${snapshotHash}.zip`);
59314
+ const file = import_node_path62.default.join(this.rootDir, `${snapshotHash}.zip`);
59505
59315
  await import_promises13.default.rm(file, { force: true });
59506
59316
  }
59507
59317
  };
@@ -59524,24 +59334,24 @@ async function startDaemon(config) {
59524
59334
  sampling: logShippingCfg.sampling,
59525
59335
  homeDir: import_node_os21.default.homedir()
59526
59336
  });
59527
- const logDir = import_node_path64.default.join(config.dataDir, "log");
59528
- import_node_fs52.default.mkdirSync(logDir, { recursive: true });
59337
+ const logDir = import_node_path63.default.join(config.dataDir, "log");
59338
+ import_node_fs51.default.mkdirSync(logDir, { recursive: true });
59529
59339
  const logger = createLogger({
59530
59340
  level: config.logLevel,
59531
- file: import_node_path64.default.join(logDir, "clawd.log"),
59341
+ file: import_node_path63.default.join(logDir, "clawd.log"),
59532
59342
  logClient
59533
59343
  });
59534
59344
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
59535
59345
  const screenIdleProbeLogger = createFileOnlyLogger({
59536
- file: import_node_path64.default.join(logDir, "screen-idle-probe.log"),
59346
+ file: import_node_path63.default.join(logDir, "screen-idle-probe.log"),
59537
59347
  level: "debug"
59538
59348
  });
59539
59349
  logger.info("screen-idle probe logger enabled", {
59540
- file: import_node_path64.default.join(logDir, "screen-idle-probe.log")
59350
+ file: import_node_path63.default.join(logDir, "screen-idle-probe.log")
59541
59351
  });
59542
59352
  const CAP_TARGETS = [
59543
- import_node_path64.default.join(logDir, "clawd.log"),
59544
- import_node_path64.default.join(logDir, "screen-idle-probe.log")
59353
+ import_node_path63.default.join(logDir, "clawd.log"),
59354
+ import_node_path63.default.join(logDir, "screen-idle-probe.log")
59545
59355
  ];
59546
59356
  const LOG_CAP_MAX_BYTES = 10 * 1024 * 1024;
59547
59357
  const LOG_CAP_KEEP_BYTES = 5 * 1024 * 1024;
@@ -59637,7 +59447,7 @@ async function startDaemon(config) {
59637
59447
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
59638
59448
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
59639
59449
  expectedToken: resolvedAuthToken,
59640
- authenticate: async (t, selfUrl) => {
59450
+ authenticate: async (t, selfUrl, selfSshPublicKey) => {
59641
59451
  const result = await authenticate(t, buildConnectAuthDeps());
59642
59452
  if (result.ok && result.context.principal.kind === "guest") {
59643
59453
  void autoReverseContact({
@@ -59648,6 +59458,10 @@ async function startDaemon(config) {
59648
59458
  provider: result.context.provider ?? "",
59649
59459
  displayName: result.context.principal.displayName ?? result.context.principal.id,
59650
59460
  selfUrl,
59461
+ // ssh identity 互换(spec 2026-07-15):guest auth 帧自报的 pubkey 落 contact;
59462
+ // pubkey 变化且已授权时立即重建 authorized_keys(重新握手 = 迁移路径)
59463
+ sshPublicKey: selfSshPublicKey,
59464
+ rebuildAuthorizedKeys: () => rebuildAuthorizedKeys(contactStore, import_node_path63.default.join(config.dataDir, "sshd")),
59651
59465
  // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
59652
59466
  // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
59653
59467
  exchangeToken: async (targetDeviceId) => {
@@ -59695,8 +59509,8 @@ async function startDaemon(config) {
59695
59509
  const agents = new AgentsScanner();
59696
59510
  const history = new ClaudeHistoryReader();
59697
59511
  let transport = null;
59698
- const personaStore = new PersonaStore(import_node_path64.default.join(config.dataDir, "personas"));
59699
- const usersRoot = import_node_path64.default.join(config.dataDir, "users");
59512
+ const personaStore = new PersonaStore(import_node_path63.default.join(config.dataDir, "personas"));
59513
+ const usersRoot = import_node_path63.default.join(config.dataDir, "users");
59700
59514
  const defaultsRoot = findDefaultsRoot(logger);
59701
59515
  if (defaultsRoot) {
59702
59516
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -59716,17 +59530,17 @@ async function startDaemon(config) {
59716
59530
  migrateCodexSandbox({ store: personaStore, logger });
59717
59531
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
59718
59532
  const personaDispatchManager = new PersonaDispatchManager({ genId: () => v4_default() });
59719
- const here = typeof __dirname === "string" ? __dirname : import_node_path64.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
59533
+ const here = typeof __dirname === "string" ? __dirname : import_node_path63.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
59720
59534
  const mcpConfigs = [];
59721
59535
  const dispatchServerCandidates = [
59722
- import_node_path64.default.join(here, "dispatch", "mcp-server.cjs"),
59536
+ import_node_path63.default.join(here, "dispatch", "mcp-server.cjs"),
59723
59537
  // 生产 dist/index → dist/dispatch/mcp-server.cjs
59724
- import_node_path64.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
59538
+ import_node_path63.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
59725
59539
  // dev tsx src/index → ../dist/dispatch/mcp-server.cjs
59726
59540
  ];
59727
- const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59541
+ const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59728
59542
  if (dispatchServerScriptPath) {
59729
- const dispatchLogPath = import_node_path64.default.join(logDir, "dispatch-mcp-server.log");
59543
+ const dispatchLogPath = import_node_path63.default.join(logDir, "dispatch-mcp-server.log");
59730
59544
  const dispatchCfgPath = writeDispatchMcpConfig({
59731
59545
  dataDir: config.dataDir,
59732
59546
  serverScriptPath: dispatchServerScriptPath,
@@ -59744,12 +59558,12 @@ async function startDaemon(config) {
59744
59558
  });
59745
59559
  }
59746
59560
  const ticketServerCandidates = [
59747
- import_node_path64.default.join(here, "ticket", "mcp-server.cjs"),
59748
- import_node_path64.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
59561
+ import_node_path63.default.join(here, "ticket", "mcp-server.cjs"),
59562
+ import_node_path63.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
59749
59563
  ];
59750
- const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59564
+ const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59751
59565
  if (ticketServerScriptPath) {
59752
- const ticketLogPath = import_node_path64.default.join(logDir, "ticket-mcp-server.log");
59566
+ const ticketLogPath = import_node_path63.default.join(logDir, "ticket-mcp-server.log");
59753
59567
  const ticketCfgPath = writeTicketMcpConfig({
59754
59568
  dataDir: config.dataDir,
59755
59569
  serverScriptPath: ticketServerScriptPath,
@@ -59769,12 +59583,12 @@ async function startDaemon(config) {
59769
59583
  });
59770
59584
  }
59771
59585
  const shiftServerCandidates = [
59772
- import_node_path64.default.join(here, "shift", "mcp-server.cjs"),
59773
- import_node_path64.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
59586
+ import_node_path63.default.join(here, "shift", "mcp-server.cjs"),
59587
+ import_node_path63.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
59774
59588
  ];
59775
- const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59589
+ const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59776
59590
  if (shiftServerScriptPath) {
59777
- const shiftLogPath = import_node_path64.default.join(logDir, "shift-mcp-server.log");
59591
+ const shiftLogPath = import_node_path63.default.join(logDir, "shift-mcp-server.log");
59778
59592
  const shiftCfgPath = await writeShiftMcpConfig({
59779
59593
  dataDir: config.dataDir,
59780
59594
  serverScriptPath: shiftServerScriptPath,
@@ -59793,12 +59607,12 @@ async function startDaemon(config) {
59793
59607
  );
59794
59608
  }
59795
59609
  const inboxServerCandidates = [
59796
- import_node_path64.default.join(here, "inbox", "mcp-server.cjs"),
59797
- import_node_path64.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
59610
+ import_node_path63.default.join(here, "inbox", "mcp-server.cjs"),
59611
+ import_node_path63.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
59798
59612
  ];
59799
- const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs52.default.existsSync(p2));
59613
+ const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs51.default.existsSync(p2));
59800
59614
  if (inboxServerScriptPath) {
59801
- const inboxLogPath = import_node_path64.default.join(logDir, "inbox-mcp-server.log");
59615
+ const inboxLogPath = import_node_path63.default.join(logDir, "inbox-mcp-server.log");
59802
59616
  const inboxCfgPath = await writeInboxMcpConfig({
59803
59617
  dataDir: config.dataDir,
59804
59618
  serverScriptPath: inboxServerScriptPath,
@@ -59817,7 +59631,7 @@ async function startDaemon(config) {
59817
59631
  );
59818
59632
  }
59819
59633
  const shiftStore = createShiftStore({
59820
- filePath: import_node_path64.default.join(config.dataDir, "shift.json"),
59634
+ filePath: import_node_path63.default.join(config.dataDir, "shift.json"),
59821
59635
  ownerIdProvider: () => ownerPrincipalId,
59822
59636
  now: () => Date.now()
59823
59637
  });
@@ -59839,7 +59653,7 @@ async function startDaemon(config) {
59839
59653
  getAdapter,
59840
59654
  historyReader: history,
59841
59655
  dataDir: config.dataDir,
59842
- personaRoot: import_node_path64.default.join(config.dataDir, "personas"),
59656
+ personaRoot: import_node_path63.default.join(config.dataDir, "personas"),
59843
59657
  usersRoot,
59844
59658
  personaStore,
59845
59659
  ownerDisplayName,
@@ -59877,10 +59691,10 @@ async function startDaemon(config) {
59877
59691
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
59878
59692
  attachmentGroup: {
59879
59693
  onFileEdit: (input) => {
59880
- const absPath = import_node_path64.default.isAbsolute(input.relPath) ? input.relPath : import_node_path64.default.join(input.cwd, input.relPath);
59694
+ const absPath = import_node_path63.default.isAbsolute(input.relPath) ? input.relPath : import_node_path63.default.join(input.cwd, input.relPath);
59881
59695
  let size = 0;
59882
59696
  try {
59883
- size = import_node_fs52.default.statSync(absPath).size;
59697
+ size = import_node_fs51.default.statSync(absPath).size;
59884
59698
  } catch (err) {
59885
59699
  logger.warn("attachment.onFileEdit stat failed", {
59886
59700
  sessionId: input.sessionId,
@@ -60067,6 +59881,13 @@ async function startDaemon(config) {
60067
59881
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
60068
59882
  );
60069
59883
  const sshLog = createContactSshLog(config.dataDir);
59884
+ try {
59885
+ await ensureSshIdentity(config.dataDir);
59886
+ } catch (e) {
59887
+ logger.warn("ssh-identity generate failed; contact SSH pubkey exchange disabled", {
59888
+ err: e.message
59889
+ });
59890
+ }
60070
59891
  const makeHandlers = () => buildMethodHandlers({
60071
59892
  manager,
60072
59893
  workspace,
@@ -60110,11 +59931,11 @@ async function startDaemon(config) {
60110
59931
  // 'persona/<pid>/owner',default 走 'default'。
60111
59932
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
60112
59933
  // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
60113
- personaRoot: import_node_path64.default.join(config.dataDir, "personas"),
59934
+ personaRoot: import_node_path63.default.join(config.dataDir, "personas"),
60114
59935
  usersRoot
60115
59936
  },
60116
59937
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
60117
- personaRoot: import_node_path64.default.join(config.dataDir, "personas"),
59938
+ personaRoot: import_node_path63.default.join(config.dataDir, "personas"),
60118
59939
  // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
60119
59940
  usersRoot,
60120
59941
  // capability:list / delete handler 依赖
@@ -60127,6 +59948,9 @@ async function startDaemon(config) {
60127
59948
  ownerFeishuUnionId,
60128
59949
  ownerId,
60129
59950
  ownerProvider,
59951
+ // ssh identity 互换(spec 2026-07-15):whoami:ok 下发本机 identity pubkey。
59952
+ // 惰性读盘,不依赖 ensureSshIdentity 完成时序(未生成 → null → 帧省略字段)。
59953
+ sshIdentityPublicKey: () => readSshIdentityPublicKey(config.dataDir),
60130
59954
  personaStore,
60131
59955
  // capability handler 也用 (capability:issue 走 registry / capability:list)
60132
59956
  capabilityRegistry,
@@ -60138,7 +59962,7 @@ async function startDaemon(config) {
60138
59962
  contactStore,
60139
59963
  // <dataDir>/sshd 绝对路径 —— contact-ssh handlers 用它拼 authorized_keys / keys/ 子路径
60140
59964
  // Task 10 会加 SshdManager 起 sshd;handlers wire 提前挂 sshdDir 让 typecheck 过
60141
- sshdDir: import_node_path64.default.join(config.dataDir, "sshd"),
59965
+ sshdDir: import_node_path63.default.join(config.dataDir, "sshd"),
60142
59966
  contactSshLog: sshLog,
60143
59967
  // inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
60144
59968
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
@@ -60206,7 +60030,11 @@ async function startDaemon(config) {
60206
60030
  // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
60207
60031
  // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
60208
60032
  // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
60209
- selfUrl: () => currentTunnelUrl
60033
+ selfUrl: () => currentTunnelUrl,
60034
+ // ssh identity 互换(spec 2026-07-15):出站 auth 帧自报本机 identity pubkey;
60035
+ // 对方 pubkey 变化且已授权时立即重建 authorized_keys(重新 connect = 迁移路径)
60036
+ selfSshPublicKey: () => readSshIdentityPublicKey(config.dataDir),
60037
+ rebuildAuthorizedKeys: () => rebuildAuthorizedKeys(contactStore, import_node_path63.default.join(config.dataDir, "sshd"))
60210
60038
  },
60211
60039
  // app-builder
60212
60040
  appBuilderStore,
@@ -60229,11 +60057,11 @@ async function startDaemon(config) {
60229
60057
  // 发布上线脚手架化 (spec 2026-06-03 §5.2):
60230
60058
  // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
60231
60059
  // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
60232
- appBuilderPersonaRoot: import_node_path64.default.join(config.dataDir, "personas", "persona-app-builder"),
60060
+ appBuilderPersonaRoot: import_node_path63.default.join(config.dataDir, "personas", "persona-app-builder"),
60233
60061
  // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
60234
- deployKitRoot: import_node_path64.default.join(config.dataDir, "deploy-kit"),
60062
+ deployKitRoot: import_node_path63.default.join(config.dataDir, "deploy-kit"),
60235
60063
  // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
60236
- resolvePersonaRoot: (personaId) => import_node_path64.default.join(config.dataDir, "personas", personaId),
60064
+ resolvePersonaRoot: (personaId) => import_node_path63.default.join(config.dataDir, "personas", personaId),
60237
60065
  // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
60238
60066
  // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
60239
60067
  // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
@@ -60276,7 +60104,7 @@ async function startDaemon(config) {
60276
60104
  }
60277
60105
  let sourceJsonlPath = "(no transcript yet \u2014 operate from the task description alone)";
60278
60106
  if (sourceFile && sourceFile.toolSessionId) {
60279
- sourceJsonlPath = import_node_path64.default.join(
60107
+ sourceJsonlPath = import_node_path63.default.join(
60280
60108
  import_node_os21.default.homedir(),
60281
60109
  ".claude",
60282
60110
  "projects",
@@ -60636,8 +60464,8 @@ async function startDaemon(config) {
60636
60464
  const lines = [
60637
60465
  `Tunnel: ${r.url}`,
60638
60466
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
60639
- `Frpc config: ${import_node_path64.default.join(config.dataDir, "frpc.toml")}`,
60640
- `Frpc log: ${import_node_path64.default.join(logDir, "frpc.log")}`
60467
+ `Frpc config: ${import_node_path63.default.join(config.dataDir, "frpc.toml")}`,
60468
+ `Frpc log: ${import_node_path63.default.join(logDir, "frpc.log")}`
60641
60469
  ];
60642
60470
  const width = Math.max(...lines.map((l) => l.length));
60643
60471
  const bar = "\u2550".repeat(width + 4);
@@ -60650,8 +60478,8 @@ ${bar}
60650
60478
 
60651
60479
  `);
60652
60480
  try {
60653
- const connectPath = import_node_path64.default.join(config.dataDir, "connect.txt");
60654
- import_node_fs52.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
60481
+ const connectPath = import_node_path63.default.join(config.dataDir, "connect.txt");
60482
+ import_node_fs51.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
60655
60483
  } catch {
60656
60484
  }
60657
60485
  } catch (err) {
@@ -60687,21 +60515,13 @@ ${bar}
60687
60515
  });
60688
60516
  try {
60689
60517
  await sshdMgr.start();
60690
- rebuildAuthorizedKeys(contactStore, import_node_path64.default.join(config.dataDir, "sshd"));
60518
+ rebuildAuthorizedKeys(contactStore, import_node_path63.default.join(config.dataDir, "sshd"));
60691
60519
  logger.info("sshd: contact-ssh sandbox ready", { port: config.sshdPort });
60692
60520
  } catch (err) {
60693
60521
  logger.warn("sshd start failed; contact SSH grant will not work until fixed", {
60694
60522
  err: err.message
60695
60523
  });
60696
60524
  }
60697
- const contactKeyPuller = new ContactKeyPuller({
60698
- store: contactStore,
60699
- dataDir: config.dataDir,
60700
- logger,
60701
- intervalMs: 6e4,
60702
- sshLog
60703
- });
60704
- void contactKeyPuller.start();
60705
60525
  void reportDevice();
60706
60526
  void fetchServerKey();
60707
60527
  const tickAttachmentGc = () => {
@@ -60736,7 +60556,6 @@ ${bar}
60736
60556
  if (tunnelMgr) {
60737
60557
  await tunnelMgr.stop();
60738
60558
  }
60739
- contactKeyPuller.stop();
60740
60559
  await sshdMgr.stop().catch((err) => {
60741
60560
  logger.warn("shutdown.sshd-stop-failed", {
60742
60561
  error: err instanceof Error ? err.message : String(err)
@@ -60755,9 +60574,9 @@ ${bar}
60755
60574
  };
60756
60575
  }
60757
60576
  function migrateDropPersonsDir(dataDir) {
60758
- const dir = import_node_path64.default.join(dataDir, "persons");
60577
+ const dir = import_node_path63.default.join(dataDir, "persons");
60759
60578
  try {
60760
- import_node_fs52.default.rmSync(dir, { recursive: true, force: true });
60579
+ import_node_fs51.default.rmSync(dir, { recursive: true, force: true });
60761
60580
  } catch {
60762
60581
  }
60763
60582
  }