@clawos-dev/clawd 0.2.199-beta.399.3cb813c → 0.2.199-beta.400.ba99f40

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
@@ -158,6 +158,11 @@ var init_methods = __esm({
158
158
  "contact:list",
159
159
  "contact:pin",
160
160
  "contact:remove",
161
+ // ---- contact:setSshAccess / contact:sshKey:issue (SSH 反向访问,PR: contact-ssh-sandbox) ----
162
+ // owner UI 授权 contact 通过 daemon 专用 sshd 反向 SSH 进本机;guest daemon 用 sshKey:issue 拉 privkey.
163
+ // 详见 protocol/src/contact-ssh.ts.
164
+ "contact:setSshAccess",
165
+ "contact:sshKey:issue",
161
166
  // ---- visitor:* (web 访客 · persona web 分享,spec 2026-06-24-persona-web-share-design) ----
162
167
  // owner-only:列出登录访问过本机 public persona 的 web 访客(零安装、飞书登录、daemon 自签
163
168
  // visitor token)。存储 ~/.clawd/visitors.json(VisitorStore),登录(exchange)即 upsert;
@@ -737,8 +742,8 @@ var init_parseUtil = __esm({
737
742
  init_errors2();
738
743
  init_en();
739
744
  makeIssue = (params) => {
740
- const { data, path: path68, errorMaps, issueData } = params;
741
- const fullPath = [...path68, ...issueData.path || []];
745
+ const { data, path: path73, errorMaps, issueData } = params;
746
+ const fullPath = [...path73, ...issueData.path || []];
742
747
  const fullIssue = {
743
748
  ...issueData,
744
749
  path: fullPath
@@ -1049,11 +1054,11 @@ var init_types = __esm({
1049
1054
  init_parseUtil();
1050
1055
  init_util();
1051
1056
  ParseInputLazyPath = class {
1052
- constructor(parent, value, path68, key) {
1057
+ constructor(parent, value, path73, key) {
1053
1058
  this._cachedPath = [];
1054
1059
  this.parent = parent;
1055
1060
  this.data = value;
1056
- this._path = path68;
1061
+ this._path = path73;
1057
1062
  this._key = key;
1058
1063
  }
1059
1064
  get path() {
@@ -5678,7 +5683,19 @@ var init_contact = __esm({
5678
5683
  * 老 contacts.json 缺此字段 → zod default 补 null(无破坏性升级;不 orphan)。
5679
5684
  * 对齐 SessionFile.pinnedAt 语义(daemon 侧持久化,contact:pin RPC 更新 → contact:pinned push)。
5680
5685
  */
5681
- pinnedAt: external_exports.number().int().nullable().default(null)
5686
+ pinnedAt: external_exports.number().int().nullable().default(null),
5687
+ /**
5688
+ * SSH 反向访问授权(PR: contact-ssh-sandbox)。owner 视角:允许该 contact 通过 daemon
5689
+ * 专用 sshd 反向 SSH 进本机。default=false 保证老 contacts.json 兼容(zod default 补齐)。
5690
+ * 授权粒度 per-people (per-contact):借用别人 persona 本来就带信任对方本人的前提;
5691
+ * 极端场景(区别对方多个 persona)留 per-persona override 演进空间。
5692
+ */
5693
+ sshAllowed: external_exports.boolean().default(false),
5694
+ /**
5695
+ * 授权访问的目录白名单(绝对路径)。空数组 + sshAllowed=true 意味 SSH 通道能拨到但无处可读,
5696
+ * jail 会拒绝 shell 启动。由 clawd-ssh-jail 运行时读,生成 sbpl / bwrap policy 白名单。
5697
+ */
5698
+ exposedDirs: external_exports.array(external_exports.string()).default([])
5682
5699
  }).strict();
5683
5700
  ContactWireSchema = ContactSchema;
5684
5701
  ContactRemoveArgsSchema = external_exports.object({
@@ -5717,6 +5734,40 @@ var init_contact = __esm({
5717
5734
  }
5718
5735
  });
5719
5736
 
5737
+ // ../protocol/src/contact-ssh.ts
5738
+ var ContactSetSshAccessArgsSchema, ContactSetSshAccessOkSchema, ContactSshKeyIssueArgsSchema, ContactSshKeyIssueOkSchema, ContactSshAccessUpdatedFrameSchema;
5739
+ var init_contact_ssh = __esm({
5740
+ "../protocol/src/contact-ssh.ts"() {
5741
+ "use strict";
5742
+ init_zod();
5743
+ ContactSetSshAccessArgsSchema = external_exports.object({
5744
+ deviceId: external_exports.string().min(1),
5745
+ sshAllowed: external_exports.boolean(),
5746
+ exposedDirs: external_exports.array(external_exports.string())
5747
+ }).strict();
5748
+ ContactSetSshAccessOkSchema = external_exports.object({
5749
+ type: external_exports.literal("contact:setSshAccess:ok"),
5750
+ deviceId: external_exports.string().min(1),
5751
+ sshAllowed: external_exports.boolean(),
5752
+ exposedDirs: external_exports.array(external_exports.string())
5753
+ }).strict();
5754
+ ContactSshKeyIssueArgsSchema = external_exports.object({
5755
+ deviceId: external_exports.string().min(1)
5756
+ }).strict();
5757
+ ContactSshKeyIssueOkSchema = external_exports.object({
5758
+ type: external_exports.literal("contact:sshKey:issue:ok"),
5759
+ privateKeyPem: external_exports.string().min(1),
5760
+ publicKeyLine: external_exports.string().min(1)
5761
+ }).strict();
5762
+ ContactSshAccessUpdatedFrameSchema = external_exports.object({
5763
+ type: external_exports.literal("contact:ssh-access-updated"),
5764
+ deviceId: external_exports.string().min(1),
5765
+ sshAllowed: external_exports.boolean(),
5766
+ exposedDirs: external_exports.array(external_exports.string())
5767
+ }).strict();
5768
+ }
5769
+ });
5770
+
5720
5771
  // ../protocol/src/extension.ts
5721
5772
  function isAllowedHostedUrl(raw) {
5722
5773
  let u;
@@ -6118,6 +6169,7 @@ var init_runtime = __esm({
6118
6169
  init_capability();
6119
6170
  init_inbox();
6120
6171
  init_contact();
6172
+ init_contact_ssh();
6121
6173
  init_extension();
6122
6174
  init_feishu_auth();
6123
6175
  init_dispatch();
@@ -6397,8 +6449,8 @@ var require_req = __commonJS({
6397
6449
  if (req.originalUrl) {
6398
6450
  _req.url = req.originalUrl;
6399
6451
  } else {
6400
- const path68 = req.path;
6401
- _req.url = typeof path68 === "string" ? path68 : req.url ? req.url.path || req.url : void 0;
6452
+ const path73 = req.path;
6453
+ _req.url = typeof path73 === "string" ? path73 : req.url ? req.url.path || req.url : void 0;
6402
6454
  }
6403
6455
  if (req.query) {
6404
6456
  _req.query = req.query;
@@ -6563,14 +6615,14 @@ var require_redact = __commonJS({
6563
6615
  }
6564
6616
  return obj;
6565
6617
  }
6566
- function parsePath(path68) {
6618
+ function parsePath(path73) {
6567
6619
  const parts = [];
6568
6620
  let current = "";
6569
6621
  let inBrackets = false;
6570
6622
  let inQuotes = false;
6571
6623
  let quoteChar = "";
6572
- for (let i = 0; i < path68.length; i++) {
6573
- const char = path68[i];
6624
+ for (let i = 0; i < path73.length; i++) {
6625
+ const char = path73[i];
6574
6626
  if (!inBrackets && char === ".") {
6575
6627
  if (current) {
6576
6628
  parts.push(current);
@@ -6701,10 +6753,10 @@ var require_redact = __commonJS({
6701
6753
  return current;
6702
6754
  }
6703
6755
  function redactPaths(obj, paths, censor, remove = false) {
6704
- for (const path68 of paths) {
6705
- const parts = parsePath(path68);
6756
+ for (const path73 of paths) {
6757
+ const parts = parsePath(path73);
6706
6758
  if (parts.includes("*")) {
6707
- redactWildcardPath(obj, parts, censor, path68, remove);
6759
+ redactWildcardPath(obj, parts, censor, path73, remove);
6708
6760
  } else {
6709
6761
  if (remove) {
6710
6762
  removeKey(obj, parts);
@@ -6789,8 +6841,8 @@ var require_redact = __commonJS({
6789
6841
  }
6790
6842
  } else {
6791
6843
  if (afterWildcard.includes("*")) {
6792
- const wrappedCensor = typeof censor === "function" ? (value, path68) => {
6793
- const fullPath = [...pathArray.slice(0, pathLength), ...path68];
6844
+ const wrappedCensor = typeof censor === "function" ? (value, path73) => {
6845
+ const fullPath = [...pathArray.slice(0, pathLength), ...path73];
6794
6846
  return censor(value, fullPath);
6795
6847
  } : censor;
6796
6848
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6825,8 +6877,8 @@ var require_redact = __commonJS({
6825
6877
  return null;
6826
6878
  }
6827
6879
  const pathStructure = /* @__PURE__ */ new Map();
6828
- for (const path68 of pathsToClone) {
6829
- const parts = parsePath(path68);
6880
+ for (const path73 of pathsToClone) {
6881
+ const parts = parsePath(path73);
6830
6882
  let current = pathStructure;
6831
6883
  for (let i = 0; i < parts.length; i++) {
6832
6884
  const part = parts[i];
@@ -6878,24 +6930,24 @@ var require_redact = __commonJS({
6878
6930
  }
6879
6931
  return cloneSelectively(obj, pathStructure);
6880
6932
  }
6881
- function validatePath(path68) {
6882
- if (typeof path68 !== "string") {
6933
+ function validatePath(path73) {
6934
+ if (typeof path73 !== "string") {
6883
6935
  throw new Error("Paths must be (non-empty) strings");
6884
6936
  }
6885
- if (path68 === "") {
6937
+ if (path73 === "") {
6886
6938
  throw new Error("Invalid redaction path ()");
6887
6939
  }
6888
- if (path68.includes("..")) {
6889
- throw new Error(`Invalid redaction path (${path68})`);
6940
+ if (path73.includes("..")) {
6941
+ throw new Error(`Invalid redaction path (${path73})`);
6890
6942
  }
6891
- if (path68.includes(",")) {
6892
- throw new Error(`Invalid redaction path (${path68})`);
6943
+ if (path73.includes(",")) {
6944
+ throw new Error(`Invalid redaction path (${path73})`);
6893
6945
  }
6894
6946
  let bracketCount = 0;
6895
6947
  let inQuotes = false;
6896
6948
  let quoteChar = "";
6897
- for (let i = 0; i < path68.length; i++) {
6898
- const char = path68[i];
6949
+ for (let i = 0; i < path73.length; i++) {
6950
+ const char = path73[i];
6899
6951
  if ((char === '"' || char === "'") && bracketCount > 0) {
6900
6952
  if (!inQuotes) {
6901
6953
  inQuotes = true;
@@ -6909,20 +6961,20 @@ var require_redact = __commonJS({
6909
6961
  } else if (char === "]" && !inQuotes) {
6910
6962
  bracketCount--;
6911
6963
  if (bracketCount < 0) {
6912
- throw new Error(`Invalid redaction path (${path68})`);
6964
+ throw new Error(`Invalid redaction path (${path73})`);
6913
6965
  }
6914
6966
  }
6915
6967
  }
6916
6968
  if (bracketCount !== 0) {
6917
- throw new Error(`Invalid redaction path (${path68})`);
6969
+ throw new Error(`Invalid redaction path (${path73})`);
6918
6970
  }
6919
6971
  }
6920
6972
  function validatePaths(paths) {
6921
6973
  if (!Array.isArray(paths)) {
6922
6974
  throw new TypeError("paths must be an array");
6923
6975
  }
6924
- for (const path68 of paths) {
6925
- validatePath(path68);
6976
+ for (const path73 of paths) {
6977
+ validatePath(path73);
6926
6978
  }
6927
6979
  }
6928
6980
  function slowRedact(options = {}) {
@@ -7090,8 +7142,8 @@ var require_redaction = __commonJS({
7090
7142
  if (shape[k2] === null) {
7091
7143
  o[k2] = (value) => topCensor(value, [k2]);
7092
7144
  } else {
7093
- const wrappedCensor = typeof censor === "function" ? (value, path68) => {
7094
- return censor(value, [k2, ...path68]);
7145
+ const wrappedCensor = typeof censor === "function" ? (value, path73) => {
7146
+ return censor(value, [k2, ...path73]);
7095
7147
  } : censor;
7096
7148
  o[k2] = Redact({
7097
7149
  paths: shape[k2],
@@ -7309,10 +7361,10 @@ var require_atomic_sleep = __commonJS({
7309
7361
  var require_sonic_boom = __commonJS({
7310
7362
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7311
7363
  "use strict";
7312
- var fs61 = require("fs");
7364
+ var fs66 = require("fs");
7313
7365
  var EventEmitter3 = require("events");
7314
7366
  var inherits = require("util").inherits;
7315
- var path68 = require("path");
7367
+ var path73 = require("path");
7316
7368
  var sleep2 = require_atomic_sleep();
7317
7369
  var assert = require("assert");
7318
7370
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7366,20 +7418,20 @@ var require_sonic_boom = __commonJS({
7366
7418
  const mode = sonic.mode;
7367
7419
  if (sonic.sync) {
7368
7420
  try {
7369
- if (sonic.mkdir) fs61.mkdirSync(path68.dirname(file), { recursive: true });
7370
- const fd = fs61.openSync(file, flags, mode);
7421
+ if (sonic.mkdir) fs66.mkdirSync(path73.dirname(file), { recursive: true });
7422
+ const fd = fs66.openSync(file, flags, mode);
7371
7423
  fileOpened(null, fd);
7372
7424
  } catch (err) {
7373
7425
  fileOpened(err);
7374
7426
  throw err;
7375
7427
  }
7376
7428
  } else if (sonic.mkdir) {
7377
- fs61.mkdir(path68.dirname(file), { recursive: true }, (err) => {
7429
+ fs66.mkdir(path73.dirname(file), { recursive: true }, (err) => {
7378
7430
  if (err) return fileOpened(err);
7379
- fs61.open(file, flags, mode, fileOpened);
7431
+ fs66.open(file, flags, mode, fileOpened);
7380
7432
  });
7381
7433
  } else {
7382
- fs61.open(file, flags, mode, fileOpened);
7434
+ fs66.open(file, flags, mode, fileOpened);
7383
7435
  }
7384
7436
  }
7385
7437
  function SonicBoom(opts) {
@@ -7420,8 +7472,8 @@ var require_sonic_boom = __commonJS({
7420
7472
  this.flush = flushBuffer;
7421
7473
  this.flushSync = flushBufferSync;
7422
7474
  this._actualWrite = actualWriteBuffer;
7423
- fsWriteSync = () => fs61.writeSync(this.fd, this._writingBuf);
7424
- fsWrite = () => fs61.write(this.fd, this._writingBuf, this.release);
7475
+ fsWriteSync = () => fs66.writeSync(this.fd, this._writingBuf);
7476
+ fsWrite = () => fs66.write(this.fd, this._writingBuf, this.release);
7425
7477
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7426
7478
  this._writingBuf = "";
7427
7479
  this.write = write;
@@ -7430,15 +7482,15 @@ var require_sonic_boom = __commonJS({
7430
7482
  this._actualWrite = actualWrite;
7431
7483
  fsWriteSync = () => {
7432
7484
  if (Buffer.isBuffer(this._writingBuf)) {
7433
- return fs61.writeSync(this.fd, this._writingBuf);
7485
+ return fs66.writeSync(this.fd, this._writingBuf);
7434
7486
  }
7435
- return fs61.writeSync(this.fd, this._writingBuf, "utf8");
7487
+ return fs66.writeSync(this.fd, this._writingBuf, "utf8");
7436
7488
  };
7437
7489
  fsWrite = () => {
7438
7490
  if (Buffer.isBuffer(this._writingBuf)) {
7439
- return fs61.write(this.fd, this._writingBuf, this.release);
7491
+ return fs66.write(this.fd, this._writingBuf, this.release);
7440
7492
  }
7441
- return fs61.write(this.fd, this._writingBuf, "utf8", this.release);
7493
+ return fs66.write(this.fd, this._writingBuf, "utf8", this.release);
7442
7494
  };
7443
7495
  } else {
7444
7496
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7495,7 +7547,7 @@ var require_sonic_boom = __commonJS({
7495
7547
  }
7496
7548
  }
7497
7549
  if (this._fsync) {
7498
- fs61.fsyncSync(this.fd);
7550
+ fs66.fsyncSync(this.fd);
7499
7551
  }
7500
7552
  const len = this._len;
7501
7553
  if (this._reopening) {
@@ -7609,7 +7661,7 @@ var require_sonic_boom = __commonJS({
7609
7661
  const onDrain = () => {
7610
7662
  if (!this._fsync) {
7611
7663
  try {
7612
- fs61.fsync(this.fd, (err) => {
7664
+ fs66.fsync(this.fd, (err) => {
7613
7665
  this._flushPending = false;
7614
7666
  cb(err);
7615
7667
  });
@@ -7711,7 +7763,7 @@ var require_sonic_boom = __commonJS({
7711
7763
  const fd = this.fd;
7712
7764
  this.once("ready", () => {
7713
7765
  if (fd !== this.fd) {
7714
- fs61.close(fd, (err) => {
7766
+ fs66.close(fd, (err) => {
7715
7767
  if (err) {
7716
7768
  return this.emit("error", err);
7717
7769
  }
@@ -7760,7 +7812,7 @@ var require_sonic_boom = __commonJS({
7760
7812
  buf = this._bufs[0];
7761
7813
  }
7762
7814
  try {
7763
- const n = Buffer.isBuffer(buf) ? fs61.writeSync(this.fd, buf) : fs61.writeSync(this.fd, buf, "utf8");
7815
+ const n = Buffer.isBuffer(buf) ? fs66.writeSync(this.fd, buf) : fs66.writeSync(this.fd, buf, "utf8");
7764
7816
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7765
7817
  buf = releasedBufObj.writingBuf;
7766
7818
  this._len = releasedBufObj.len;
@@ -7776,7 +7828,7 @@ var require_sonic_boom = __commonJS({
7776
7828
  }
7777
7829
  }
7778
7830
  try {
7779
- fs61.fsyncSync(this.fd);
7831
+ fs66.fsyncSync(this.fd);
7780
7832
  } catch {
7781
7833
  }
7782
7834
  }
@@ -7797,7 +7849,7 @@ var require_sonic_boom = __commonJS({
7797
7849
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7798
7850
  }
7799
7851
  try {
7800
- const n = fs61.writeSync(this.fd, buf);
7852
+ const n = fs66.writeSync(this.fd, buf);
7801
7853
  buf = buf.subarray(n);
7802
7854
  this._len = Math.max(this._len - n, 0);
7803
7855
  if (buf.length <= 0) {
@@ -7825,13 +7877,13 @@ var require_sonic_boom = __commonJS({
7825
7877
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7826
7878
  if (this.sync) {
7827
7879
  try {
7828
- const written = Buffer.isBuffer(this._writingBuf) ? fs61.writeSync(this.fd, this._writingBuf) : fs61.writeSync(this.fd, this._writingBuf, "utf8");
7880
+ const written = Buffer.isBuffer(this._writingBuf) ? fs66.writeSync(this.fd, this._writingBuf) : fs66.writeSync(this.fd, this._writingBuf, "utf8");
7829
7881
  release(null, written);
7830
7882
  } catch (err) {
7831
7883
  release(err);
7832
7884
  }
7833
7885
  } else {
7834
- fs61.write(this.fd, this._writingBuf, release);
7886
+ fs66.write(this.fd, this._writingBuf, release);
7835
7887
  }
7836
7888
  }
7837
7889
  function actualWriteBuffer() {
@@ -7840,7 +7892,7 @@ var require_sonic_boom = __commonJS({
7840
7892
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7841
7893
  if (this.sync) {
7842
7894
  try {
7843
- const written = fs61.writeSync(this.fd, this._writingBuf);
7895
+ const written = fs66.writeSync(this.fd, this._writingBuf);
7844
7896
  release(null, written);
7845
7897
  } catch (err) {
7846
7898
  release(err);
@@ -7849,7 +7901,7 @@ var require_sonic_boom = __commonJS({
7849
7901
  if (kCopyBuffer) {
7850
7902
  this._writingBuf = Buffer.from(this._writingBuf);
7851
7903
  }
7852
- fs61.write(this.fd, this._writingBuf, release);
7904
+ fs66.write(this.fd, this._writingBuf, release);
7853
7905
  }
7854
7906
  }
7855
7907
  function actualClose(sonic) {
@@ -7865,12 +7917,12 @@ var require_sonic_boom = __commonJS({
7865
7917
  sonic._lens = [];
7866
7918
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7867
7919
  try {
7868
- fs61.fsync(sonic.fd, closeWrapped);
7920
+ fs66.fsync(sonic.fd, closeWrapped);
7869
7921
  } catch {
7870
7922
  }
7871
7923
  function closeWrapped() {
7872
7924
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7873
- fs61.close(sonic.fd, done);
7925
+ fs66.close(sonic.fd, done);
7874
7926
  } else {
7875
7927
  done();
7876
7928
  }
@@ -11005,11 +11057,11 @@ var init_lib = __esm({
11005
11057
  }
11006
11058
  }
11007
11059
  },
11008
- addToPath: function addToPath(path68, added, removed, oldPosInc, options) {
11009
- var last = path68.lastComponent;
11060
+ addToPath: function addToPath(path73, added, removed, oldPosInc, options) {
11061
+ var last = path73.lastComponent;
11010
11062
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
11011
11063
  return {
11012
- oldPos: path68.oldPos + oldPosInc,
11064
+ oldPos: path73.oldPos + oldPosInc,
11013
11065
  lastComponent: {
11014
11066
  count: last.count + 1,
11015
11067
  added,
@@ -11019,7 +11071,7 @@ var init_lib = __esm({
11019
11071
  };
11020
11072
  } else {
11021
11073
  return {
11022
- oldPos: path68.oldPos + oldPosInc,
11074
+ oldPos: path73.oldPos + oldPosInc,
11023
11075
  lastComponent: {
11024
11076
  count: 1,
11025
11077
  added,
@@ -11485,10 +11537,10 @@ function attachmentToHistoryMessage(o, ts) {
11485
11537
  const memories = raw.map((m2) => {
11486
11538
  if (!m2 || typeof m2 !== "object") return null;
11487
11539
  const rec3 = m2;
11488
- const path68 = typeof rec3.path === "string" ? rec3.path : null;
11540
+ const path73 = typeof rec3.path === "string" ? rec3.path : null;
11489
11541
  const content = typeof rec3.content === "string" ? rec3.content : null;
11490
- if (!path68 || content == null) return null;
11491
- const entry = { path: path68, content };
11542
+ if (!path73 || content == null) return null;
11543
+ const entry = { path: path73, content };
11492
11544
  if (typeof rec3.mtimeMs === "number") entry.mtimeMs = rec3.mtimeMs;
11493
11545
  return entry;
11494
11546
  }).filter((m2) => m2 !== null);
@@ -12300,10 +12352,10 @@ function parseAttachment(obj) {
12300
12352
  const memories = raw.map((m2) => {
12301
12353
  if (!m2 || typeof m2 !== "object") return null;
12302
12354
  const rec3 = m2;
12303
- const path68 = typeof rec3.path === "string" ? rec3.path : null;
12355
+ const path73 = typeof rec3.path === "string" ? rec3.path : null;
12304
12356
  const content = typeof rec3.content === "string" ? rec3.content : null;
12305
- if (!path68 || content == null) return null;
12306
- const out = { path: path68, content };
12357
+ if (!path73 || content == null) return null;
12358
+ const out = { path: path73, content };
12307
12359
  if (typeof rec3.mtimeMs === "number") out.mtimeMs = rec3.mtimeMs;
12308
12360
  return out;
12309
12361
  }).filter((m2) => m2 !== null);
@@ -28978,7 +29030,7 @@ var require_websocket = __commonJS({
28978
29030
  var http3 = require("http");
28979
29031
  var net3 = require("net");
28980
29032
  var tls = require("tls");
28981
- var { randomBytes, createHash: createHash3 } = require("crypto");
29033
+ var { randomBytes, createHash: createHash2 } = require("crypto");
28982
29034
  var { Duplex, Readable: Readable3 } = require("stream");
28983
29035
  var { URL: URL2 } = require("url");
28984
29036
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -29638,7 +29690,7 @@ var require_websocket = __commonJS({
29638
29690
  abortHandshake(websocket, socket, "Invalid Upgrade header");
29639
29691
  return;
29640
29692
  }
29641
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
29693
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
29642
29694
  if (res.headers["sec-websocket-accept"] !== digest) {
29643
29695
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
29644
29696
  return;
@@ -30005,7 +30057,7 @@ var require_websocket_server = __commonJS({
30005
30057
  var EventEmitter3 = require("events");
30006
30058
  var http3 = require("http");
30007
30059
  var { Duplex } = require("stream");
30008
- var { createHash: createHash3 } = require("crypto");
30060
+ var { createHash: createHash2 } = require("crypto");
30009
30061
  var extension2 = require_extension();
30010
30062
  var PerMessageDeflate2 = require_permessage_deflate();
30011
30063
  var subprotocol2 = require_subprotocol();
@@ -30306,7 +30358,7 @@ var require_websocket_server = __commonJS({
30306
30358
  );
30307
30359
  }
30308
30360
  if (this._state > RUNNING) return abortHandshake(socket, 503);
30309
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
30361
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
30310
30362
  const headers = [
30311
30363
  "HTTP/1.1 101 Switching Protocols",
30312
30364
  "Upgrade: websocket",
@@ -33268,8 +33320,8 @@ var require_utils = __commonJS({
33268
33320
  var result = transform[inputType][outputType](input);
33269
33321
  return result;
33270
33322
  };
33271
- exports2.resolve = function(path68) {
33272
- var parts = path68.split("/");
33323
+ exports2.resolve = function(path73) {
33324
+ var parts = path73.split("/");
33273
33325
  var result = [];
33274
33326
  for (var index = 0; index < parts.length; index++) {
33275
33327
  var part = parts[index];
@@ -39122,18 +39174,18 @@ var require_object = __commonJS({
39122
39174
  var object = new ZipObject(name, zipObjectContent, o);
39123
39175
  this.files[name] = object;
39124
39176
  };
39125
- var parentFolder = function(path68) {
39126
- if (path68.slice(-1) === "/") {
39127
- path68 = path68.substring(0, path68.length - 1);
39177
+ var parentFolder = function(path73) {
39178
+ if (path73.slice(-1) === "/") {
39179
+ path73 = path73.substring(0, path73.length - 1);
39128
39180
  }
39129
- var lastSlash = path68.lastIndexOf("/");
39130
- return lastSlash > 0 ? path68.substring(0, lastSlash) : "";
39181
+ var lastSlash = path73.lastIndexOf("/");
39182
+ return lastSlash > 0 ? path73.substring(0, lastSlash) : "";
39131
39183
  };
39132
- var forceTrailingSlash = function(path68) {
39133
- if (path68.slice(-1) !== "/") {
39134
- path68 += "/";
39184
+ var forceTrailingSlash = function(path73) {
39185
+ if (path73.slice(-1) !== "/") {
39186
+ path73 += "/";
39135
39187
  }
39136
- return path68;
39188
+ return path73;
39137
39189
  };
39138
39190
  var folderAdd = function(name, createFolders) {
39139
39191
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -40135,7 +40187,7 @@ var require_lib3 = __commonJS({
40135
40187
  // src/run-case/recorder.ts
40136
40188
  function startRunCaseRecorder(opts) {
40137
40189
  const now = opts.now ?? Date.now;
40138
- const dir = import_node_path56.default.dirname(opts.recordPath);
40190
+ const dir = import_node_path61.default.dirname(opts.recordPath);
40139
40191
  let stream = null;
40140
40192
  let closing = false;
40141
40193
  let closedSettled = false;
@@ -40149,8 +40201,8 @@ function startRunCaseRecorder(opts) {
40149
40201
  });
40150
40202
  const ensureStream = () => {
40151
40203
  if (stream) return stream;
40152
- import_node_fs43.default.mkdirSync(dir, { recursive: true });
40153
- stream = import_node_fs43.default.createWriteStream(opts.recordPath, { flags: "a" });
40204
+ import_node_fs48.default.mkdirSync(dir, { recursive: true });
40205
+ stream = import_node_fs48.default.createWriteStream(opts.recordPath, { flags: "a" });
40154
40206
  stream.on("close", () => closedResolve());
40155
40207
  return stream;
40156
40208
  };
@@ -40175,12 +40227,12 @@ function startRunCaseRecorder(opts) {
40175
40227
  };
40176
40228
  return { tap, close, closed };
40177
40229
  }
40178
- var import_node_fs43, import_node_path56;
40230
+ var import_node_fs48, import_node_path61;
40179
40231
  var init_recorder = __esm({
40180
40232
  "src/run-case/recorder.ts"() {
40181
40233
  "use strict";
40182
- import_node_fs43 = __toESM(require("fs"), 1);
40183
- import_node_path56 = __toESM(require("path"), 1);
40234
+ import_node_fs48 = __toESM(require("fs"), 1);
40235
+ import_node_path61 = __toESM(require("path"), 1);
40184
40236
  }
40185
40237
  });
40186
40238
 
@@ -40223,7 +40275,7 @@ var init_wire = __esm({
40223
40275
  // src/run-case/controller.ts
40224
40276
  async function runController(opts) {
40225
40277
  const now = opts.now ?? Date.now;
40226
- const cwd = opts.cwd ?? (0, import_node_fs44.mkdtempSync)(import_node_path57.default.join(import_node_os22.default.tmpdir(), "clawd-runcase-"));
40278
+ const cwd = opts.cwd ?? (0, import_node_fs49.mkdtempSync)(import_node_path62.default.join(import_node_os22.default.tmpdir(), "clawd-runcase-"));
40227
40279
  const ownsCwd = opts.cwd === void 0;
40228
40280
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
40229
40281
  const spawnCtx = { cwd };
@@ -40384,19 +40436,19 @@ async function runController(opts) {
40384
40436
  if (sigintHandler) process.off("SIGINT", sigintHandler);
40385
40437
  if (ownsCwd) {
40386
40438
  try {
40387
- (0, import_node_fs44.rmSync)(cwd, { recursive: true, force: true });
40439
+ (0, import_node_fs49.rmSync)(cwd, { recursive: true, force: true });
40388
40440
  } catch {
40389
40441
  }
40390
40442
  }
40391
40443
  return exitCode ?? 0;
40392
40444
  }
40393
- var import_node_fs44, import_node_os22, import_node_path57;
40445
+ var import_node_fs49, import_node_os22, import_node_path62;
40394
40446
  var init_controller = __esm({
40395
40447
  "src/run-case/controller.ts"() {
40396
40448
  "use strict";
40397
- import_node_fs44 = require("fs");
40449
+ import_node_fs49 = require("fs");
40398
40450
  import_node_os22 = __toESM(require("os"), 1);
40399
- import_node_path57 = __toESM(require("path"), 1);
40451
+ import_node_path62 = __toESM(require("path"), 1);
40400
40452
  init_claude();
40401
40453
  init_stdout_splitter();
40402
40454
  init_permission_stdio();
@@ -40499,6 +40551,7 @@ var import_node_path = __toESM(require("path"), 1);
40499
40551
  init_protocol();
40500
40552
  var DEFAULT_PORT = 18790;
40501
40553
  var DEFAULT_HOST = "127.0.0.1";
40554
+ var DEFAULT_SSHD_PORT = 22422;
40502
40555
  var DEFAULT_CLAWOS_API = "https://api.clawos.chat";
40503
40556
  var DEFAULT_LOG_ENDPOINT = "https://clawd-prod.cn-hangzhou.log.aliyuncs.com/logstores/app-logs/track";
40504
40557
  function resolveLogShipping(raw, cliNoShipping) {
@@ -40566,6 +40619,14 @@ function parseArgs(argv) {
40566
40619
  case "--no-log-shipping":
40567
40620
  out.noLogShipping = true;
40568
40621
  break;
40622
+ case "--sshd-port": {
40623
+ const n = Number.parseInt(next() ?? "", 10);
40624
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) {
40625
+ throw new Error(`invalid --sshd-port value: ${argv[i]}`);
40626
+ }
40627
+ out.sshdPort = n;
40628
+ break;
40629
+ }
40569
40630
  default:
40570
40631
  if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
40571
40632
  break;
@@ -40615,6 +40676,7 @@ function resolveConfig(opts) {
40615
40676
  fileCfg.logShipping,
40616
40677
  args.noLogShipping ?? false
40617
40678
  );
40679
+ const sshdPort = args.sshdPort ?? (typeof fileCfg.sshdPort === "number" ? fileCfg.sshdPort : DEFAULT_SSHD_PORT);
40618
40680
  return {
40619
40681
  port,
40620
40682
  host,
@@ -40628,7 +40690,8 @@ function resolveConfig(opts) {
40628
40690
  frpcBinary,
40629
40691
  mode,
40630
40692
  previewPorts,
40631
- logShipping
40693
+ logShipping,
40694
+ sshdPort
40632
40695
  };
40633
40696
  }
40634
40697
  var HELP_TEXT = `clawd [options]
@@ -40640,6 +40703,7 @@ var HELP_TEXT = `clawd [options]
40640
40703
  --clawos-api <url> tunnel register \u63A5\u53E3\u7684 base url\uFF08\u9ED8\u8BA4 https://api.clawos.chat\uFF09
40641
40704
  --auth-token <s> \u6307\u5B9A daemon auth token\uFF1B\u7F3A\u7701\u65F6 tunnel \u6A21\u5F0F\u4ECE ~/.clawd/auth.json \u590D\u7528\uFF0C\u6CA1\u6709\u5C31\u751F\u6210
40642
40705
  --no-log-shipping \u7981\u7528 SLS \u65E5\u5FD7\u4E0A\u884C\uFF08\u8986\u76D6 config.json logShipping.mode=off\uFF09
40706
+ --sshd-port <n> Contact SSH \u53CD\u5411\u8BBF\u95EE loopback \u7AEF\u53E3\uFF0C\u9ED8\u8BA4 22422
40643
40707
  --help / -h \u663E\u793A\u5E2E\u52A9
40644
40708
  --version / -v \u663E\u793A\u7248\u672C
40645
40709
 
@@ -40658,8 +40722,8 @@ Env (advanced):
40658
40722
  `;
40659
40723
 
40660
40724
  // src/index.ts
40661
- var import_node_path55 = __toESM(require("path"), 1);
40662
- var import_node_fs42 = __toESM(require("fs"), 1);
40725
+ var import_node_path60 = __toESM(require("path"), 1);
40726
+ var import_node_fs47 = __toESM(require("fs"), 1);
40663
40727
  var import_node_os21 = __toESM(require("os"), 1);
40664
40728
 
40665
40729
  // ../node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/stringify.js
@@ -40779,18 +40843,6 @@ function createLogger(opts = {}) {
40779
40843
  );
40780
40844
  return wrap(base);
40781
40845
  }
40782
- function createFileOnlyLogger(opts) {
40783
- const level = opts.level ?? "debug";
40784
- try {
40785
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(opts.file), { recursive: true });
40786
- } catch {
40787
- }
40788
- const base = (0, import_pino.default)(
40789
- { level, base: {} },
40790
- import_pino.default.destination({ dest: opts.file, mkdir: true, sync: true })
40791
- );
40792
- return wrap(base);
40793
- }
40794
40846
  function pinoLevelToString(n) {
40795
40847
  if (typeof n !== "number") return null;
40796
40848
  if (n >= 50) return "error";
@@ -42956,18 +43008,9 @@ var SessionManager = class {
42956
43008
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
42957
43009
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
42958
43010
  realUuidBySynth = /* @__PURE__ */ new Map();
42959
- // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s 挂进 pending,等 `notifyScreenIdle`
42960
- // (屏幕 armed=true 触发点)flush 成真正的 turn_end reducer。
42961
- //
42962
- // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
42963
- //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
42964
- // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
42965
- //
42966
- // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
42967
- // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
42968
- //
42969
- // 清理点:newSession / stop / session-delete / stopAll。
42970
- pendingTurnDurationSignals = /* @__PURE__ */ new Set();
43011
+ // observeScreenIdle 复合条件闸用:sessionIdobserver 上次喂出业务事件的时刻(deps.now())。
43012
+ // observerIdleWaitMs 据此判 observer 是否也静止 idleMs(屏幕静止 AND observer 静止才补 turn_end)。
43013
+ lastObserverEventAt = /* @__PURE__ */ new Map();
42971
43014
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
42972
43015
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
42973
43016
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43307,14 +43350,6 @@ var SessionManager = class {
43307
43350
  routeFromRunner(frame, target) {
43308
43351
  const compressed = compressFrameForWire(frame);
43309
43352
  if (!compressed) return;
43310
- if (compressed.type === "session:status") {
43311
- const s = compressed;
43312
- this.deps.screenIdleProbeLogger?.info("session:status wire emit", {
43313
- sessionId: s.sessionId,
43314
- status: s.status,
43315
- target
43316
- });
43317
- }
43318
43353
  if (compressed.type === "session:event" || compressed.type === "session:status") {
43319
43354
  const sid = compressed.sessionId;
43320
43355
  if (sid) {
@@ -43588,7 +43623,7 @@ var SessionManager = class {
43588
43623
  this.runners.delete(args.sessionId);
43589
43624
  this.realUuidBySynth.delete(args.sessionId);
43590
43625
  this.lastUiSizeBySessionId.delete(args.sessionId);
43591
- this.clearPendingTurnEnd(args.sessionId);
43626
+ this.lastObserverEventAt.delete(args.sessionId);
43592
43627
  return { response: { sessionId: args.sessionId }, broadcast };
43593
43628
  }
43594
43629
  this.deleteOwned(args.sessionId);
@@ -43618,7 +43653,6 @@ var SessionManager = class {
43618
43653
  async stop(args) {
43619
43654
  const runner = this.runners.get(args.sessionId);
43620
43655
  if (!runner) return { response: { ok: true }, broadcast: [] };
43621
- this.clearPendingTurnEnd(args.sessionId);
43622
43656
  const { broadcast } = this.withCollector(() => {
43623
43657
  runner.input({ kind: "command", command: { kind: "stop" } });
43624
43658
  });
@@ -43752,7 +43786,6 @@ var SessionManager = class {
43752
43786
  newSession(args) {
43753
43787
  const existingFile = this.getFile(args.sessionId);
43754
43788
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
43755
- this.clearPendingTurnEnd(args.sessionId);
43756
43789
  const runner = this.runners.get(args.sessionId);
43757
43790
  if (runner) {
43758
43791
  const { value, broadcast } = this.withCollector(() => {
@@ -43853,7 +43886,6 @@ var SessionManager = class {
43853
43886
  for (const r of this.runners.values()) {
43854
43887
  r.input({ kind: "command", command: { kind: "stop" } });
43855
43888
  }
43856
- this.pendingTurnDurationSignals.clear();
43857
43889
  }
43858
43890
  // 给 observer 用:拿已存在的 runner
43859
43891
  getActive(sessionId) {
@@ -44070,7 +44102,7 @@ var SessionManager = class {
44070
44102
  this.runners.delete(args.sessionId);
44071
44103
  this.realUuidBySynth.delete(args.sessionId);
44072
44104
  this.lastUiSizeBySessionId.delete(args.sessionId);
44073
- this.clearPendingTurnEnd(args.sessionId);
44105
+ this.lastObserverEventAt.delete(args.sessionId);
44074
44106
  return { response: { sessionId: args.sessionId }, broadcast };
44075
44107
  }
44076
44108
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44316,93 +44348,23 @@ var SessionManager = class {
44316
44348
  return;
44317
44349
  }
44318
44350
  }
44351
+ this.lastObserverEventAt.set(sessionId, (this.deps.now ?? Date.now)());
44319
44352
  let feedEvents = outEvents;
44320
44353
  if (outEvents.some((e) => e.kind === "turn_end")) {
44321
- const runnerState = runner.getState();
44322
- const toolSessionId = runnerState.file.toolSessionId;
44323
- const adapter = this.deps.getAdapter(runnerState.file.tool ?? "claude");
44324
- const gateOpen = !adapter.canAcceptTurnEnd || !toolSessionId ? true : adapter.canAcceptTurnEnd(toolSessionId);
44325
- this.deps.screenIdleProbeLogger?.info("turn_duration signal received", {
44326
- sessionId,
44327
- toolSessionId,
44328
- batchKinds: outEvents.map((e) => e.kind),
44329
- gateOpen,
44330
- hasCanAcceptGate: !!adapter.canAcceptTurnEnd
44331
- });
44332
- if (gateOpen) {
44333
- this.deps.screenIdleProbeLogger?.info(
44334
- "turn_duration \u2192 turn_end confirmed (gate open) \u2192 fed to reducer",
44335
- { sessionId, toolSessionId }
44336
- );
44337
- } else {
44354
+ const ev = this.peekTurnEvidence(runner);
44355
+ if (!ev.turnHasContent) {
44338
44356
  feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44339
- if (this.pendingTurnDurationSignals.has(sessionId)) {
44340
- this.deps.screenIdleProbeLogger?.info(
44341
- "turn_duration dedup: pending already set (repeated observer signal)",
44342
- { sessionId, toolSessionId }
44343
- );
44344
- } else {
44345
- this.pendingTurnDurationSignals.add(sessionId);
44346
- this.deps.screenIdleProbeLogger?.info(
44347
- "turn_duration pending: gate closed \u2192 waiting for screen-idle signal",
44348
- { sessionId, toolSessionId }
44349
- );
44350
- }
44357
+ this.deps.logger?.info("[TE-PROBE] drop spurious observer turn_end", {
44358
+ sessionId,
44359
+ src: "observer",
44360
+ ...ev,
44361
+ batchKinds: outEvents.map((e) => e.kind)
44362
+ });
44351
44363
  }
44352
44364
  }
44353
44365
  if (feedEvents.length === 0) return;
44354
44366
  runner.feedObserverEvents(feedEvents);
44355
44367
  }
44356
- /**
44357
- * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
44358
- *
44359
- * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
44360
- * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
44361
- * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
44362
- *
44363
- * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
44364
- * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
44365
- * turn_end" 语义不同。
44366
- *
44367
- * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
44368
- */
44369
- notifyScreenIdle(toolSessionId) {
44370
- if (this.deps.mode !== "tui") return;
44371
- const sid = this.sessionIdByToolSid(toolSessionId);
44372
- if (!sid) {
44373
- this.deps.screenIdleProbeLogger?.warn("notifyScreenIdle: no session for toolSessionId", {
44374
- toolSessionId
44375
- });
44376
- return;
44377
- }
44378
- if (!this.pendingTurnDurationSignals.has(sid)) {
44379
- this.deps.screenIdleProbeLogger?.info(
44380
- "notifyScreenIdle: no pending turn_duration \u2192 noop",
44381
- { sessionId: sid, toolSessionId }
44382
- );
44383
- return;
44384
- }
44385
- const runner = this.runners.get(sid);
44386
- if (!runner) {
44387
- this.pendingTurnDurationSignals.delete(sid);
44388
- this.deps.screenIdleProbeLogger?.warn(
44389
- "notifyScreenIdle: pending but no runner \u2192 cleared without inject",
44390
- { sessionId: sid, toolSessionId }
44391
- );
44392
- return;
44393
- }
44394
- this.pendingTurnDurationSignals.delete(sid);
44395
- this.deps.screenIdleProbeLogger?.info(
44396
- "notifyScreenIdle: pending turn_duration + screen idle confirmed \u2192 inject turn_end",
44397
- { sessionId: sid, toolSessionId }
44398
- );
44399
- runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44400
- }
44401
- clearPendingTurnEnd(sessionId) {
44402
- if (this.pendingTurnDurationSignals.delete(sessionId)) {
44403
- this.deps.screenIdleProbeLogger?.info("pending turn_duration cleared", { sessionId });
44404
- }
44405
- }
44406
44368
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44407
44369
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44408
44370
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -44661,6 +44623,70 @@ var SessionManager = class {
44661
44623
  if (!runner) return;
44662
44624
  runner.input({ kind: "ready-detected" });
44663
44625
  }
44626
+ /**
44627
+ * ClaudeTuiAdapter onTurnIdle callback:屏幕内容静止时**复发**本轮已出现过的权威 turn_end。
44628
+ * 本意:turn_duration 写盘早于尾段正文 → observer 把尾随 text 推进 buffer 盖掉 lastEventKind →
44629
+ * spinner 不熄;屏幕静止时再补一条 turn_end 排到尾随 text 之后。
44630
+ *
44631
+ * Fix A(修 bug1 "UI 还在变却显示结束" / bug2 "发消息后无 spinner"):补偿**只复发不 originate**——
44632
+ * 仅当本轮已出现过 turn_end(turnEndSeenThisTurn,即真有过尾随 text 覆盖场景)才补。turnEndSeenThisTurn
44633
+ * ===false 说明本轮 CC 从未结束过(仍在工作 / 刚发消息没产出 / 漏检弹框等用户),屏幕静止 ≠ turn 结束,
44634
+ * 凭空 inject turn_end 会误灭 spinner——故跳过,让真正的 turn_duration 来时再正常收。
44635
+ * 仅 tui 模式;runner 缺失 noop。turn_end 无 uuid 不参与 dedup。
44636
+ */
44637
+ dispatchTurnIdle(toolSessionId) {
44638
+ if (this.deps.mode !== "tui") return;
44639
+ const sid = this.sessionIdByToolSid(toolSessionId);
44640
+ const runner = sid ? this.runners.get(sid) : void 0;
44641
+ if (!runner) return;
44642
+ const ev = this.peekTurnEvidence(runner);
44643
+ const willInject = ev.turnEndSeenThisTurn;
44644
+ this.deps.logger?.info("[TE-PROBE] screen-idle compensation", {
44645
+ sessionId: sid,
44646
+ src: "screen-idle",
44647
+ willInject,
44648
+ ...ev
44649
+ });
44650
+ if (!willInject) return;
44651
+ runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44652
+ }
44653
+ /**
44654
+ * 读 runner 当前 turn 态快照,供两个 turn_end 注入源(屏幕静止补偿 / observer turn_duration)
44655
+ * 判断误发 + 打 [TE-PROBE] 日志。
44656
+ * turnEndSeenThisTurn:从 buffer 末尾回扫到最近 user_text 期间是否已出现过 turn_end
44657
+ * (已出现=本轮真结束过、合法尾随;未出现=本轮还没结束过)→ Fix A 复发闸。
44658
+ * turnHasContent:末条是否 assistant 产出(非 user_text/turn_end/空)→ Fix B 空 turn 守卫闸。
44659
+ */
44660
+ peekTurnEvidence(runner) {
44661
+ const st = runner.getState();
44662
+ const buf = st.buffer;
44663
+ const lastEventKindBefore = buf.length > 0 ? buf[buf.length - 1].event.kind : null;
44664
+ let turnEndSeenThisTurn = false;
44665
+ for (let i = buf.length - 1; i >= 0; i--) {
44666
+ const k2 = buf[i].event.kind;
44667
+ if (k2 === "user_text") break;
44668
+ if (k2 === "turn_end") {
44669
+ turnEndSeenThisTurn = true;
44670
+ break;
44671
+ }
44672
+ }
44673
+ const turnHasContent = lastEventKindBefore !== null && lastEventKindBefore !== "user_text" && lastEventKindBefore !== "turn_end";
44674
+ return { turnOpenBefore: st.turnOpen, lastEventKindBefore, turnEndSeenThisTurn, turnHasContent };
44675
+ }
44676
+ /**
44677
+ * observer 还需静止多久(ms)才满 idleMs,0 = 已满。observeScreenIdle 复合条件闸:屏幕静止后
44678
+ * 精确等这段剩余再补 turn_end —— turn_duration 写盘早于尾段正文,observer 把尾随 text poll 落盘
44679
+ * 期间屏幕可能已静止,仅看屏幕会早 fire(补的 turn_end 盖不到尾随 text 之后)。
44680
+ * 找不到 runner / 从无事件 → 0(不阻塞 fire)。idleMs 由装配处传 SCREEN_IDLE_MS。
44681
+ */
44682
+ observerIdleWaitMs(toolSessionId, idleMs) {
44683
+ const sid = this.sessionIdByToolSid(toolSessionId);
44684
+ if (!sid) return 0;
44685
+ const last = this.lastObserverEventAt.get(sid);
44686
+ if (last === void 0) return 0;
44687
+ const elapsed = (this.deps.now ?? Date.now)() - last;
44688
+ return Math.max(0, idleMs - elapsed);
44689
+ }
44664
44690
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
44665
44691
  sessionIdByToolSid(toolSessionId) {
44666
44692
  for (const [sid, runner] of this.runners) {
@@ -46170,8 +46196,8 @@ function turnStartInput(text) {
46170
46196
  const items = [];
46171
46197
  let leftover = text;
46172
46198
  for (const m2 of text.matchAll(SKILL_RE)) {
46173
- const [marker, name, path68] = m2;
46174
- items.push({ type: "skill", name, path: path68 });
46199
+ const [marker, name, path73] = m2;
46200
+ items.push({ type: "skill", name, path: path73 });
46175
46201
  leftover = leftover.replace(marker, "");
46176
46202
  }
46177
46203
  for (const m2 of text.matchAll(ATTACHMENT_RE2)) {
@@ -46403,7 +46429,6 @@ var CodexAdapter = class {
46403
46429
  };
46404
46430
 
46405
46431
  // src/tools/claude-tui.ts
46406
- var import_node_crypto5 = require("crypto");
46407
46432
  var import_node_fs16 = __toESM(require("fs"), 1);
46408
46433
  var import_node_os7 = __toESM(require("os"), 1);
46409
46434
  var import_node_path14 = __toESM(require("path"), 1);
@@ -47214,56 +47239,22 @@ function observeScreenIdle(surface, opts) {
47214
47239
  timer = null;
47215
47240
  if (disposed) return;
47216
47241
  if (opts.getPopupVisible()) {
47217
- opts.probeLogger?.info("screen-idle fire suppressed: popup visible", {
47218
- label: opts.probeLabel
47219
- });
47220
47242
  timer = setTimeout(fire, opts.idleMs);
47221
47243
  return;
47222
47244
  }
47223
47245
  const obsWait = opts.getObserverWaitMs?.() ?? 0;
47224
47246
  if (obsWait > 0) {
47225
- opts.probeLogger?.info("screen-idle fire suppressed: observer not idle", {
47226
- label: opts.probeLabel,
47227
- obsWait
47228
- });
47229
47247
  timer = setTimeout(fire, Math.max(obsWait, REWAIT_MIN_MS));
47230
47248
  return;
47231
47249
  }
47232
- if (armed) {
47233
- opts.probeLogger?.debug("screen-idle fire noop: already armed", {
47234
- label: opts.probeLabel
47235
- });
47236
- return;
47237
- }
47250
+ if (armed) return;
47238
47251
  armed = true;
47239
- opts.probeLogger?.info("screen-idle fire triggered \u2192 armed=true, calling onIdle", {
47240
- label: opts.probeLabel
47241
- });
47242
47252
  opts.onIdle();
47243
47253
  };
47244
47254
  const unsub = surface.onTick((lines) => {
47245
47255
  if (disposed) return;
47246
47256
  const snap = snapOf(lines);
47247
47257
  if (snap === lastSnap) return;
47248
- if (opts.probeLogger) {
47249
- const prev = lastSnap;
47250
- const meta = {
47251
- label: opts.probeLabel,
47252
- prevHash: prev === null ? null : shortHash(prev),
47253
- nextHash: shortHash(snap),
47254
- prevLen: prev?.length ?? 0,
47255
- nextLen: snap.length
47256
- };
47257
- if (prev !== null) {
47258
- const diff2 = firstLineDiff(prev, snap);
47259
- if (diff2) {
47260
- meta.diffRow = diff2.row;
47261
- meta.prevRow = diff2.prev;
47262
- meta.nextRow = diff2.next;
47263
- }
47264
- }
47265
- opts.probeLogger.info("screen-idle tick snap changed", meta);
47266
- }
47267
47258
  lastSnap = snap;
47268
47259
  armed = false;
47269
47260
  clear();
@@ -47274,38 +47265,9 @@ function observeScreenIdle(surface, opts) {
47274
47265
  disposed = true;
47275
47266
  unsub();
47276
47267
  clear();
47277
- },
47278
- isIdle() {
47279
- const popupVisible = opts.getPopupVisible();
47280
- const idle = armed && !popupVisible;
47281
- if (opts.probeLogger) {
47282
- opts.probeLogger.info("screen-idle isIdle check", {
47283
- label: opts.probeLabel,
47284
- idle,
47285
- armed,
47286
- popupVisible
47287
- });
47288
- }
47289
- return idle;
47290
47268
  }
47291
47269
  };
47292
47270
  }
47293
- function shortHash(s) {
47294
- return (0, import_node_crypto5.createHash)("sha1").update(s).digest("hex").slice(0, 8);
47295
- }
47296
- function firstLineDiff(prev, next) {
47297
- const p2 = prev.split("\n");
47298
- const n = next.split("\n");
47299
- const rows = Math.max(p2.length, n.length);
47300
- for (let i = 0; i < rows; i++) {
47301
- const pl = p2[i] ?? "";
47302
- const nl = n[i] ?? "";
47303
- if (pl !== nl) {
47304
- return { row: i, prev: pl.slice(0, 60), next: nl.slice(0, 60) };
47305
- }
47306
- }
47307
- return null;
47308
- }
47309
47271
  var BYPASS_SETTLE_MS = 300;
47310
47272
  var SCREEN_IDLE_MS = 5e3;
47311
47273
  function createBootGate(pty, logger) {
@@ -47380,42 +47342,11 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47380
47342
  // 用于 spawn / PtyChildProcess 链路打日志
47381
47343
  tuiLogger;
47382
47344
  tuiOpts;
47383
- /**
47384
- * per-toolSessionId 的 tui 观察者句柄,仅用于 turn_end gate 查询(`canAcceptTurnEnd`)。
47385
- * onIdle / onPopupTransition 等回调仍走原有闭包(不复用这份 map),本 map 只承担
47386
- * "manager 需要跨模块查屏幕/弹框状态"这单一职责。
47387
- */
47388
- tuiStates = /* @__PURE__ */ new Map();
47389
47345
  constructor(opts = {}) {
47390
47346
  super(opts);
47391
47347
  this.tuiLogger = opts.logger;
47392
47348
  this.tuiOpts = opts;
47393
47349
  }
47394
- /**
47395
- * TUI adapter 的 turn_end 权威判定:屏幕已 idle 且非弹框态才放行。
47396
- *
47397
- * `feedObserverEvents` 收到 observer 回灌 `turn_end` 时调用。屏幕仍在变(如后台 agent 在跑)
47398
- * 时 drop 掉 turn_end,避免 `system/turn_duration` JSONL 帧误触发 running-idle 状态转换。
47399
- *
47400
- * 未跟踪的 toolSessionId(spawn 前 / spawn 失败 / 已 dispose)视为 pass —— gate 只 drop
47401
- * "有证据判定为伪信号"的场景,不做 unknown → block。
47402
- */
47403
- canAcceptTurnEnd(toolSessionId) {
47404
- const state = this.tuiStates.get(toolSessionId);
47405
- if (!state) {
47406
- this.tuiOpts.screenIdleProbeLogger?.info(
47407
- "canAcceptTurnEnd: no tuiState \u2192 pass (\u672A\u8DDF\u8E2A)",
47408
- { toolSessionId }
47409
- );
47410
- return true;
47411
- }
47412
- const result = state.screenIdle.isIdle();
47413
- this.tuiOpts.screenIdleProbeLogger?.info("canAcceptTurnEnd", {
47414
- toolSessionId,
47415
- result
47416
- });
47417
- return result;
47418
- }
47419
47350
  spawn(ctx) {
47420
47351
  const args = buildTuiSpawnArgs(ctx, jsonlExistsForCtx(ctx));
47421
47352
  const cmd = process.env.CLAUDE_BIN ?? "claude";
@@ -47473,26 +47404,18 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47473
47404
  const screenIdleObserver = observeScreenIdle(surface, {
47474
47405
  idleMs: SCREEN_IDLE_MS,
47475
47406
  onIdle: () => {
47476
- if (!ctx.toolSessionId || !this.tuiOpts.onScreenIdle) return;
47477
- this.tuiLogger?.debug("screen-idle \u2192 notifyScreenIdle", { toolSessionId: ctx.toolSessionId });
47478
- this.tuiOpts.onScreenIdle(ctx.toolSessionId);
47407
+ if (!ctx.toolSessionId || !this.tuiOpts.onTurnIdle) return;
47408
+ this.tuiLogger?.debug("screen-idle \u2192 turn_end", { toolSessionId: ctx.toolSessionId });
47409
+ this.tuiOpts.onTurnIdle(ctx.toolSessionId);
47479
47410
  },
47480
47411
  getPopupVisible: () => popupObserver.visibleKind !== null,
47481
- // 取证 probe(可选,装配处传独立 file-only logger,跟主 daemon.log 解耦)
47482
- ...this.tuiOpts.screenIdleProbeLogger ? {
47483
- probeLogger: this.tuiOpts.screenIdleProbeLogger,
47484
- probeLabel: ctx.toolSessionId ?? "<no-tsid>"
47485
- } : {}
47412
+ // observer 还需静止多久才满 SCREEN_IDLE_MS(复合条件 AND):屏幕静止后精确等这段剩余再补
47413
+ // turn_end,确保它排在尾段 text + turn_duration 全部 poll 落盘之后 = buffer 末条。
47414
+ getObserverWaitMs: () => ctx.toolSessionId ? this.tuiOpts.getObserverWaitMs?.(ctx.toolSessionId, SCREEN_IDLE_MS) ?? 0 : 0
47486
47415
  });
47487
47416
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceRegister) {
47488
47417
  this.tuiOpts.onSurfaceRegister(ctx.toolSessionId, surface);
47489
47418
  }
47490
- if (ctx.toolSessionId) {
47491
- this.tuiStates.set(ctx.toolSessionId, {
47492
- screenIdle: screenIdleObserver,
47493
- popup: popupObserver
47494
- });
47495
- }
47496
47419
  let chunkSeq = 0;
47497
47420
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
47498
47421
  this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
@@ -47538,9 +47461,6 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47538
47461
  readyObserver.dispose();
47539
47462
  popupObserver.dispose();
47540
47463
  screenIdleObserver.dispose();
47541
- if (ctx.toolSessionId) {
47542
- this.tuiStates.delete(ctx.toolSessionId);
47543
- }
47544
47464
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceUnregister) {
47545
47465
  this.tuiOpts.onSurfaceUnregister(ctx.toolSessionId);
47546
47466
  }
@@ -47823,7 +47743,7 @@ async function writeInboxMcpConfig(args) {
47823
47743
  // src/shift/store.ts
47824
47744
  var import_promises = __toESM(require("fs/promises"), 1);
47825
47745
  var import_node_path19 = __toESM(require("path"), 1);
47826
- var import_node_crypto6 = require("crypto");
47746
+ var import_node_crypto5 = require("crypto");
47827
47747
 
47828
47748
  // src/shift/constants.ts
47829
47749
  var MAX_RUNS_PER_SHIFT = 30;
@@ -47919,7 +47839,7 @@ function createShiftStore(deps) {
47919
47839
  const nextRunAtMs = computeNextRunAtMs(input.schedule, now) ?? void 0;
47920
47840
  const shift = {
47921
47841
  ...input,
47922
- id: (0, import_node_crypto6.randomUUID)(),
47842
+ id: (0, import_node_crypto5.randomUUID)(),
47923
47843
  createdAtMs: now,
47924
47844
  updatedAtMs: now,
47925
47845
  state: { nextRunAtMs },
@@ -48695,13 +48615,13 @@ function mapSkillsListResponse(res) {
48695
48615
  const r = s ?? {};
48696
48616
  const name = str3(r.name);
48697
48617
  if (!name) continue;
48698
- const path68 = str3(r.path);
48618
+ const path73 = str3(r.path);
48699
48619
  const description = str3(r.description);
48700
48620
  const isPlugin = name.includes(":");
48701
48621
  out.push({
48702
48622
  name,
48703
48623
  source: isPlugin ? "plugin" : "project",
48704
- ...path68 ? { path: path68 } : {},
48624
+ ...path73 ? { path: path73 } : {},
48705
48625
  ...description ? { description } : {},
48706
48626
  ...isPlugin ? { plugin: name.split(":")[0] } : {}
48707
48627
  });
@@ -50495,6 +50415,23 @@ var ContactStore = class {
50495
50415
  this.flush();
50496
50416
  return true;
50497
50417
  }
50418
+ /**
50419
+ * 更新单条 contact 的 SSH 授权(PR: contact-ssh-sandbox)。对齐 setPin pattern:
50420
+ * store 只做原始 mutation,不做业务校验(如"sshAllowed=false 时清空 exposedDirs")——
50421
+ * 那是 handler / UI 的责任。数组语义是完全替换(不 append)。
50422
+ * @returns 是否命中:deviceId 不存在返 false;命中即 flush.
50423
+ */
50424
+ setSshAccess(deviceId, opts) {
50425
+ const existing = this.contacts.get(deviceId);
50426
+ if (!existing) return false;
50427
+ this.contacts.set(deviceId, {
50428
+ ...existing,
50429
+ sshAllowed: opts.sshAllowed,
50430
+ exposedDirs: opts.exposedDirs
50431
+ });
50432
+ this.flush();
50433
+ return true;
50434
+ }
50498
50435
  flush() {
50499
50436
  const file = path30.join(this.dataDir, FILE_NAME);
50500
50437
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
@@ -50642,7 +50579,9 @@ async function autoReverseContact(args) {
50642
50579
  connectToken: "",
50643
50580
  grants,
50644
50581
  addedAt: now(),
50645
- pinnedAt: null
50582
+ pinnedAt: null,
50583
+ sshAllowed: false,
50584
+ exposedDirs: []
50646
50585
  };
50647
50586
  args.store.upsert(base);
50648
50587
  args.broadcast({ type: "contact:added", contact: base });
@@ -50871,7 +50810,7 @@ function lookupMime(filePathOrName) {
50871
50810
  }
50872
50811
 
50873
50812
  // src/attachment/sign-url.ts
50874
- var import_node_crypto7 = __toESM(require("crypto"), 1);
50813
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
50875
50814
  var HMAC_ALGO = "sha256";
50876
50815
  function base64urlEncode(buf) {
50877
50816
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -50888,7 +50827,7 @@ function decodeAbsPathFromUrl(encoded) {
50888
50827
  }
50889
50828
  function computeSig(secret, absPath, e) {
50890
50829
  const msg = e === null ? absPath : `${absPath}|${e}`;
50891
- return import_node_crypto7.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
50830
+ return import_node_crypto6.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
50892
50831
  }
50893
50832
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
50894
50833
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -50923,7 +50862,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50923
50862
  if (provided.length !== expected.length) {
50924
50863
  return { ok: false, code: "BAD_SIG" };
50925
50864
  }
50926
- if (!import_node_crypto7.default.timingSafeEqual(provided, expected)) {
50865
+ if (!import_node_crypto6.default.timingSafeEqual(provided, expected)) {
50927
50866
  return { ok: false, code: "BAD_SIG" };
50928
50867
  }
50929
50868
  if (e !== null && now() / 1e3 > e) {
@@ -50935,7 +50874,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50935
50874
  // src/attachment/upload.ts
50936
50875
  var import_node_fs25 = __toESM(require("fs"), 1);
50937
50876
  var import_node_path25 = __toESM(require("path"), 1);
50938
- var import_node_crypto8 = __toESM(require("crypto"), 1);
50877
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
50939
50878
  var import_promises2 = require("stream/promises");
50940
50879
  var UploadError = class extends Error {
50941
50880
  constructor(code, message) {
@@ -50959,11 +50898,11 @@ async function writeUploadedAttachment(args) {
50959
50898
  } catch (err) {
50960
50899
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
50961
50900
  }
50962
- const hasher = import_node_crypto8.default.createHash("sha256");
50901
+ const hasher = import_node_crypto7.default.createHash("sha256");
50963
50902
  let actualSize = 0;
50964
50903
  const tmpPath = import_node_path25.default.join(
50965
50904
  attachmentsRoot,
50966
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto8.default.randomBytes(4).toString("hex")}`
50905
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto7.default.randomBytes(4).toString("hex")}`
50967
50906
  );
50968
50907
  try {
50969
50908
  await (0, import_promises2.pipeline)(
@@ -51839,7 +51778,7 @@ function runAttachmentGc(args) {
51839
51778
  // src/attachment/group.ts
51840
51779
  var import_node_fs28 = __toESM(require("fs"), 1);
51841
51780
  var import_node_path29 = __toESM(require("path"), 1);
51842
- var import_node_crypto9 = __toESM(require("crypto"), 1);
51781
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
51843
51782
  init_protocol();
51844
51783
  var GroupFileStore = class {
51845
51784
  dataDir;
@@ -51928,7 +51867,7 @@ var GroupFileStore = class {
51928
51867
  entries[idx] = next;
51929
51868
  } else {
51930
51869
  next = {
51931
- id: `gf-${import_node_crypto9.default.randomBytes(6).toString("base64url")}`,
51870
+ id: `gf-${import_node_crypto8.default.randomBytes(6).toString("base64url")}`,
51932
51871
  relPath: input.relPath,
51933
51872
  from: input.from,
51934
51873
  label: input.label,
@@ -52047,7 +51986,7 @@ function readDaemonSourceFromEnv(env = process.env) {
52047
51986
  // src/tunnel/tunnel-manager.ts
52048
51987
  var import_node_fs33 = __toESM(require("fs"), 1);
52049
51988
  var import_node_path34 = __toESM(require("path"), 1);
52050
- var import_node_crypto10 = __toESM(require("crypto"), 1);
51989
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
52051
51990
  var import_node_child_process9 = require("child_process");
52052
51991
 
52053
51992
  // src/tunnel/tunnel-store.ts
@@ -52546,7 +52485,7 @@ var TunnelManager = class {
52546
52485
  override: this.deps.frpcBinaryOverride ?? void 0
52547
52486
  });
52548
52487
  const tomlPath = import_node_path34.default.join(this.deps.dataDir, "frpc.toml");
52549
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto10.default.randomBytes(3).toString("hex")}`;
52488
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto9.default.randomBytes(3).toString("hex")}`;
52550
52489
  const toml = buildFrpcToml({
52551
52490
  serverAddr: t.frpsHost,
52552
52491
  serverPort: t.frpsPort,
@@ -52642,29 +52581,510 @@ async function waitForFrpcReady(proc, timeoutMs) {
52642
52581
  });
52643
52582
  }
52644
52583
 
52584
+ // src/sshd/sshd-manager.ts
52585
+ var import_node_fs36 = __toESM(require("fs"), 1);
52586
+ var import_node_path37 = __toESM(require("path"), 1);
52587
+ var import_node_child_process11 = require("child_process");
52588
+
52589
+ // src/sshd/sshd-config.ts
52590
+ function buildSshdConfig(input) {
52591
+ const lines = [
52592
+ `ListenAddress ${input.listenAddress}`,
52593
+ `Port ${input.port}`,
52594
+ `HostKey ${input.hostKeyPath}`,
52595
+ `PidFile ${input.pidFilePath}`,
52596
+ `AuthorizedKeysFile ${input.authorizedKeysFile}`,
52597
+ `PubkeyAuthentication yes`,
52598
+ `PasswordAuthentication no`,
52599
+ `ChallengeResponseAuthentication no`,
52600
+ `KbdInteractiveAuthentication no`,
52601
+ `PermitRootLogin no`,
52602
+ `StrictModes no`,
52603
+ `UsePAM no`,
52604
+ `LogLevel INFO`,
52605
+ `Subsystem sftp internal-sftp`
52606
+ ];
52607
+ return lines.join("\n") + "\n";
52608
+ }
52609
+
52610
+ // src/sshd/sshd-process.ts
52611
+ var import_node_fs34 = __toESM(require("fs"), 1);
52612
+ var import_node_path35 = __toESM(require("path"), 1);
52613
+ var import_node_child_process10 = require("child_process");
52614
+ function sshdPidFilePath(dataDir) {
52615
+ return import_node_path35.default.join(dataDir, "sshd", "sshd.pid");
52616
+ }
52617
+ function writeSshdPid(dataDir, pid) {
52618
+ try {
52619
+ const p2 = sshdPidFilePath(dataDir);
52620
+ import_node_fs34.default.mkdirSync(import_node_path35.default.dirname(p2), { recursive: true, mode: 448 });
52621
+ import_node_fs34.default.writeFileSync(p2, String(pid), { mode: 384 });
52622
+ } catch {
52623
+ }
52624
+ }
52625
+ function clearSshdPid(dataDir) {
52626
+ try {
52627
+ import_node_fs34.default.unlinkSync(sshdPidFilePath(dataDir));
52628
+ } catch {
52629
+ }
52630
+ }
52631
+ function defaultIsPidAlive2(pid) {
52632
+ if (!Number.isFinite(pid) || pid <= 0) return false;
52633
+ try {
52634
+ process.kill(pid, 0);
52635
+ return true;
52636
+ } catch (err) {
52637
+ const code = err.code;
52638
+ return code === "EPERM";
52639
+ }
52640
+ }
52641
+ function defaultReadPidFile2(file) {
52642
+ try {
52643
+ return import_node_fs34.default.readFileSync(file, "utf8");
52644
+ } catch {
52645
+ return null;
52646
+ }
52647
+ }
52648
+ function defaultKillPid2(pid, signal) {
52649
+ try {
52650
+ process.kill(pid, signal);
52651
+ } catch {
52652
+ }
52653
+ }
52654
+ function defaultSleep2(ms) {
52655
+ return new Promise((r) => setTimeout(r, ms));
52656
+ }
52657
+ async function killStaleSshd(deps) {
52658
+ const pidFile = sshdPidFilePath(deps.dataDir);
52659
+ const configPath = import_node_path35.default.join(deps.dataDir, "sshd", "sshd_config");
52660
+ const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile2;
52661
+ const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive2;
52662
+ const killPid = deps.killPidImpl ?? defaultKillPid2;
52663
+ const scanPids = deps.scanSshdPidsImpl ?? ((cp) => defaultScanSshdPidsByCmdline(cp, deps.logger));
52664
+ const sleep2 = deps.sleepImpl ?? defaultSleep2;
52665
+ const victims = /* @__PURE__ */ new Set();
52666
+ const raw = readPidFile(pidFile);
52667
+ if (raw) {
52668
+ const pid = parseInt(raw.trim(), 10);
52669
+ if (Number.isFinite(pid) && pid > 0 && pid !== deps.ownPid && isAlive(pid)) {
52670
+ victims.add(pid);
52671
+ }
52672
+ }
52673
+ try {
52674
+ const scanned = await scanPids(configPath);
52675
+ for (const pid of scanned) {
52676
+ if (pid > 0 && pid !== deps.ownPid && isAlive(pid)) victims.add(pid);
52677
+ }
52678
+ } catch (e) {
52679
+ deps.logger?.warn("sshd: stale-sshd cmdline scan failed", { err: e.message });
52680
+ }
52681
+ if (victims.size === 0) {
52682
+ try {
52683
+ import_node_fs34.default.unlinkSync(pidFile);
52684
+ } catch {
52685
+ }
52686
+ return;
52687
+ }
52688
+ for (const pid of victims) {
52689
+ deps.logger?.warn("sshd: killing stale sshd before respawn", { pid });
52690
+ killPid(pid, "SIGKILL");
52691
+ }
52692
+ await sleep2(deps.reapWaitMs ?? 300);
52693
+ try {
52694
+ import_node_fs34.default.unlinkSync(pidFile);
52695
+ } catch {
52696
+ }
52697
+ }
52698
+ async function defaultScanSshdPidsByCmdline(configPath, logger) {
52699
+ if (process.platform === "win32") return [];
52700
+ return new Promise((resolve6) => {
52701
+ const ps = (0, import_node_child_process10.spawn)("ps", ["-axo", "pid=,command="], { stdio: ["ignore", "pipe", "ignore"] });
52702
+ let buf = "";
52703
+ ps.stdout.on("data", (c) => {
52704
+ buf += c.toString();
52705
+ });
52706
+ ps.on("exit", () => {
52707
+ const pids = [];
52708
+ for (const line of buf.split("\n")) {
52709
+ const m2 = /^\s*(\d+)\s+(.*)$/.exec(line);
52710
+ if (!m2) continue;
52711
+ const cmd = m2[2];
52712
+ if (!/\bsshd\b/.test(cmd)) continue;
52713
+ if (!cmd.includes(configPath)) continue;
52714
+ const pid = parseInt(m2[1], 10);
52715
+ if (Number.isFinite(pid) && pid > 0) pids.push(pid);
52716
+ }
52717
+ resolve6(pids);
52718
+ });
52719
+ ps.on("error", (e) => {
52720
+ logger?.warn("sshd: ps scan failed", { err: e.message });
52721
+ resolve6([]);
52722
+ });
52723
+ });
52724
+ }
52725
+
52726
+ // src/sshd/jail-script.ts
52727
+ var import_node_fs35 = __toESM(require("fs"), 1);
52728
+ var import_node_path36 = __toESM(require("path"), 1);
52729
+ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
52730
+ # clawd-ssh-jail — SSH reverse access sandbox wrapper (managed by clawd; do not edit)
52731
+ #
52732
+ # 由 sshd authorized_keys 的 command= 强制入口调用。
52733
+ # 用法: sshd 会以 \`clawd-ssh-jail <deviceId>\` 起本脚本;$SSH_ORIGINAL_COMMAND = client
52734
+ # 真实请求(interactive shell 时为空)。
52735
+ #
52736
+ # 职责:
52737
+ # 1. 读 ~/.clawd/contacts.json 找 contact.exposedDirs
52738
+ # 2. macOS 用 sandbox-exec + sbpl; Linux 用 bwrap
52739
+ # 3. exec 沙箱 shell
52740
+
52741
+ set -euo pipefail
52742
+
52743
+ DEVICE_ID="\${1:-}"
52744
+ if [ -z "$DEVICE_ID" ]; then
52745
+ echo "clawd-ssh-jail: missing deviceId" >&2
52746
+ exit 1
52747
+ fi
52748
+
52749
+ CONTACTS="\${HOME}/.clawd/contacts.json"
52750
+ if [ ! -f "$CONTACTS" ]; then
52751
+ echo "clawd-ssh-jail: contacts.json missing" >&2
52752
+ exit 1
52753
+ fi
52754
+
52755
+ # 读 contact 的 exposedDirs (mac/linux 都自带 python3)
52756
+ EXPOSED_JSON=$(python3 -c "
52757
+ import json, sys
52758
+ with open('$CONTACTS') as f:
52759
+ data = json.load(f)
52760
+ for c in data.get('contacts', []):
52761
+ if c.get('deviceId') == '$DEVICE_ID':
52762
+ if not c.get('sshAllowed'):
52763
+ print('DENIED', file=sys.stderr); sys.exit(2)
52764
+ for d in c.get('exposedDirs', []):
52765
+ print(d)
52766
+ sys.exit(0)
52767
+ sys.exit(3)
52768
+ ")
52769
+
52770
+ if [ -z "$EXPOSED_JSON" ]; then
52771
+ echo "clawd-ssh-jail: contact not found or no exposed dirs" >&2
52772
+ exit 1
52773
+ fi
52774
+
52775
+ # 校验路径安全(bash 侧二次防御)
52776
+ while IFS= read -r line; do
52777
+ case "$line" in
52778
+ /*) : ;;
52779
+ *) echo "clawd-ssh-jail: bad path: $line" >&2; exit 1 ;;
52780
+ esac
52781
+ case "$line" in
52782
+ *[\"\'\`\$\;\|\&\(\)\{\}\[\]\<\>\*\?]*)
52783
+ echo "clawd-ssh-jail: unsafe path: $line" >&2; exit 1 ;;
52784
+ esac
52785
+ done <<< "$EXPOSED_JSON"
52786
+
52787
+ CMD="\${SSH_ORIGINAL_COMMAND:-}"
52788
+ if [ -z "$CMD" ]; then
52789
+ SHELL_CMD=(bash --login)
52790
+ else
52791
+ SHELL_CMD=(bash -c "$CMD")
52792
+ fi
52793
+
52794
+ case "$(uname -s)" in
52795
+ Darwin)
52796
+ POLICY="(version 1)
52797
+ (deny default)
52798
+ (allow process*)
52799
+ (allow signal (target self))
52800
+ (allow sysctl-read)
52801
+ (allow mach-lookup)
52802
+ (allow file-read-metadata)
52803
+ (allow network*)
52804
+ (allow file-read* (subpath \"/usr\"))
52805
+ (allow file-read* (subpath \"/bin\"))
52806
+ (allow file-read* (subpath \"/sbin\"))
52807
+ (allow file-read* (subpath \"/System\"))
52808
+ (allow file-read* (subpath \"/Library\"))
52809
+ (allow file-read* (subpath \"/etc\"))
52810
+ (allow file-read* (subpath \"/private/etc\"))"
52811
+ while IFS= read -r d; do
52812
+ POLICY+="
52813
+ (allow file-read* file-write* (subpath \"$d\"))"
52814
+ done <<< "$EXPOSED_JSON"
52815
+ exec sandbox-exec -p "$POLICY" "\${SHELL_CMD[@]}"
52816
+ ;;
52817
+ Linux)
52818
+ BWRAP_ARGS=(
52819
+ --unshare-user --unshare-ipc --unshare-pid --unshare-uts
52820
+ --die-with-parent
52821
+ --proc /proc --dev /dev --tmpfs /tmp
52822
+ --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /sbin /sbin
52823
+ --ro-bind /lib /lib --ro-bind /etc /etc
52824
+ )
52825
+ if [ -d /lib64 ]; then BWRAP_ARGS+=(--ro-bind /lib64 /lib64); fi
52826
+ while IFS= read -r d; do
52827
+ BWRAP_ARGS+=(--bind "$d" "$d")
52828
+ done <<< "$EXPOSED_JSON"
52829
+ exec bwrap "\${BWRAP_ARGS[@]}" "\${SHELL_CMD[@]}"
52830
+ ;;
52831
+ *)
52832
+ echo "clawd-ssh-jail: unsupported OS $(uname -s)" >&2
52833
+ exit 1
52834
+ ;;
52835
+ esac
52836
+ `;
52837
+ function ensureJailScript(dataDir) {
52838
+ const binDir = import_node_path36.default.join(dataDir, "bin");
52839
+ import_node_fs35.default.mkdirSync(binDir, { recursive: true, mode: 493 });
52840
+ const target = import_node_path36.default.join(binDir, "clawd-ssh-jail");
52841
+ import_node_fs35.default.writeFileSync(target, CLAWD_SSH_JAIL_SCRIPT, { mode: 493 });
52842
+ return target;
52843
+ }
52844
+
52845
+ // src/sshd/sshd-manager.ts
52846
+ var SshdManager = class {
52847
+ constructor(deps) {
52848
+ this.deps = deps;
52849
+ this.sshdDir = import_node_path37.default.join(deps.dataDir, "sshd");
52850
+ this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
52851
+ }
52852
+ deps;
52853
+ proc = null;
52854
+ sshdDir;
52855
+ stopping = false;
52856
+ exitHookInstalled = false;
52857
+ startupTimeoutMs;
52858
+ get port() {
52859
+ return this.deps.port;
52860
+ }
52861
+ async start() {
52862
+ const { logger } = this.deps;
52863
+ await (this.deps.killStaleImpl ?? killStaleSshd)({
52864
+ dataDir: this.deps.dataDir,
52865
+ ownPid: process.pid,
52866
+ logger
52867
+ });
52868
+ import_node_fs36.default.mkdirSync(this.sshdDir, { recursive: true, mode: 448 });
52869
+ import_node_fs36.default.mkdirSync(import_node_path37.default.join(this.sshdDir, "authorized_keys.d"), { recursive: true, mode: 448 });
52870
+ ensureJailScript(this.deps.dataDir);
52871
+ const hostKeyPath = import_node_path37.default.join(this.sshdDir, "host_key");
52872
+ if (!import_node_fs36.default.existsSync(hostKeyPath)) {
52873
+ await this.generateHostKey(hostKeyPath);
52874
+ }
52875
+ const akFile = import_node_path37.default.join(this.sshdDir, "authorized_keys.d", "clawd-contacts");
52876
+ if (!import_node_fs36.default.existsSync(akFile)) {
52877
+ import_node_fs36.default.writeFileSync(akFile, "", { mode: 384 });
52878
+ }
52879
+ const configPath = import_node_path37.default.join(this.sshdDir, "sshd_config");
52880
+ const config = buildSshdConfig({
52881
+ listenAddress: "127.0.0.1",
52882
+ port: this.deps.port,
52883
+ hostKeyPath,
52884
+ authorizedKeysFile: akFile,
52885
+ pidFilePath: import_node_path37.default.join(this.sshdDir, "sshd.pid")
52886
+ });
52887
+ import_node_fs36.default.writeFileSync(configPath, config, { mode: 384 });
52888
+ const sshdBin = this.deps.sshdBin ?? "/usr/sbin/sshd";
52889
+ const proc = (this.deps.spawnImpl ?? import_node_child_process11.spawn)(sshdBin, ["-D", "-e", "-f", configPath], {
52890
+ stdio: ["ignore", "pipe", "pipe"]
52891
+ });
52892
+ const logStream = import_node_fs36.default.createWriteStream(import_node_path37.default.join(this.sshdDir, "sshd.log"), {
52893
+ flags: "a",
52894
+ mode: 384
52895
+ });
52896
+ logStream.on("error", () => {
52897
+ });
52898
+ const tee = (c) => {
52899
+ logStream.write(String(c));
52900
+ };
52901
+ proc.stdout?.on("data", tee);
52902
+ proc.stderr?.on("data", tee);
52903
+ proc.once("exit", () => logStream.end());
52904
+ const ready = await waitForSshdReady(proc, this.startupTimeoutMs);
52905
+ if (!ready.ok) {
52906
+ try {
52907
+ proc.kill("SIGTERM");
52908
+ } catch {
52909
+ }
52910
+ const tail = ready.output.slice(-500);
52911
+ const msg = tail ? `${ready.error}
52912
+ ${tail}` : ready.error;
52913
+ throw new Error(msg);
52914
+ }
52915
+ if (typeof proc.pid === "number") writeSshdPid(this.deps.dataDir, proc.pid);
52916
+ this.proc = proc;
52917
+ this.installProcessExitHandlersIfNeeded();
52918
+ this.attachExitListener(proc);
52919
+ logger?.info("sshd: up", { port: this.deps.port, pid: proc.pid ?? null });
52920
+ return { port: this.deps.port };
52921
+ }
52922
+ async stop() {
52923
+ this.stopping = true;
52924
+ const proc = this.proc;
52925
+ this.proc = null;
52926
+ if (!proc) {
52927
+ clearSshdPid(this.deps.dataDir);
52928
+ return;
52929
+ }
52930
+ proc.kill("SIGTERM");
52931
+ await new Promise((resolve6) => {
52932
+ const t = setTimeout(() => {
52933
+ try {
52934
+ proc.kill("SIGKILL");
52935
+ } catch {
52936
+ }
52937
+ resolve6();
52938
+ }, 5e3);
52939
+ proc.once("exit", () => {
52940
+ clearTimeout(t);
52941
+ resolve6();
52942
+ });
52943
+ });
52944
+ clearSshdPid(this.deps.dataDir);
52945
+ }
52946
+ killSync() {
52947
+ const proc = this.proc;
52948
+ this.proc = null;
52949
+ clearSshdPid(this.deps.dataDir);
52950
+ if (!proc) return;
52951
+ try {
52952
+ proc.kill("SIGTERM");
52953
+ } catch {
52954
+ }
52955
+ }
52956
+ attachExitListener(proc) {
52957
+ proc.on("exit", (code) => {
52958
+ this.deps.logger?.warn("sshd exited", { code });
52959
+ if (this.stopping) return;
52960
+ this.proc = null;
52961
+ this.deps.onSshdExit?.({ code });
52962
+ });
52963
+ }
52964
+ installProcessExitHandlersIfNeeded() {
52965
+ if (this.exitHookInstalled) return;
52966
+ if (this.deps.installProcessExitHandlers !== true) return;
52967
+ this.exitHookInstalled = true;
52968
+ const sync = () => this.killSync();
52969
+ process.once("exit", sync);
52970
+ process.once("SIGHUP", sync);
52971
+ process.once("uncaughtException", sync);
52972
+ }
52973
+ async generateHostKey(hostKeyPath) {
52974
+ const keygenBin = this.deps.keygenBin ?? "/usr/bin/ssh-keygen";
52975
+ await new Promise((resolve6, reject) => {
52976
+ const p2 = (this.deps.spawnImpl ?? import_node_child_process11.spawn)(
52977
+ keygenBin,
52978
+ ["-t", "ed25519", "-f", hostKeyPath, "-N", "", "-q"],
52979
+ { stdio: "ignore" }
52980
+ );
52981
+ p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
52982
+ p2.on("error", reject);
52983
+ });
52984
+ try {
52985
+ import_node_fs36.default.chmodSync(hostKeyPath, 384);
52986
+ } catch {
52987
+ }
52988
+ }
52989
+ };
52990
+ async function waitForSshdReady(proc, timeoutMs) {
52991
+ return new Promise((resolve6) => {
52992
+ let settled = false;
52993
+ let buf = "";
52994
+ const finish = (r) => {
52995
+ if (settled) return;
52996
+ settled = true;
52997
+ cleanup();
52998
+ resolve6(r);
52999
+ };
53000
+ const onData = (chunk) => {
53001
+ buf += String(chunk);
53002
+ if (/Server listening on/i.test(buf)) finish({ ok: true });
53003
+ if (/fatal:/i.test(buf) || /error: Bind to port/i.test(buf)) {
53004
+ finish({ ok: false, error: "sshd startup failed", output: buf });
53005
+ }
53006
+ };
53007
+ const onExit = (code) => finish({ ok: false, error: `sshd exited before ready (code=${code})`, output: buf });
53008
+ const onErr = (err) => finish({ ok: false, error: `sshd spawn error: ${err.message}`, output: buf });
53009
+ const cleanup = () => {
53010
+ proc.stdout?.off("data", onData);
53011
+ proc.stderr?.off("data", onData);
53012
+ proc.off("exit", onExit);
53013
+ proc.off("error", onErr);
53014
+ clearTimeout(timer);
53015
+ };
53016
+ proc.stdout?.on("data", onData);
53017
+ proc.stderr?.on("data", onData);
53018
+ proc.on("exit", onExit);
53019
+ proc.on("error", onErr);
53020
+ const timer = setTimeout(
53021
+ () => finish({ ok: false, error: `sshd startup timeout after ${timeoutMs}ms`, output: buf }),
53022
+ timeoutMs
53023
+ );
53024
+ });
53025
+ }
53026
+
53027
+ // src/sshd/authorized-keys.ts
53028
+ var import_node_fs37 = __toESM(require("fs"), 1);
53029
+ var import_node_path38 = __toESM(require("path"), 1);
53030
+ var JAIL_BIN_PATH_ENV = "CLAWD_JAIL_BIN_PATH";
53031
+ var AUTHORIZED_KEYS_FILE = "clawd-contacts";
53032
+ function jailBinPath() {
53033
+ return process.env[JAIL_BIN_PATH_ENV] ?? import_node_path38.default.join(process.env.HOME ?? "", ".clawd", "bin", "clawd-ssh-jail");
53034
+ }
53035
+ function rebuildAuthorizedKeys(store, sshdDir) {
53036
+ const akDir = import_node_path38.default.join(sshdDir, "authorized_keys.d");
53037
+ const target = import_node_path38.default.join(akDir, AUTHORIZED_KEYS_FILE);
53038
+ import_node_fs37.default.mkdirSync(akDir, { recursive: true, mode: 448 });
53039
+ const lines = ["# managed by clawd; do not edit", ""];
53040
+ for (const c of store.list()) {
53041
+ if (!c.sshAllowed) continue;
53042
+ const safe = /^[A-Za-z0-9_.-]+$/.test(c.deviceId);
53043
+ if (!safe) continue;
53044
+ const pubkey = readIssuedPubkey(sshdDir, c.deviceId);
53045
+ if (!pubkey) continue;
53046
+ const bin = jailBinPath();
53047
+ lines.push(`command="${bin} ${c.deviceId}",restrict ${pubkey.trim()}`);
53048
+ lines.push(`# contact:${c.deviceId}`);
53049
+ }
53050
+ const body = lines.join("\n") + "\n";
53051
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
53052
+ import_node_fs37.default.writeFileSync(tmp, body, { mode: 384 });
53053
+ import_node_fs37.default.renameSync(tmp, target);
53054
+ }
53055
+ function readIssuedPubkey(sshdDir, deviceId) {
53056
+ const safeId = deviceId.replace(/[\/\\]/g, "_");
53057
+ const p2 = import_node_path38.default.join(sshdDir, "keys", `${safeId}.ed25519.pub`);
53058
+ try {
53059
+ return import_node_fs37.default.readFileSync(p2, "utf8");
53060
+ } catch {
53061
+ return null;
53062
+ }
53063
+ }
53064
+
52645
53065
  // src/tunnel/device-key.ts
52646
53066
  var import_node_os14 = __toESM(require("os"), 1);
52647
- var import_node_path35 = __toESM(require("path"), 1);
52648
- var import_node_crypto11 = __toESM(require("crypto"), 1);
53067
+ var import_node_path39 = __toESM(require("path"), 1);
53068
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
52649
53069
  var DERIVE_SALT = "clawd-tunnel-device-v1";
52650
53070
  function deriveStableDeviceKey(opts = {}) {
52651
53071
  const hostname = opts.hostname ?? import_node_os14.default.hostname();
52652
53072
  const uid = opts.uid ?? (typeof import_node_os14.default.userInfo === "function" ? import_node_os14.default.userInfo().uid : 0);
52653
53073
  const home = opts.home ?? import_node_os14.default.homedir();
52654
- const defaultDataDir = import_node_path35.default.resolve(import_node_path35.default.join(home, ".clawd"));
52655
- const normalizedDataDir = opts.dataDir ? import_node_path35.default.resolve(opts.dataDir) : null;
53074
+ const defaultDataDir = import_node_path39.default.resolve(import_node_path39.default.join(home, ".clawd"));
53075
+ const normalizedDataDir = opts.dataDir ? import_node_path39.default.resolve(opts.dataDir) : null;
52656
53076
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
52657
53077
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
52658
- return import_node_crypto11.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
53078
+ return import_node_crypto10.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
52659
53079
  }
52660
53080
 
52661
53081
  // src/auth-store.ts
52662
- var import_node_fs34 = __toESM(require("fs"), 1);
52663
- var import_node_path36 = __toESM(require("path"), 1);
52664
- var import_node_crypto12 = __toESM(require("crypto"), 1);
53082
+ var import_node_fs38 = __toESM(require("fs"), 1);
53083
+ var import_node_path40 = __toESM(require("path"), 1);
53084
+ var import_node_crypto11 = __toESM(require("crypto"), 1);
52665
53085
  var AUTH_FILE_NAME = "auth.json";
52666
53086
  function authFilePath(dataDir) {
52667
- return import_node_path36.default.join(dataDir, AUTH_FILE_NAME);
53087
+ return import_node_path40.default.join(dataDir, AUTH_FILE_NAME);
52668
53088
  }
52669
53089
  function loadOrCreateAuthFile(opts) {
52670
53090
  const file = authFilePath(opts.dataDir);
@@ -52693,14 +53113,14 @@ function loadOrCreateAuthFile(opts) {
52693
53113
  return next;
52694
53114
  }
52695
53115
  function defaultGenerateToken() {
52696
- return import_node_crypto12.default.randomBytes(32).toString("base64url");
53116
+ return import_node_crypto11.default.randomBytes(32).toString("base64url");
52697
53117
  }
52698
53118
  function defaultGenerateOwnerPrincipalId() {
52699
- return `owner-${import_node_crypto12.default.randomUUID()}`;
53119
+ return `owner-${import_node_crypto11.default.randomUUID()}`;
52700
53120
  }
52701
53121
  function readAuthFile(file) {
52702
53122
  try {
52703
- const raw = import_node_fs34.default.readFileSync(file, "utf8");
53123
+ const raw = import_node_fs38.default.readFileSync(file, "utf8");
52704
53124
  const parsed = JSON.parse(raw);
52705
53125
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
52706
53126
  return null;
@@ -52720,25 +53140,25 @@ function readAuthFile(file) {
52720
53140
  }
52721
53141
  }
52722
53142
  function writeAuthFile(file, content) {
52723
- import_node_fs34.default.mkdirSync(import_node_path36.default.dirname(file), { recursive: true });
52724
- import_node_fs34.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
53143
+ import_node_fs38.default.mkdirSync(import_node_path40.default.dirname(file), { recursive: true });
53144
+ import_node_fs38.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
52725
53145
  try {
52726
- import_node_fs34.default.chmodSync(file, 384);
53146
+ import_node_fs38.default.chmodSync(file, 384);
52727
53147
  } catch {
52728
53148
  }
52729
53149
  }
52730
53150
 
52731
53151
  // src/owner-profile.ts
52732
- var import_node_fs35 = __toESM(require("fs"), 1);
53152
+ var import_node_fs39 = __toESM(require("fs"), 1);
52733
53153
  var import_node_os15 = __toESM(require("os"), 1);
52734
- var import_node_path37 = __toESM(require("path"), 1);
53154
+ var import_node_path41 = __toESM(require("path"), 1);
52735
53155
  var PROFILE_FILENAME = "profile.json";
52736
53156
  function loadOwnerDisplayName(dataDir) {
52737
53157
  const fallback = import_node_os15.default.userInfo().username;
52738
- const profilePath = import_node_path37.default.join(dataDir, PROFILE_FILENAME);
53158
+ const profilePath = import_node_path41.default.join(dataDir, PROFILE_FILENAME);
52739
53159
  let raw;
52740
53160
  try {
52741
- raw = import_node_fs35.default.readFileSync(profilePath, "utf8");
53161
+ raw = import_node_fs39.default.readFileSync(profilePath, "utf8");
52742
53162
  } catch {
52743
53163
  return fallback;
52744
53164
  }
@@ -52761,18 +53181,18 @@ function loadOwnerDisplayName(dataDir) {
52761
53181
  }
52762
53182
 
52763
53183
  // src/feishu-auth/owner-identity-store.ts
52764
- var import_node_fs36 = __toESM(require("fs"), 1);
52765
- var import_node_path38 = __toESM(require("path"), 1);
53184
+ var import_node_fs40 = __toESM(require("fs"), 1);
53185
+ var import_node_path42 = __toESM(require("path"), 1);
52766
53186
  var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
52767
53187
  var OwnerIdentityStore = class {
52768
53188
  file;
52769
53189
  constructor(dataDir) {
52770
- this.file = import_node_path38.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
53190
+ this.file = import_node_path42.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
52771
53191
  }
52772
53192
  read() {
52773
53193
  let raw;
52774
53194
  try {
52775
- raw = import_node_fs36.default.readFileSync(this.file, "utf8");
53195
+ raw = import_node_fs40.default.readFileSync(this.file, "utf8");
52776
53196
  } catch {
52777
53197
  return null;
52778
53198
  }
@@ -52800,16 +53220,16 @@ var OwnerIdentityStore = class {
52800
53220
  };
52801
53221
  }
52802
53222
  write(record) {
52803
- import_node_fs36.default.mkdirSync(import_node_path38.default.dirname(this.file), { recursive: true });
52804
- import_node_fs36.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
53223
+ import_node_fs40.default.mkdirSync(import_node_path42.default.dirname(this.file), { recursive: true });
53224
+ import_node_fs40.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
52805
53225
  try {
52806
- import_node_fs36.default.chmodSync(this.file, 384);
53226
+ import_node_fs40.default.chmodSync(this.file, 384);
52807
53227
  } catch {
52808
53228
  }
52809
53229
  }
52810
53230
  clear() {
52811
53231
  try {
52812
- import_node_fs36.default.unlinkSync(this.file);
53232
+ import_node_fs40.default.unlinkSync(this.file);
52813
53233
  } catch (err) {
52814
53234
  const code = err?.code;
52815
53235
  if (code !== "ENOENT") throw err;
@@ -52818,7 +53238,7 @@ var OwnerIdentityStore = class {
52818
53238
  };
52819
53239
 
52820
53240
  // src/feishu-auth/login-flow.ts
52821
- var import_node_crypto13 = __toESM(require("crypto"), 1);
53241
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
52822
53242
  var STATE_TTL_MS = 5 * 60 * 1e3;
52823
53243
  var LoginFlow = class {
52824
53244
  constructor(deps) {
@@ -52827,7 +53247,7 @@ var LoginFlow = class {
52827
53247
  deps;
52828
53248
  pendingStates = /* @__PURE__ */ new Map();
52829
53249
  start() {
52830
- const state = import_node_crypto13.default.randomBytes(16).toString("base64url");
53250
+ const state = import_node_crypto12.default.randomBytes(16).toString("base64url");
52831
53251
  const now = (this.deps.now ?? Date.now)();
52832
53252
  this.pendingStates.set(state, now);
52833
53253
  this.gcExpired(now);
@@ -52930,9 +53350,9 @@ var CentralClientError = class extends Error {
52930
53350
  code;
52931
53351
  cause;
52932
53352
  };
52933
- async function centralRequest(opts, path68, init) {
53353
+ async function centralRequest(opts, path73, init) {
52934
53354
  const f = opts.fetchImpl ?? globalThis.fetch;
52935
- const url = `${opts.api.replace(/\/+$/, "")}${path68}`;
53355
+ const url = `${opts.api.replace(/\/+$/, "")}${path73}`;
52936
53356
  const ctrl = new AbortController();
52937
53357
  const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
52938
53358
  let res;
@@ -53074,8 +53494,8 @@ function verifyConnectToken(args) {
53074
53494
  }
53075
53495
 
53076
53496
  // src/feishu-auth/server-key.ts
53077
- var fs46 = __toESM(require("fs"), 1);
53078
- var path47 = __toESM(require("path"), 1);
53497
+ var fs50 = __toESM(require("fs"), 1);
53498
+ var path51 = __toESM(require("path"), 1);
53079
53499
  var FILE_NAME2 = "server-signing-key.json";
53080
53500
  var ServerKeyStore = class {
53081
53501
  constructor(dataDir) {
@@ -53083,12 +53503,12 @@ var ServerKeyStore = class {
53083
53503
  }
53084
53504
  dataDir;
53085
53505
  filePath() {
53086
- return path47.join(this.dataDir, FILE_NAME2);
53506
+ return path51.join(this.dataDir, FILE_NAME2);
53087
53507
  }
53088
53508
  /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
53089
53509
  read() {
53090
53510
  try {
53091
- const raw = fs46.readFileSync(this.filePath(), "utf8");
53511
+ const raw = fs50.readFileSync(this.filePath(), "utf8");
53092
53512
  const parsed = JSON.parse(raw);
53093
53513
  if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
53094
53514
  return parsed.publicKeyPem;
@@ -53103,12 +53523,12 @@ var ServerKeyStore = class {
53103
53523
  publicKeyPem,
53104
53524
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
53105
53525
  };
53106
- fs46.mkdirSync(this.dataDir, { recursive: true });
53107
- fs46.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
53526
+ fs50.mkdirSync(this.dataDir, { recursive: true });
53527
+ fs50.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
53108
53528
  }
53109
53529
  clear() {
53110
53530
  try {
53111
- fs46.unlinkSync(this.filePath());
53531
+ fs50.unlinkSync(this.filePath());
53112
53532
  } catch {
53113
53533
  }
53114
53534
  }
@@ -53121,12 +53541,12 @@ init_protocol();
53121
53541
  init_protocol();
53122
53542
 
53123
53543
  // src/session/fork.ts
53124
- var import_node_fs37 = __toESM(require("fs"), 1);
53544
+ var import_node_fs41 = __toESM(require("fs"), 1);
53125
53545
  var import_node_os16 = __toESM(require("os"), 1);
53126
- var import_node_path39 = __toESM(require("path"), 1);
53546
+ var import_node_path43 = __toESM(require("path"), 1);
53127
53547
  init_claude_history();
53128
53548
  function readJsonlEntries(file) {
53129
- const raw = import_node_fs37.default.readFileSync(file, "utf8");
53549
+ const raw = import_node_fs41.default.readFileSync(file, "utf8");
53130
53550
  const out = [];
53131
53551
  for (const line of raw.split("\n")) {
53132
53552
  const t = line.trim();
@@ -53139,10 +53559,10 @@ function readJsonlEntries(file) {
53139
53559
  return out;
53140
53560
  }
53141
53561
  function forkSession(input) {
53142
- const baseDir = input.baseDir ?? import_node_path39.default.join(import_node_os16.default.homedir(), ".claude");
53143
- const projectDir = import_node_path39.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
53144
- const sourceFile = import_node_path39.default.join(projectDir, `${input.toolSessionId}.jsonl`);
53145
- if (!import_node_fs37.default.existsSync(sourceFile)) {
53562
+ const baseDir = input.baseDir ?? import_node_path43.default.join(import_node_os16.default.homedir(), ".claude");
53563
+ const projectDir = import_node_path43.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
53564
+ const sourceFile = import_node_path43.default.join(projectDir, `${input.toolSessionId}.jsonl`);
53565
+ if (!import_node_fs41.default.existsSync(sourceFile)) {
53146
53566
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
53147
53567
  }
53148
53568
  const entries = readJsonlEntries(sourceFile);
@@ -53172,9 +53592,9 @@ function forkSession(input) {
53172
53592
  }
53173
53593
  forkedLines.push(JSON.stringify(forked));
53174
53594
  }
53175
- const forkedFilePath = import_node_path39.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
53176
- import_node_fs37.default.mkdirSync(projectDir, { recursive: true });
53177
- import_node_fs37.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
53595
+ const forkedFilePath = import_node_path43.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
53596
+ import_node_fs41.default.mkdirSync(projectDir, { recursive: true });
53597
+ import_node_fs41.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
53178
53598
  return { forkedToolSessionId, forkedFilePath };
53179
53599
  }
53180
53600
 
@@ -53526,7 +53946,7 @@ function buildPermissionHandlers(deps) {
53526
53946
  }
53527
53947
 
53528
53948
  // src/handlers/history.ts
53529
- var path50 = __toESM(require("path"), 1);
53949
+ var path54 = __toESM(require("path"), 1);
53530
53950
  init_protocol();
53531
53951
 
53532
53952
  // src/session/recent-dirs.ts
@@ -53544,7 +53964,7 @@ function listRecentDirs(store, limit = 50) {
53544
53964
  }
53545
53965
 
53546
53966
  // src/permission/persona-paths.ts
53547
- var path49 = __toESM(require("path"), 1);
53967
+ var path53 = __toESM(require("path"), 1);
53548
53968
  function getAllowedPersonaIds(grants, action) {
53549
53969
  const ids = /* @__PURE__ */ new Set();
53550
53970
  for (const g2 of grants) {
@@ -53557,42 +53977,42 @@ function getAllowedPersonaIds(grants, action) {
53557
53977
  return ids;
53558
53978
  }
53559
53979
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read", userWorkDir) {
53560
- const target = path49.resolve(absPath);
53980
+ const target = path53.resolve(absPath);
53561
53981
  if (userWorkDir) {
53562
- const u = path49.resolve(userWorkDir);
53563
- const usep = u.endsWith(path49.sep) ? "" : path49.sep;
53982
+ const u = path53.resolve(userWorkDir);
53983
+ const usep = u.endsWith(path53.sep) ? "" : path53.sep;
53564
53984
  if (target === u || target.startsWith(u + usep)) return true;
53565
53985
  }
53566
- const root = path49.resolve(personaRoot);
53567
- const sep3 = root.endsWith(path49.sep) ? "" : path49.sep;
53986
+ const root = path53.resolve(personaRoot);
53987
+ const sep3 = root.endsWith(path53.sep) ? "" : path53.sep;
53568
53988
  if (!target.startsWith(root + sep3)) return false;
53569
- const rel = path49.relative(root, target);
53989
+ const rel = path53.relative(root, target);
53570
53990
  if (!rel || rel.startsWith("..")) return false;
53571
- const personaId = rel.split(path49.sep)[0];
53991
+ const personaId = rel.split(path53.sep)[0];
53572
53992
  if (!personaId) return false;
53573
53993
  const allowed = getAllowedPersonaIds(grants, action);
53574
53994
  if (allowed === "*") return true;
53575
53995
  return allowed.has(personaId);
53576
53996
  }
53577
53997
  function personaIdFromPath(absPath, personaRoot) {
53578
- const root = path49.resolve(personaRoot);
53579
- const target = path49.resolve(absPath);
53580
- const sep3 = root.endsWith(path49.sep) ? "" : path49.sep;
53998
+ const root = path53.resolve(personaRoot);
53999
+ const target = path53.resolve(absPath);
54000
+ const sep3 = root.endsWith(path53.sep) ? "" : path53.sep;
53581
54001
  if (!target.startsWith(root + sep3)) return null;
53582
- const rel = path49.relative(root, target);
54002
+ const rel = path53.relative(root, target);
53583
54003
  if (!rel || rel.startsWith("..")) return null;
53584
- const id = rel.split(path49.sep)[0];
54004
+ const id = rel.split(path53.sep)[0];
53585
54005
  return id || null;
53586
54006
  }
53587
54007
  function isPathWithin(dir, absPath) {
53588
- const d = path49.resolve(dir);
53589
- const t = path49.resolve(absPath);
53590
- const sep3 = d.endsWith(path49.sep) ? "" : path49.sep;
54008
+ const d = path53.resolve(dir);
54009
+ const t = path53.resolve(absPath);
54010
+ const sep3 = d.endsWith(path53.sep) ? "" : path53.sep;
53591
54011
  return t === d || t.startsWith(d + sep3);
53592
54012
  }
53593
54013
  function isPathInGuestBoundary(personaRoot, personaId, userWorkDir, absPath) {
53594
54014
  if (userWorkDir && isPathWithin(userWorkDir, absPath)) return true;
53595
- return personaIdFromPath(path49.resolve(absPath), personaRoot) === personaId;
54015
+ return personaIdFromPath(path53.resolve(absPath), personaRoot) === personaId;
53596
54016
  }
53597
54017
 
53598
54018
  // src/handlers/history.ts
@@ -53618,7 +54038,7 @@ function buildHistoryHandlers(deps) {
53618
54038
  if (!pid) return false;
53619
54039
  return isGuestPathAllowed(
53620
54040
  ctx.grants,
53621
- path50.join(personaRoot, pid),
54041
+ path54.join(personaRoot, pid),
53622
54042
  personaRoot,
53623
54043
  "read",
53624
54044
  userWorkDir
@@ -53630,7 +54050,7 @@ function buildHistoryHandlers(deps) {
53630
54050
  };
53631
54051
  const list = async (frame, _client, ctx) => {
53632
54052
  const args = HistoryListArgs.parse(frame);
53633
- assertGuestPath(ctx, path50.resolve(args.projectPath), personaRoot, "history:list");
54053
+ assertGuestPath(ctx, path54.resolve(args.projectPath), personaRoot, "history:list");
53634
54054
  const sessions = await history.listSessions(args);
53635
54055
  return { response: { type: "history:list", sessions } };
53636
54056
  };
@@ -53662,13 +54082,13 @@ function buildHistoryHandlers(deps) {
53662
54082
  };
53663
54083
  const subagents = async (frame, _client, ctx) => {
53664
54084
  const args = HistorySubagentsArgs.parse(frame);
53665
- assertGuestPath(ctx, path50.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
54085
+ assertGuestPath(ctx, path54.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
53666
54086
  const subs = await history.listSubagents(args);
53667
54087
  return { response: { type: "history:subagents", subagents: subs } };
53668
54088
  };
53669
54089
  const subagentRead = async (frame, _client, ctx) => {
53670
54090
  const args = HistorySubagentReadArgs.parse(frame);
53671
- assertGuestPath(ctx, path50.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
54091
+ assertGuestPath(ctx, path54.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
53672
54092
  const res = await history.readSubagent(args);
53673
54093
  return { response: { type: "history:subagent-read", ...res } };
53674
54094
  };
@@ -53677,7 +54097,7 @@ function buildHistoryHandlers(deps) {
53677
54097
  if (ctx?.principal.kind === "guest" && personaRoot) {
53678
54098
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
53679
54099
  const filtered = dirs.filter(
53680
- (d) => isGuestPathAllowed(ctx.grants, path50.resolve(d.cwd), personaRoot, "read", userWorkDir)
54100
+ (d) => isGuestPathAllowed(ctx.grants, path54.resolve(d.cwd), personaRoot, "read", userWorkDir)
53681
54101
  );
53682
54102
  return { response: { type: "history:recentDirs", dirs: filtered } };
53683
54103
  }
@@ -53694,7 +54114,7 @@ function buildHistoryHandlers(deps) {
53694
54114
  }
53695
54115
 
53696
54116
  // src/handlers/workspace.ts
53697
- var path51 = __toESM(require("path"), 1);
54117
+ var path55 = __toESM(require("path"), 1);
53698
54118
  var os16 = __toESM(require("os"), 1);
53699
54119
  init_protocol();
53700
54120
  init_protocol();
@@ -53736,22 +54156,22 @@ function buildWorkspaceHandlers(deps) {
53736
54156
  const args = WorkspaceListArgs.parse(frame);
53737
54157
  const isGuest = ctx?.principal.kind === "guest";
53738
54158
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os16.homedir();
53739
- const resolvedCwd = path51.resolve(args.cwd ?? fallbackCwd);
53740
- const target = args.path ? path51.resolve(resolvedCwd, args.path) : resolvedCwd;
54159
+ const resolvedCwd = path55.resolve(args.cwd ?? fallbackCwd);
54160
+ const target = args.path ? path55.resolve(resolvedCwd, args.path) : resolvedCwd;
53741
54161
  assertGuestPath2(ctx, target, personaRoot, "workspace:list", usersRoot);
53742
54162
  const res = workspace.list({ ...args, cwd: resolvedCwd });
53743
54163
  return { response: { type: "workspace:list", ...res } };
53744
54164
  };
53745
54165
  const read = async (frame, _client, ctx) => {
53746
54166
  const args = WorkspaceReadArgs.parse(frame);
53747
- const target = path51.isAbsolute(args.path) ? path51.resolve(args.path) : path51.resolve(args.cwd, args.path);
54167
+ const target = path55.isAbsolute(args.path) ? path55.resolve(args.path) : path55.resolve(args.cwd, args.path);
53748
54168
  assertGuestPath2(ctx, target, personaRoot, "workspace:read", usersRoot);
53749
54169
  const res = workspace.read(args);
53750
54170
  return { response: { type: "workspace:read", ...res } };
53751
54171
  };
53752
54172
  const skillsList = async (frame, _client, ctx) => {
53753
54173
  const args = SkillsListArgs.parse(frame);
53754
- const cwdAbs = path51.resolve(args.cwd);
54174
+ const cwdAbs = path55.resolve(args.cwd);
53755
54175
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list", usersRoot);
53756
54176
  const list2 = await getSkillsForTool(args.tool ?? "claude", cwdAbs);
53757
54177
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -53763,7 +54183,7 @@ function buildWorkspaceHandlers(deps) {
53763
54183
  };
53764
54184
  const agentsList = async (frame, _client, ctx) => {
53765
54185
  const args = AgentsListArgs.parse(frame);
53766
- const cwdAbs = path51.resolve(args.cwd);
54186
+ const cwdAbs = path55.resolve(args.cwd);
53767
54187
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list", usersRoot);
53768
54188
  if (args.tool === "codex") {
53769
54189
  return { response: { type: "agents:list", agents: [] } };
@@ -53785,20 +54205,20 @@ function buildWorkspaceHandlers(deps) {
53785
54205
  }
53786
54206
 
53787
54207
  // src/handlers/git.ts
53788
- var path53 = __toESM(require("path"), 1);
54208
+ var path57 = __toESM(require("path"), 1);
53789
54209
  init_protocol();
53790
54210
  init_protocol();
53791
54211
 
53792
54212
  // src/workspace/git.ts
53793
- var import_node_child_process10 = require("child_process");
53794
- var import_node_fs38 = __toESM(require("fs"), 1);
53795
- var import_node_path40 = __toESM(require("path"), 1);
54213
+ var import_node_child_process12 = require("child_process");
54214
+ var import_node_fs42 = __toESM(require("fs"), 1);
54215
+ var import_node_path44 = __toESM(require("path"), 1);
53796
54216
  var import_node_util = require("util");
53797
- var pexec = (0, import_node_util.promisify)(import_node_child_process10.execFile);
54217
+ var pexec = (0, import_node_util.promisify)(import_node_child_process12.execFile);
53798
54218
  function normalizePath(p2) {
53799
- const resolved = import_node_path40.default.resolve(p2);
54219
+ const resolved = import_node_path44.default.resolve(p2);
53800
54220
  try {
53801
- return import_node_fs38.default.realpathSync(resolved);
54221
+ return import_node_fs42.default.realpathSync(resolved);
53802
54222
  } catch {
53803
54223
  return resolved;
53804
54224
  }
@@ -53872,7 +54292,7 @@ async function listGitBranches(cwd) {
53872
54292
  function assertGuestCwd(ctx, cwd, personaRoot, method, usersRoot) {
53873
54293
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
53874
54294
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
53875
- if (!isGuestPathAllowed(ctx.grants, path53.resolve(cwd), personaRoot, "read", userWorkDir)) {
54295
+ if (!isGuestPathAllowed(ctx.grants, path57.resolve(cwd), personaRoot, "read", userWorkDir)) {
53876
54296
  throw new ClawdError(
53877
54297
  ERROR_CODES.UNAUTHORIZED,
53878
54298
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -54205,6 +54625,128 @@ function buildContactHandlers(deps) {
54205
54625
  };
54206
54626
  }
54207
54627
 
54628
+ // src/handlers/contact-ssh.ts
54629
+ init_protocol();
54630
+
54631
+ // src/sshd/key-issue.ts
54632
+ var import_node_fs43 = __toESM(require("fs"), 1);
54633
+ var import_node_path45 = __toESM(require("path"), 1);
54634
+ var import_node_child_process13 = require("child_process");
54635
+ function safeDeviceId(deviceId) {
54636
+ return deviceId.replace(/[\/\\]/g, "_");
54637
+ }
54638
+ async function issueContactSshKey(deviceId, sshdDir, opts = {}) {
54639
+ const safeId = safeDeviceId(deviceId);
54640
+ const keysDir = import_node_path45.default.join(sshdDir, "keys");
54641
+ import_node_fs43.default.mkdirSync(keysDir, { recursive: true, mode: 448 });
54642
+ const privPath = import_node_path45.default.join(keysDir, `${safeId}.ed25519`);
54643
+ const pubPath = `${privPath}.pub`;
54644
+ if (import_node_fs43.default.existsSync(privPath) && import_node_fs43.default.existsSync(pubPath)) {
54645
+ return {
54646
+ privateKeyPem: import_node_fs43.default.readFileSync(privPath, "utf8"),
54647
+ publicKeyLine: import_node_fs43.default.readFileSync(pubPath, "utf8").trim()
54648
+ };
54649
+ }
54650
+ const bin = opts.keygenBin ?? "/usr/bin/ssh-keygen";
54651
+ await new Promise((resolve6, reject) => {
54652
+ const p2 = (opts.spawnImpl ?? import_node_child_process13.spawn)(
54653
+ bin,
54654
+ ["-t", "ed25519", "-f", privPath, "-N", "", "-q", "-C", `clawd-contact-${safeId}`],
54655
+ { stdio: "ignore" }
54656
+ );
54657
+ p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
54658
+ p2.on("error", reject);
54659
+ });
54660
+ try {
54661
+ import_node_fs43.default.chmodSync(privPath, 384);
54662
+ } catch {
54663
+ }
54664
+ try {
54665
+ import_node_fs43.default.chmodSync(pubPath, 420);
54666
+ } catch {
54667
+ }
54668
+ return {
54669
+ privateKeyPem: import_node_fs43.default.readFileSync(privPath, "utf8"),
54670
+ publicKeyLine: import_node_fs43.default.readFileSync(pubPath, "utf8").trim()
54671
+ };
54672
+ }
54673
+
54674
+ // src/handlers/contact-ssh.ts
54675
+ function ensureOwner2(ctx) {
54676
+ if (!ctx || ctx.principal.kind !== "owner") {
54677
+ throw new ClawdError(
54678
+ ERROR_CODES.UNAUTHORIZED,
54679
+ "UNAUTHORIZED: contact:setSshAccess requires owner ctx"
54680
+ );
54681
+ }
54682
+ }
54683
+ function ensureGuest(ctx) {
54684
+ if (!ctx || ctx.principal.kind !== "guest") {
54685
+ throw new ClawdError(
54686
+ ERROR_CODES.UNAUTHORIZED,
54687
+ "UNAUTHORIZED: contact:sshKey:issue requires guest ctx"
54688
+ );
54689
+ }
54690
+ return ctx.principal.id;
54691
+ }
54692
+ function buildContactSshHandlers(deps) {
54693
+ const setSshAccess = async (frame, _client, ctx) => {
54694
+ ensureOwner2(ctx);
54695
+ const { type: _t, requestId: _r, ...rest } = frame;
54696
+ const args = ContactSetSshAccessArgsSchema.parse(rest);
54697
+ const hit = deps.store.setSshAccess(args.deviceId, {
54698
+ sshAllowed: args.sshAllowed,
54699
+ exposedDirs: args.exposedDirs
54700
+ });
54701
+ if (!hit) {
54702
+ throw new ClawdError(
54703
+ ERROR_CODES.CONTACT_NOT_FOUND,
54704
+ `CONTACT_NOT_FOUND: contact ${args.deviceId} not in store`
54705
+ );
54706
+ }
54707
+ rebuildAuthorizedKeys(deps.store, deps.sshdDir);
54708
+ deps.broadcast({
54709
+ type: "contact:ssh-access-updated",
54710
+ deviceId: args.deviceId,
54711
+ sshAllowed: args.sshAllowed,
54712
+ exposedDirs: args.exposedDirs
54713
+ });
54714
+ return {
54715
+ response: {
54716
+ type: "contact:setSshAccess:ok",
54717
+ deviceId: args.deviceId,
54718
+ sshAllowed: args.sshAllowed,
54719
+ exposedDirs: args.exposedDirs
54720
+ }
54721
+ };
54722
+ };
54723
+ const sshKeyIssue = async (frame, _client, ctx) => {
54724
+ const callerDeviceId = ensureGuest(ctx);
54725
+ const { type: _t, requestId: _r, ...rest } = frame;
54726
+ ContactSshKeyIssueArgsSchema.parse(rest);
54727
+ const contact = deps.store.get(callerDeviceId);
54728
+ if (!contact || !contact.sshAllowed) {
54729
+ throw new ClawdError(
54730
+ ERROR_CODES.UNAUTHORIZED,
54731
+ `UNAUTHORIZED: contact ${callerDeviceId} not authorized for SSH`
54732
+ );
54733
+ }
54734
+ const { privateKeyPem, publicKeyLine } = await issueContactSshKey(callerDeviceId, deps.sshdDir);
54735
+ rebuildAuthorizedKeys(deps.store, deps.sshdDir);
54736
+ return {
54737
+ response: {
54738
+ type: "contact:sshKey:issue:ok",
54739
+ privateKeyPem,
54740
+ publicKeyLine
54741
+ }
54742
+ };
54743
+ };
54744
+ return {
54745
+ "contact:setSshAccess": setSshAccess,
54746
+ "contact:sshKey:issue": sshKeyIssue
54747
+ };
54748
+ }
54749
+
54208
54750
  // src/handlers/whoami.ts
54209
54751
  init_protocol();
54210
54752
  function buildWhoamiHandler(deps) {
@@ -54300,7 +54842,7 @@ function buildFeishuAuthHandlers(deps) {
54300
54842
 
54301
54843
  // src/handlers/device.ts
54302
54844
  init_protocol();
54303
- function ensureOwner2(ctx) {
54845
+ function ensureOwner3(ctx) {
54304
54846
  if (!ctx || ctx.principal.kind !== "owner") {
54305
54847
  throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
54306
54848
  }
@@ -54308,14 +54850,14 @@ function ensureOwner2(ctx) {
54308
54850
  function buildDeviceHandlers(deps) {
54309
54851
  const now = deps.now ?? Date.now;
54310
54852
  const list = async (_frame, _client, ctx) => {
54311
- ensureOwner2(ctx);
54853
+ ensureOwner3(ctx);
54312
54854
  const devices = await deps.listDevices();
54313
54855
  return {
54314
54856
  response: { type: "device:list:ok", devices }
54315
54857
  };
54316
54858
  };
54317
54859
  const connect = async (frame, _client, ctx) => {
54318
- ensureOwner2(ctx);
54860
+ ensureOwner3(ctx);
54319
54861
  const { type: _t, requestId: _r, ...rest } = frame;
54320
54862
  const args = DeviceConnectArgsSchema.parse(rest);
54321
54863
  const exchanged = await deps.exchange(args.deviceId);
@@ -54358,7 +54900,9 @@ function buildDeviceHandlers(deps) {
54358
54900
  connectToken: exchanged.token,
54359
54901
  grants: wh.grants,
54360
54902
  addedAt: now(),
54361
- pinnedAt: null
54903
+ pinnedAt: null,
54904
+ sshAllowed: false,
54905
+ exposedDirs: []
54362
54906
  };
54363
54907
  deps.store.upsert(contact);
54364
54908
  deps.broadcast({ type: "contact:added", contact });
@@ -54514,7 +55058,7 @@ function buildPersonaHandlers(deps) {
54514
55058
  }
54515
55059
 
54516
55060
  // src/handlers/attachment.ts
54517
- var import_node_path41 = __toESM(require("path"), 1);
55061
+ var import_node_path46 = __toESM(require("path"), 1);
54518
55062
  init_protocol();
54519
55063
  init_protocol();
54520
55064
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -54594,12 +55138,12 @@ function buildAttachmentHandlers(deps) {
54594
55138
  `session ${args.sessionId} scope unresolved`
54595
55139
  );
54596
55140
  }
54597
- const cwdAbs = import_node_path41.default.resolve(sessionFile.cwd);
54598
- const candidateAbs = import_node_path41.default.isAbsolute(args.relPath) ? import_node_path41.default.resolve(args.relPath) : import_node_path41.default.resolve(cwdAbs, args.relPath);
55141
+ const cwdAbs = import_node_path46.default.resolve(sessionFile.cwd);
55142
+ const candidateAbs = import_node_path46.default.isAbsolute(args.relPath) ? import_node_path46.default.resolve(args.relPath) : import_node_path46.default.resolve(cwdAbs, args.relPath);
54599
55143
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.signUrl", "group-acl");
54600
55144
  const entries = deps.groupFileStore.list(scope, args.sessionId);
54601
55145
  const entry = entries.find((e) => {
54602
- const storedAbs = import_node_path41.default.isAbsolute(e.relPath) ? import_node_path41.default.resolve(e.relPath) : import_node_path41.default.resolve(cwdAbs, e.relPath);
55146
+ const storedAbs = import_node_path46.default.isAbsolute(e.relPath) ? import_node_path46.default.resolve(e.relPath) : import_node_path46.default.resolve(cwdAbs, e.relPath);
54603
55147
  return storedAbs === candidateAbs && !e.stale;
54604
55148
  });
54605
55149
  if (!entry) {
@@ -54624,7 +55168,7 @@ function buildAttachmentHandlers(deps) {
54624
55168
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
54625
55169
  const f = deps.sessionStore.read(sessionId);
54626
55170
  if (!f) return;
54627
- assertGuestAttachmentPath(ctx, import_node_path41.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
55171
+ assertGuestAttachmentPath(ctx, import_node_path46.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
54628
55172
  }
54629
55173
  const groupAdd = async (frame, _client, ctx) => {
54630
55174
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -54639,8 +55183,8 @@ function buildAttachmentHandlers(deps) {
54639
55183
  if (!scope) {
54640
55184
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
54641
55185
  }
54642
- const cwdAbs = import_node_path41.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
54643
- const candidateAbs = import_node_path41.default.isAbsolute(args.relPath) ? import_node_path41.default.resolve(args.relPath) : import_node_path41.default.resolve(cwdAbs, args.relPath);
55186
+ const cwdAbs = import_node_path46.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
55187
+ const candidateAbs = import_node_path46.default.isAbsolute(args.relPath) ? import_node_path46.default.resolve(args.relPath) : import_node_path46.default.resolve(cwdAbs, args.relPath);
54644
55188
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.groupAdd", "cwd-subtree");
54645
55189
  const from = ctx?.principal.kind === "owner" ? "owner" : "agent";
54646
55190
  const size = 0;
@@ -54699,19 +55243,19 @@ function buildAttachmentHandlers(deps) {
54699
55243
 
54700
55244
  // src/handlers/extension.ts
54701
55245
  var import_promises8 = __toESM(require("fs/promises"), 1);
54702
- var import_node_path46 = __toESM(require("path"), 1);
55246
+ var import_node_path51 = __toESM(require("path"), 1);
54703
55247
  init_protocol();
54704
55248
 
54705
55249
  // src/extension/bundle-zip.ts
54706
55250
  var import_promises5 = __toESM(require("fs/promises"), 1);
54707
- var import_node_path42 = __toESM(require("path"), 1);
54708
- var import_node_crypto14 = __toESM(require("crypto"), 1);
55251
+ var import_node_path47 = __toESM(require("path"), 1);
55252
+ var import_node_crypto13 = __toESM(require("crypto"), 1);
54709
55253
  var import_jszip2 = __toESM(require_lib3(), 1);
54710
55254
  async function bundleExtensionDir(dir) {
54711
55255
  const entries = await listFilesSorted(dir);
54712
55256
  const zip = new import_jszip2.default();
54713
55257
  for (const rel of entries) {
54714
- const abs = import_node_path42.default.join(dir, rel);
55258
+ const abs = import_node_path47.default.join(dir, rel);
54715
55259
  const content = await import_promises5.default.readFile(abs);
54716
55260
  zip.file(rel, content, { date: FIXED_DATE });
54717
55261
  }
@@ -54720,7 +55264,7 @@ async function bundleExtensionDir(dir) {
54720
55264
  compression: "DEFLATE",
54721
55265
  compressionOptions: { level: 6 }
54722
55266
  });
54723
- const sha256 = import_node_crypto14.default.createHash("sha256").update(buffer).digest("hex");
55267
+ const sha256 = import_node_crypto13.default.createHash("sha256").update(buffer).digest("hex");
54724
55268
  return { buffer, sha256 };
54725
55269
  }
54726
55270
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -54732,7 +55276,7 @@ async function listFilesSorted(rootDir) {
54732
55276
  return out;
54733
55277
  }
54734
55278
  async function walk(absRoot, relPrefix, out) {
54735
- const dirAbs = import_node_path42.default.join(absRoot, relPrefix);
55279
+ const dirAbs = import_node_path47.default.join(absRoot, relPrefix);
54736
55280
  const entries = await import_promises5.default.readdir(dirAbs, { withFileTypes: true });
54737
55281
  for (const e of entries) {
54738
55282
  if (IGNORE_BASENAMES.has(e.name)) continue;
@@ -54786,25 +55330,25 @@ function computePublishCheck(args) {
54786
55330
 
54787
55331
  // src/extension/install-flow.ts
54788
55332
  var import_promises6 = __toESM(require("fs/promises"), 1);
54789
- var import_node_path44 = __toESM(require("path"), 1);
55333
+ var import_node_path49 = __toESM(require("path"), 1);
54790
55334
  var import_node_os19 = __toESM(require("os"), 1);
54791
- var import_node_crypto15 = __toESM(require("crypto"), 1);
55335
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
54792
55336
  var import_jszip3 = __toESM(require_lib3(), 1);
54793
55337
 
54794
55338
  // src/extension/paths.ts
54795
55339
  var import_node_os18 = __toESM(require("os"), 1);
54796
- var import_node_path43 = __toESM(require("path"), 1);
55340
+ var import_node_path48 = __toESM(require("path"), 1);
54797
55341
  function clawdHomeRoot(override) {
54798
- return override ?? process.env.CLAWD_HOME ?? import_node_path43.default.join(import_node_os18.default.homedir(), ".clawd");
55342
+ return override ?? process.env.CLAWD_HOME ?? import_node_path48.default.join(import_node_os18.default.homedir(), ".clawd");
54799
55343
  }
54800
55344
  function extensionsRoot(override) {
54801
- return import_node_path43.default.join(clawdHomeRoot(override), "extensions");
55345
+ return import_node_path48.default.join(clawdHomeRoot(override), "extensions");
54802
55346
  }
54803
55347
  function publishedChannelsFile(override) {
54804
- return import_node_path43.default.join(clawdHomeRoot(override), "extensions-published.json");
55348
+ return import_node_path48.default.join(clawdHomeRoot(override), "extensions-published.json");
54805
55349
  }
54806
55350
  function bundleCacheRoot(override) {
54807
- return import_node_path43.default.join(clawdHomeRoot(override), "extension-bundles");
55351
+ return import_node_path48.default.join(clawdHomeRoot(override), "extension-bundles");
54808
55352
  }
54809
55353
 
54810
55354
  // src/extension/install-flow.ts
@@ -54817,7 +55361,7 @@ var InstallError = class extends Error {
54817
55361
  };
54818
55362
  async function installFromChannel(args, deps) {
54819
55363
  const { channelRef, snapshotHash, bundleZip } = args;
54820
- const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
55364
+ const computed = import_node_crypto14.default.createHash("sha256").update(bundleZip).digest("hex");
54821
55365
  if (computed !== snapshotHash) {
54822
55366
  throw new InstallError(
54823
55367
  "HASH_MISMATCH",
@@ -54831,7 +55375,7 @@ async function installFromChannel(args, deps) {
54831
55375
  throw new InstallError("ZIP_INVALID", `failed to load zip: ${e.message}`);
54832
55376
  }
54833
55377
  for (const name of Object.keys(zip.files)) {
54834
- if (name.includes("..") || name.startsWith("/") || import_node_path44.default.isAbsolute(name)) {
55378
+ if (name.includes("..") || name.startsWith("/") || import_node_path49.default.isAbsolute(name)) {
54835
55379
  throw new InstallError("ZIP_INVALID", `unsafe zip entry: ${name}`);
54836
55380
  }
54837
55381
  }
@@ -54863,7 +55407,7 @@ async function installFromChannel(args, deps) {
54863
55407
  );
54864
55408
  }
54865
55409
  const localExtId = namespacedExtId(ownerSlug, channelRef.ownerPrincipalId);
54866
- const destDir = import_node_path44.default.join(deps.extensionsRoot, localExtId);
55410
+ const destDir = import_node_path49.default.join(deps.extensionsRoot, localExtId);
54867
55411
  let destExists = false;
54868
55412
  try {
54869
55413
  await import_promises6.default.access(destDir);
@@ -54877,16 +55421,16 @@ async function installFromChannel(args, deps) {
54877
55421
  );
54878
55422
  }
54879
55423
  const stage = await import_promises6.default.mkdtemp(
54880
- import_node_path44.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
55424
+ import_node_path49.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
54881
55425
  );
54882
55426
  try {
54883
55427
  for (const [name, entry] of Object.entries(zip.files)) {
54884
- const dest = import_node_path44.default.join(stage, name);
55428
+ const dest = import_node_path49.default.join(stage, name);
54885
55429
  if (entry.dir) {
54886
55430
  await import_promises6.default.mkdir(dest, { recursive: true });
54887
55431
  continue;
54888
55432
  }
54889
- await import_promises6.default.mkdir(import_node_path44.default.dirname(dest), { recursive: true });
55433
+ await import_promises6.default.mkdir(import_node_path49.default.dirname(dest), { recursive: true });
54890
55434
  if (name === "manifest.json") {
54891
55435
  const rewritten = { ...parsed.data, id: localExtId };
54892
55436
  await import_promises6.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -54907,9 +55451,9 @@ async function installFromChannel(args, deps) {
54907
55451
 
54908
55452
  // src/extension/update-flow.ts
54909
55453
  var import_promises7 = __toESM(require("fs/promises"), 1);
54910
- var import_node_path45 = __toESM(require("path"), 1);
55454
+ var import_node_path50 = __toESM(require("path"), 1);
54911
55455
  var import_node_os20 = __toESM(require("os"), 1);
54912
- var import_node_crypto16 = __toESM(require("crypto"), 1);
55456
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
54913
55457
  var import_jszip4 = __toESM(require_lib3(), 1);
54914
55458
  var UpdateError = class extends Error {
54915
55459
  constructor(code, message) {
@@ -54924,11 +55468,11 @@ async function updateFromChannel(args, deps) {
54924
55468
  channelRef.extId,
54925
55469
  channelRef.ownerPrincipalId
54926
55470
  );
54927
- const liveDir = import_node_path45.default.join(deps.extensionsRoot, localExtId);
55471
+ const liveDir = import_node_path50.default.join(deps.extensionsRoot, localExtId);
54928
55472
  const prevDir = `${liveDir}.prev`;
54929
55473
  let existingVersion;
54930
55474
  try {
54931
- const raw = await import_promises7.default.readFile(import_node_path45.default.join(liveDir, "manifest.json"), "utf8");
55475
+ const raw = await import_promises7.default.readFile(import_node_path50.default.join(liveDir, "manifest.json"), "utf8");
54932
55476
  const parsed2 = ExtensionManifestSchema.safeParse(JSON.parse(raw));
54933
55477
  if (!parsed2.success) {
54934
55478
  throw new UpdateError(
@@ -54947,7 +55491,7 @@ async function updateFromChannel(args, deps) {
54947
55491
  if (e instanceof UpdateError) throw e;
54948
55492
  throw e;
54949
55493
  }
54950
- const computed = import_node_crypto16.default.createHash("sha256").update(bundleZip).digest("hex");
55494
+ const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
54951
55495
  if (computed !== snapshotHash) {
54952
55496
  throw new UpdateError(
54953
55497
  "HASH_MISMATCH",
@@ -54961,7 +55505,7 @@ async function updateFromChannel(args, deps) {
54961
55505
  throw new UpdateError("ZIP_INVALID", `failed to load zip: ${e.message}`);
54962
55506
  }
54963
55507
  for (const name of Object.keys(zip.files)) {
54964
- if (name.includes("..") || name.startsWith("/") || import_node_path45.default.isAbsolute(name)) {
55508
+ if (name.includes("..") || name.startsWith("/") || import_node_path50.default.isAbsolute(name)) {
54965
55509
  throw new UpdateError("ZIP_INVALID", `unsafe zip entry: ${name}`);
54966
55510
  }
54967
55511
  }
@@ -54996,16 +55540,16 @@ async function updateFromChannel(args, deps) {
54996
55540
  await import_promises7.default.rm(prevDir, { recursive: true, force: true });
54997
55541
  await import_promises7.default.rename(liveDir, prevDir);
54998
55542
  const stage = await import_promises7.default.mkdtemp(
54999
- import_node_path45.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
55543
+ import_node_path50.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
55000
55544
  );
55001
55545
  try {
55002
55546
  for (const [name, entry] of Object.entries(zip.files)) {
55003
- const dest = import_node_path45.default.join(stage, name);
55547
+ const dest = import_node_path50.default.join(stage, name);
55004
55548
  if (entry.dir) {
55005
55549
  await import_promises7.default.mkdir(dest, { recursive: true });
55006
55550
  continue;
55007
55551
  }
55008
- await import_promises7.default.mkdir(import_node_path45.default.dirname(dest), { recursive: true });
55552
+ await import_promises7.default.mkdir(import_node_path50.default.dirname(dest), { recursive: true });
55009
55553
  if (name === "manifest.json") {
55010
55554
  const rewritten = { ...parsed.data, id: localExtId };
55011
55555
  await import_promises7.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -55098,7 +55642,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55098
55642
  );
55099
55643
  }
55100
55644
  }
55101
- const manifestPath = import_node_path46.default.join(root, extId, "manifest.json");
55645
+ const manifestPath = import_node_path51.default.join(root, extId, "manifest.json");
55102
55646
  const manifest = await readManifest(root, extId);
55103
55647
  const next = { ...manifest, version: newVersion };
55104
55648
  const tmp = `${manifestPath}.tmp`;
@@ -55106,7 +55650,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55106
55650
  await import_promises8.default.rename(tmp, manifestPath);
55107
55651
  }
55108
55652
  async function readManifest(root, extId) {
55109
- const file = import_node_path46.default.join(root, extId, "manifest.json");
55653
+ const file = import_node_path51.default.join(root, extId, "manifest.json");
55110
55654
  let raw;
55111
55655
  try {
55112
55656
  raw = await import_promises8.default.readFile(file, "utf8");
@@ -55197,7 +55741,7 @@ function buildExtensionHandlers(deps) {
55197
55741
  };
55198
55742
  async function buildSnapshotMeta(extId) {
55199
55743
  const manifest = await readManifest(deps.root, extId);
55200
- const { sha256, buffer } = await bundleExtensionDir(import_node_path46.default.join(deps.root, extId));
55744
+ const { sha256, buffer } = await bundleExtensionDir(import_node_path51.default.join(deps.root, extId));
55201
55745
  return { manifest, contentHash: sha256, buffer };
55202
55746
  }
55203
55747
  const publish = async (frame, _client, ctx) => {
@@ -55378,9 +55922,9 @@ function buildExtensionHandlers(deps) {
55378
55922
  }
55379
55923
 
55380
55924
  // src/app-builder/project-store.ts
55381
- var import_node_fs39 = require("fs");
55382
- var import_node_child_process11 = require("child_process");
55383
- var import_node_path47 = require("path");
55925
+ var import_node_fs44 = require("fs");
55926
+ var import_node_child_process14 = require("child_process");
55927
+ var import_node_path52 = require("path");
55384
55928
  init_protocol();
55385
55929
  var PROJECTS_DIR = "projects";
55386
55930
  var META_FILE = ".clawd-project.json";
@@ -55394,19 +55938,19 @@ var ProjectStore = class {
55394
55938
  root;
55395
55939
  /** projects/<name>/.clawd-project.json 路径 */
55396
55940
  metaPath(name) {
55397
- return (0, import_node_path47.join)(this.projectsRoot(), name, META_FILE);
55941
+ return (0, import_node_path52.join)(this.projectsRoot(), name, META_FILE);
55398
55942
  }
55399
55943
  /** projects/<name>/ 目录路径(cwd 用) */
55400
55944
  projectDir(name) {
55401
- return (0, import_node_path47.join)(this.projectsRoot(), name);
55945
+ return (0, import_node_path52.join)(this.projectsRoot(), name);
55402
55946
  }
55403
55947
  projectsRoot() {
55404
- return (0, import_node_path47.join)(this.root, PROJECTS_DIR);
55948
+ return (0, import_node_path52.join)(this.root, PROJECTS_DIR);
55405
55949
  }
55406
55950
  async list() {
55407
55951
  let entries;
55408
55952
  try {
55409
- entries = await import_node_fs39.promises.readdir(this.projectsRoot());
55953
+ entries = await import_node_fs44.promises.readdir(this.projectsRoot());
55410
55954
  } catch (err) {
55411
55955
  if (err.code === "ENOENT") return [];
55412
55956
  throw err;
@@ -55414,7 +55958,7 @@ var ProjectStore = class {
55414
55958
  const out = [];
55415
55959
  for (const name of entries) {
55416
55960
  try {
55417
- const raw = await import_node_fs39.promises.readFile(this.metaPath(name), "utf8");
55961
+ const raw = await import_node_fs44.promises.readFile(this.metaPath(name), "utf8");
55418
55962
  const json = JSON.parse(raw);
55419
55963
  let migrated = false;
55420
55964
  if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
@@ -55425,7 +55969,7 @@ var ProjectStore = class {
55425
55969
  if (parsed.success) {
55426
55970
  out.push(parsed.data);
55427
55971
  if (migrated) {
55428
- void import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
55972
+ void import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
55429
55973
  });
55430
55974
  }
55431
55975
  }
@@ -55469,8 +56013,8 @@ var ProjectStore = class {
55469
56013
  throw new Error(`invalid name "${name}": ${validated.error.message}`);
55470
56014
  }
55471
56015
  const dir = this.projectDir(name);
55472
- await import_node_fs39.promises.mkdir(dir, { recursive: true });
55473
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
56016
+ await import_node_fs44.promises.mkdir(dir, { recursive: true });
56017
+ await import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
55474
56018
  return meta;
55475
56019
  }
55476
56020
  /**
@@ -55484,7 +56028,7 @@ var ProjectStore = class {
55484
56028
  async scaffold(name, templateSrcDir, scaffoldScriptPath) {
55485
56029
  const destDir = this.projectDir(name);
55486
56030
  return await new Promise((resolve6, reject) => {
55487
- const child = (0, import_node_child_process11.spawn)("bash", [scaffoldScriptPath, name, templateSrcDir, destDir], {
56031
+ const child = (0, import_node_child_process14.spawn)("bash", [scaffoldScriptPath, name, templateSrcDir, destDir], {
55488
56032
  env: { ...process.env, PATH: process.env.PATH ?? "" },
55489
56033
  stdio: ["ignore", "pipe", "pipe"]
55490
56034
  });
@@ -55513,7 +56057,7 @@ var ProjectStore = class {
55513
56057
  }
55514
56058
  async delete(name) {
55515
56059
  const dir = this.projectDir(name);
55516
- await import_node_fs39.promises.rm(dir, { recursive: true, force: true });
56060
+ await import_node_fs44.promises.rm(dir, { recursive: true, force: true });
55517
56061
  }
55518
56062
  async updatePort(name, newPort) {
55519
56063
  if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
@@ -55529,7 +56073,7 @@ var ProjectStore = class {
55529
56073
  throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
55530
56074
  }
55531
56075
  const updated = { ...target, port: newPort };
55532
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
56076
+ await import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
55533
56077
  return updated;
55534
56078
  }
55535
56079
  /**
@@ -55546,7 +56090,7 @@ var ProjectStore = class {
55546
56090
  if (!validated.success) {
55547
56091
  throw new Error(`invalid prodUrl "${url}": ${validated.error.message}`);
55548
56092
  }
55549
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56093
+ await import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55550
56094
  return validated.data;
55551
56095
  }
55552
56096
  /**
@@ -55567,7 +56111,7 @@ var ProjectStore = class {
55567
56111
  if (!validated.success) {
55568
56112
  throw new Error(`invalid publishJob: ${validated.error.message}`);
55569
56113
  }
55570
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56114
+ await import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55571
56115
  return validated.data;
55572
56116
  }
55573
56117
  /** 清掉 .clawd-project.json.publishJob 字段。其他字段保持原样。 */
@@ -55582,13 +56126,13 @@ var ProjectStore = class {
55582
56126
  if (!validated.success) {
55583
56127
  throw new Error(`failed to clear publishJob: ${validated.error.message}`);
55584
56128
  }
55585
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56129
+ await import_node_fs44.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55586
56130
  return validated.data;
55587
56131
  }
55588
56132
  };
55589
56133
 
55590
56134
  // src/app-builder/kill-port.ts
55591
- var import_node_child_process12 = require("child_process");
56135
+ var import_node_child_process15 = require("child_process");
55592
56136
  async function killPortOccupants(port, ownedPids, logger) {
55593
56137
  let pids;
55594
56138
  try {
@@ -55630,7 +56174,7 @@ async function killPortOccupants(port, ownedPids, logger) {
55630
56174
  }
55631
56175
  function listPidsOnPort(port) {
55632
56176
  return new Promise((resolve6, reject) => {
55633
- (0, import_node_child_process12.execFile)(
56177
+ (0, import_node_child_process15.execFile)(
55634
56178
  "lsof",
55635
56179
  ["-ti", `:${port}`],
55636
56180
  { timeout: 3e3 },
@@ -55652,7 +56196,7 @@ function listPidsOnPort(port) {
55652
56196
  }
55653
56197
 
55654
56198
  // src/app-builder/publish-registry.ts
55655
- var import_node_crypto17 = require("crypto");
56199
+ var import_node_crypto16 = require("crypto");
55656
56200
  var PublishJobRegistry = class {
55657
56201
  jobs = /* @__PURE__ */ new Map();
55658
56202
  has(name) {
@@ -55669,7 +56213,7 @@ var PublishJobRegistry = class {
55669
56213
  if (this.jobs.has(args.name)) {
55670
56214
  throw new Error(`already publishing: ${args.name}`);
55671
56215
  }
55672
- const jobId = args.jobId ?? `job-${(0, import_node_crypto17.randomUUID)()}`;
56216
+ const jobId = args.jobId ?? `job-${(0, import_node_crypto16.randomUUID)()}`;
55673
56217
  this.jobs.set(args.name, {
55674
56218
  jobId,
55675
56219
  name: args.name,
@@ -55702,9 +56246,9 @@ var PublishJobRegistry = class {
55702
56246
  };
55703
56247
 
55704
56248
  // src/app-builder/publish-job-runner.ts
55705
- var import_node_child_process13 = require("child_process");
55706
- var import_node_fs40 = require("fs");
55707
- var import_node_path48 = require("path");
56249
+ var import_node_child_process16 = require("child_process");
56250
+ var import_node_fs45 = require("fs");
56251
+ var import_node_path53 = require("path");
55708
56252
 
55709
56253
  // src/app-builder/publish-stage-parser.ts
55710
56254
  var STAGE_RE = /^\s*::stage::(build|deploy|verify)\s*$/;
@@ -55731,19 +56275,19 @@ function tailStderrLines(buf, n) {
55731
56275
  // src/app-builder/publish-job-runner.ts
55732
56276
  async function startPublishJob(deps, args) {
55733
56277
  const { registry: registry2, projectDir } = deps;
55734
- const spawn12 = deps.spawnImpl ?? import_node_child_process13.spawn;
56278
+ const spawn15 = deps.spawnImpl ?? import_node_child_process16.spawn;
55735
56279
  if (registry2.has(args.name)) {
55736
56280
  return { jobId: registry2.get(args.name).jobId, status: "already-publishing" };
55737
56281
  }
55738
56282
  const projDir = projectDir(args.name);
55739
- const logPath = (0, import_node_path48.join)(projDir, ".publish.log");
56283
+ const logPath = (0, import_node_path53.join)(projDir, ".publish.log");
55740
56284
  let logStream = null;
55741
56285
  try {
55742
- logStream = (0, import_node_fs40.createWriteStream)(logPath, { flags: "w" });
56286
+ logStream = (0, import_node_fs45.createWriteStream)(logPath, { flags: "w" });
55743
56287
  } catch {
55744
56288
  logStream = null;
55745
56289
  }
55746
- const child = spawn12("bash", [args.scriptPath, projDir, args.personaRoot ?? ""], {
56290
+ const child = spawn15("bash", [args.scriptPath, projDir, args.personaRoot ?? ""], {
55747
56291
  cwd: projDir,
55748
56292
  env: process.env,
55749
56293
  stdio: ["ignore", "pipe", "pipe"]
@@ -55996,8 +56540,8 @@ async function recoverInterruptedJobs(deps) {
55996
56540
 
55997
56541
  // src/handlers/app-builder.ts
55998
56542
  init_protocol();
55999
- var import_node_path49 = require("path");
56000
- var import_node_fs41 = require("fs");
56543
+ var import_node_path54 = require("path");
56544
+ var import_node_fs46 = require("fs");
56001
56545
  var APP_BUILDER_PERSONAS = ["persona-app-builder", "persona-dataclaw-builder"];
56002
56546
  var DEV_SERVER_READY_TIMEOUT_MS = 3e4;
56003
56547
  async function recoverInterruptedPublishJobs(store, logger) {
@@ -56078,7 +56622,7 @@ function buildAppBuilderHandlers(deps) {
56078
56622
  async function listAllUsersProjects() {
56079
56623
  if (!deps.usersRoot || !deps.getStore) return [];
56080
56624
  const getStore = deps.getStore;
56081
- const userIds = await import_node_fs41.promises.readdir(deps.usersRoot).catch(() => []);
56625
+ const userIds = await import_node_fs46.promises.readdir(deps.usersRoot).catch(() => []);
56082
56626
  const perUser = await Promise.all(
56083
56627
  userIds.map((uid) => getStore(uid).list().catch(() => []))
56084
56628
  );
@@ -56154,8 +56698,8 @@ function buildAppBuilderHandlers(deps) {
56154
56698
  const project = await userStore.create(f.name, reservedPorts);
56155
56699
  try {
56156
56700
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(session.ownerPersonaId ?? "") : deps.personaRoot;
56157
- const templateSrcDir = (0, import_node_path49.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
56158
- const scaffoldScript = (0, import_node_path49.join)(deps.deployKitRoot, "scripts", "new-extension.sh");
56701
+ const templateSrcDir = (0, import_node_path54.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
56702
+ const scaffoldScript = (0, import_node_path54.join)(deps.deployKitRoot, "scripts", "new-extension.sh");
56159
56703
  const scaffoldResult = await userStore.scaffold(project.name, templateSrcDir, scaffoldScript);
56160
56704
  deps.logger?.info("app-builder.scaffold.done", {
56161
56705
  name: project.name,
@@ -56376,7 +56920,7 @@ function buildAppBuilderHandlers(deps) {
56376
56920
  await userStore.clearPublishJob(args.name);
56377
56921
  }
56378
56922
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(boundSession.ownerPersonaId ?? "") : deps.personaRoot;
56379
- const scriptPath = (0, import_node_path49.join)(deps.deployKitRoot, "scripts", "publish.sh");
56923
+ const scriptPath = (0, import_node_path54.join)(deps.deployKitRoot, "scripts", "publish.sh");
56380
56924
  deps.logger?.info("app-builder.publish.start", {
56381
56925
  name: args.name,
56382
56926
  sessionId: boundSession.sessionId,
@@ -56520,7 +57064,7 @@ function buildShiftHandlers(deps) {
56520
57064
 
56521
57065
  // src/handlers/visitor.ts
56522
57066
  init_protocol();
56523
- function ensureOwner3(ctx) {
57067
+ function ensureOwner4(ctx) {
56524
57068
  if (!ctx || ctx.principal.kind !== "owner") {
56525
57069
  throw new ClawdError(
56526
57070
  ERROR_CODES.UNAUTHORIZED,
@@ -56530,7 +57074,7 @@ function ensureOwner3(ctx) {
56530
57074
  }
56531
57075
  function buildVisitorHandlers(deps) {
56532
57076
  const list = async (_frame, _client, ctx) => {
56533
- ensureOwner3(ctx);
57077
+ ensureOwner4(ctx);
56534
57078
  return {
56535
57079
  response: {
56536
57080
  type: "visitor:list",
@@ -56545,7 +57089,7 @@ function buildVisitorHandlers(deps) {
56545
57089
 
56546
57090
  // src/extension/registry.ts
56547
57091
  var import_promises9 = __toESM(require("fs/promises"), 1);
56548
- var import_node_path50 = __toESM(require("path"), 1);
57092
+ var import_node_path55 = __toESM(require("path"), 1);
56549
57093
  async function loadAll(root) {
56550
57094
  let entries;
56551
57095
  try {
@@ -56558,13 +57102,13 @@ async function loadAll(root) {
56558
57102
  for (const ent of entries) {
56559
57103
  if (!ent.isDirectory()) continue;
56560
57104
  if (ent.name.startsWith(".")) continue;
56561
- records.push(await loadOne(import_node_path50.default.join(root, ent.name), ent.name));
57105
+ records.push(await loadOne(import_node_path55.default.join(root, ent.name), ent.name));
56562
57106
  }
56563
57107
  records.sort((a, b2) => a.extId < b2.extId ? -1 : a.extId > b2.extId ? 1 : 0);
56564
57108
  return records;
56565
57109
  }
56566
57110
  async function loadOne(dir, dirName) {
56567
- const manifestPath = import_node_path50.default.join(dir, "manifest.json");
57111
+ const manifestPath = import_node_path55.default.join(dir, "manifest.json");
56568
57112
  let raw;
56569
57113
  try {
56570
57114
  raw = await import_promises9.default.readFile(manifestPath, "utf8");
@@ -56609,7 +57153,7 @@ async function loadOne(dir, dirName) {
56609
57153
 
56610
57154
  // src/extension/uninstall.ts
56611
57155
  var import_promises10 = __toESM(require("fs/promises"), 1);
56612
- var import_node_path51 = __toESM(require("path"), 1);
57156
+ var import_node_path56 = __toESM(require("path"), 1);
56613
57157
  var UninstallError = class extends Error {
56614
57158
  constructor(code, message) {
56615
57159
  super(message);
@@ -56618,7 +57162,7 @@ var UninstallError = class extends Error {
56618
57162
  code;
56619
57163
  };
56620
57164
  async function uninstall(deps) {
56621
- const dir = import_node_path51.default.join(deps.root, deps.extId);
57165
+ const dir = import_node_path56.default.join(deps.root, deps.extId);
56622
57166
  try {
56623
57167
  await import_promises10.default.access(dir);
56624
57168
  } catch {
@@ -56629,7 +57173,7 @@ async function uninstall(deps) {
56629
57173
  }
56630
57174
 
56631
57175
  // src/handlers/index.ts
56632
- var import_node_crypto18 = require("crypto");
57176
+ var import_node_crypto17 = require("crypto");
56633
57177
  function buildMethodHandlers(deps) {
56634
57178
  return {
56635
57179
  ...buildSessionHandlers({
@@ -56662,7 +57206,7 @@ function buildMethodHandlers(deps) {
56662
57206
  const c = deps.contactStore.get(deviceId);
56663
57207
  return c ? { deviceId: c.deviceId, remoteUrl: c.remoteUrl, connectToken: c.connectToken } : null;
56664
57208
  },
56665
- genId: () => (0, import_node_crypto18.randomUUID)(),
57209
+ genId: () => (0, import_node_crypto17.randomUUID)(),
56666
57210
  now: () => Date.now(),
56667
57211
  forwardInboxPostToPeer,
56668
57212
  logger: deps.logger
@@ -56673,6 +57217,11 @@ function buildMethodHandlers(deps) {
56673
57217
  broadcast: deps.broadcastToOwners,
56674
57218
  now: () => Date.now()
56675
57219
  }),
57220
+ ...buildContactSshHandlers({
57221
+ store: deps.contactStore,
57222
+ broadcast: deps.broadcastToOwners,
57223
+ sshdDir: deps.sshdDir
57224
+ }),
56676
57225
  whoami: buildWhoamiHandler({
56677
57226
  ownerDisplayName: deps.ownerDisplayName,
56678
57227
  ownerPrincipalId: deps.ownerPrincipalId,
@@ -56719,7 +57268,7 @@ function buildMethodHandlers(deps) {
56719
57268
  }
56720
57269
 
56721
57270
  // src/app-builder/dev-server-supervisor.ts
56722
- var import_node_child_process14 = require("child_process");
57271
+ var import_node_child_process17 = require("child_process");
56723
57272
  var import_node_events2 = require("events");
56724
57273
  var DEFAULT_READY_PATTERN = /Local:\s+https?:\/\/|Nest application successfully started|server listening on/i;
56725
57274
  var DevServerSupervisor = class extends import_node_events2.EventEmitter {
@@ -56756,7 +57305,7 @@ var DevServerSupervisor = class extends import_node_events2.EventEmitter {
56756
57305
  tunnelHost: args.tunnelHost,
56757
57306
  devCommand: cmd
56758
57307
  });
56759
- const child = (0, import_node_child_process14.spawn)("sh", ["-c", cmd], {
57308
+ const child = (0, import_node_child_process17.spawn)("sh", ["-c", cmd], {
56760
57309
  cwd: args.cwd,
56761
57310
  env,
56762
57311
  stdio: "pipe",
@@ -57008,6 +57557,12 @@ var METHOD_GRANT_MAP = {
57008
57557
  "contact:list": ADMIN_ANY,
57009
57558
  "contact:pin": ADMIN_ANY,
57010
57559
  "contact:remove": ADMIN_ANY,
57560
+ // contact:setSshAccess (owner UI 配 SSH 授权):ADMIN_ANY
57561
+ // contact:sshKey:issue (guest daemon 拉自己的 privkey):public — handler 内校
57562
+ // ctx.principal.kind==='guest' + store.get(callerId).sshAllowed
57563
+ // (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
57564
+ "contact:setSshAccess": ADMIN_ANY,
57565
+ "contact:sshKey:issue": { kind: "public" },
57011
57566
  // ---- visitor:* (访客名单,owner-only) ----
57012
57567
  // owner 看完整访客名单(含没开会话的);guest 不可调(handler 内再 assertOwner 兜底)。
57013
57568
  "visitor:list": ADMIN_ANY,
@@ -57188,8 +57743,8 @@ async function dispatchRpc(method, frame, client, ctx, deps) {
57188
57743
  }
57189
57744
 
57190
57745
  // src/extension/runtime.ts
57191
- var import_node_child_process15 = require("child_process");
57192
- var import_node_path52 = __toESM(require("path"), 1);
57746
+ var import_node_child_process18 = require("child_process");
57747
+ var import_node_path57 = __toESM(require("path"), 1);
57193
57748
  var import_promises11 = require("timers/promises");
57194
57749
 
57195
57750
  // src/extension/port-allocator.ts
@@ -57290,13 +57845,13 @@ var Runtime = class {
57290
57845
  /\$CLAWOS_EXT_PORT/g,
57291
57846
  String(port)
57292
57847
  );
57293
- const dir = import_node_path52.default.join(this.root, extId);
57848
+ const dir = import_node_path57.default.join(this.root, extId);
57294
57849
  const env = {
57295
57850
  ...process.env,
57296
57851
  CLAWOS_EXT_PORT: String(port),
57297
57852
  CLAWOS_EXT_ID: extId
57298
57853
  };
57299
- const child = (0, import_node_child_process15.spawn)("sh", ["-c", cmd], {
57854
+ const child = (0, import_node_child_process18.spawn)("sh", ["-c", cmd], {
57300
57855
  cwd: dir,
57301
57856
  env,
57302
57857
  stdio: ["ignore", "pipe", "pipe"],
@@ -57402,7 +57957,7 @@ ${handle.stderrTail}`
57402
57957
 
57403
57958
  // src/extension/published-channels.ts
57404
57959
  var import_promises12 = __toESM(require("fs/promises"), 1);
57405
- var import_node_path53 = __toESM(require("path"), 1);
57960
+ var import_node_path58 = __toESM(require("path"), 1);
57406
57961
  init_zod();
57407
57962
  var PublishedChannelsError = class extends Error {
57408
57963
  constructor(code, message) {
@@ -57501,7 +58056,7 @@ var PublishedChannelStore = class {
57501
58056
  )
57502
58057
  };
57503
58058
  const tmp = `${this.filePath}.tmp`;
57504
- await import_promises12.default.mkdir(import_node_path53.default.dirname(this.filePath), { recursive: true });
58059
+ await import_promises12.default.mkdir(import_node_path58.default.dirname(this.filePath), { recursive: true });
57505
58060
  await import_promises12.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
57506
58061
  await import_promises12.default.rename(tmp, this.filePath);
57507
58062
  }
@@ -57509,7 +58064,7 @@ var PublishedChannelStore = class {
57509
58064
 
57510
58065
  // src/extension/bundle-cache.ts
57511
58066
  var import_promises13 = __toESM(require("fs/promises"), 1);
57512
- var import_node_path54 = __toESM(require("path"), 1);
58067
+ var import_node_path59 = __toESM(require("path"), 1);
57513
58068
  var BundleCache = class {
57514
58069
  constructor(rootDir) {
57515
58070
  this.rootDir = rootDir;
@@ -57518,14 +58073,14 @@ var BundleCache = class {
57518
58073
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
57519
58074
  async write(snapshotHash, buffer) {
57520
58075
  await import_promises13.default.mkdir(this.rootDir, { recursive: true });
57521
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58076
+ const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
57522
58077
  const tmp = `${file}.tmp`;
57523
58078
  await import_promises13.default.writeFile(tmp, buffer, { mode: 384 });
57524
58079
  await import_promises13.default.rename(tmp, file);
57525
58080
  }
57526
58081
  /** Returns the bundle bytes, or null when the file doesn't exist. */
57527
58082
  async read(snapshotHash) {
57528
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58083
+ const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
57529
58084
  try {
57530
58085
  return await import_promises13.default.readFile(file);
57531
58086
  } catch (e) {
@@ -57535,7 +58090,7 @@ var BundleCache = class {
57535
58090
  }
57536
58091
  /** Idempotent — missing file is not an error. */
57537
58092
  async delete(snapshotHash) {
57538
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58093
+ const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
57539
58094
  await import_promises13.default.rm(file, { force: true });
57540
58095
  }
57541
58096
  };
@@ -57560,17 +58115,10 @@ async function startDaemon(config) {
57560
58115
  });
57561
58116
  const logger = createLogger({
57562
58117
  level: config.logLevel,
57563
- file: import_node_path55.default.join(config.dataDir, "clawd.log"),
58118
+ file: import_node_path60.default.join(config.dataDir, "clawd.log"),
57564
58119
  logClient
57565
58120
  });
57566
58121
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
57567
- const screenIdleProbeLogger = createFileOnlyLogger({
57568
- file: import_node_path55.default.join(config.dataDir, "screen-idle-probe.log"),
57569
- level: "debug"
57570
- });
57571
- logger.info("screen-idle probe logger enabled", {
57572
- file: import_node_path55.default.join(config.dataDir, "screen-idle-probe.log")
57573
- });
57574
58122
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
57575
58123
  const pre = stateMgr.preflight();
57576
58124
  if (pre.status === "active") {
@@ -57707,8 +58255,8 @@ async function startDaemon(config) {
57707
58255
  const agents = new AgentsScanner();
57708
58256
  const history = new ClaudeHistoryReader();
57709
58257
  let transport = null;
57710
- const personaStore = new PersonaStore(import_node_path55.default.join(config.dataDir, "personas"));
57711
- const usersRoot = import_node_path55.default.join(config.dataDir, "users");
58258
+ const personaStore = new PersonaStore(import_node_path60.default.join(config.dataDir, "personas"));
58259
+ const usersRoot = import_node_path60.default.join(config.dataDir, "users");
57712
58260
  const defaultsRoot = findDefaultsRoot(logger);
57713
58261
  if (defaultsRoot) {
57714
58262
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -57728,17 +58276,17 @@ async function startDaemon(config) {
57728
58276
  migrateCodexSandbox({ store: personaStore, logger });
57729
58277
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
57730
58278
  const personaDispatchManager = new PersonaDispatchManager({ genId: () => v4_default() });
57731
- const here = typeof __dirname === "string" ? __dirname : import_node_path55.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
58279
+ const here = typeof __dirname === "string" ? __dirname : import_node_path60.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
57732
58280
  const dispatchServerCandidates = [
57733
- import_node_path55.default.join(here, "dispatch", "mcp-server.cjs"),
58281
+ import_node_path60.default.join(here, "dispatch", "mcp-server.cjs"),
57734
58282
  // 生产 dist/index → dist/dispatch/mcp-server.cjs
57735
- import_node_path55.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
58283
+ import_node_path60.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
57736
58284
  // dev tsx src/index → ../dist/dispatch/mcp-server.cjs
57737
58285
  ];
57738
- const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58286
+ const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
57739
58287
  let dispatchMcpConfigPath2;
57740
58288
  if (dispatchServerScriptPath) {
57741
- const dispatchLogPath = import_node_path55.default.join(config.dataDir, "dispatch-mcp-server.log");
58289
+ const dispatchLogPath = import_node_path60.default.join(config.dataDir, "dispatch-mcp-server.log");
57742
58290
  dispatchMcpConfigPath2 = writeDispatchMcpConfig({
57743
58291
  dataDir: config.dataDir,
57744
58292
  serverScriptPath: dispatchServerScriptPath,
@@ -57755,15 +58303,15 @@ async function startDaemon(config) {
57755
58303
  });
57756
58304
  }
57757
58305
  const ticketServerCandidates = [
57758
- import_node_path55.default.join(here, "ticket", "mcp-server.cjs"),
57759
- import_node_path55.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
58306
+ import_node_path60.default.join(here, "ticket", "mcp-server.cjs"),
58307
+ import_node_path60.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
57760
58308
  ];
57761
- const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58309
+ const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
57762
58310
  const ticketOwnerUnionId = feishuIdentity?.identity.unionId ?? "";
57763
58311
  const ticketOwnerName = feishuIdentity?.identity.displayName ?? "";
57764
58312
  let ticketMcpConfigPath2;
57765
58313
  if (ticketServerScriptPath && ticketOwnerUnionId) {
57766
- const ticketLogPath = import_node_path55.default.join(config.dataDir, "ticket-mcp-server.log");
58314
+ const ticketLogPath = import_node_path60.default.join(config.dataDir, "ticket-mcp-server.log");
57767
58315
  ticketMcpConfigPath2 = writeTicketMcpConfig({
57768
58316
  dataDir: config.dataDir,
57769
58317
  serverScriptPath: ticketServerScriptPath,
@@ -57784,13 +58332,13 @@ async function startDaemon(config) {
57784
58332
  });
57785
58333
  }
57786
58334
  const shiftServerCandidates = [
57787
- import_node_path55.default.join(here, "shift", "mcp-server.cjs"),
57788
- import_node_path55.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
58335
+ import_node_path60.default.join(here, "shift", "mcp-server.cjs"),
58336
+ import_node_path60.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
57789
58337
  ];
57790
- const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58338
+ const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
57791
58339
  let shiftMcpConfigPath2;
57792
58340
  if (shiftServerScriptPath) {
57793
- const shiftLogPath = import_node_path55.default.join(config.dataDir, "shift-mcp-server.log");
58341
+ const shiftLogPath = import_node_path60.default.join(config.dataDir, "shift-mcp-server.log");
57794
58342
  shiftMcpConfigPath2 = await writeShiftMcpConfig({
57795
58343
  dataDir: config.dataDir,
57796
58344
  serverScriptPath: shiftServerScriptPath,
@@ -57808,13 +58356,13 @@ async function startDaemon(config) {
57808
58356
  );
57809
58357
  }
57810
58358
  const inboxServerCandidates = [
57811
- import_node_path55.default.join(here, "inbox", "mcp-server.cjs"),
57812
- import_node_path55.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
58359
+ import_node_path60.default.join(here, "inbox", "mcp-server.cjs"),
58360
+ import_node_path60.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
57813
58361
  ];
57814
- const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58362
+ const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
57815
58363
  let inboxMcpConfigPath2;
57816
58364
  if (inboxServerScriptPath) {
57817
- const inboxLogPath = import_node_path55.default.join(config.dataDir, "inbox-mcp-server.log");
58365
+ const inboxLogPath = import_node_path60.default.join(config.dataDir, "inbox-mcp-server.log");
57818
58366
  inboxMcpConfigPath2 = await writeInboxMcpConfig({
57819
58367
  dataDir: config.dataDir,
57820
58368
  serverScriptPath: inboxServerScriptPath,
@@ -57832,7 +58380,7 @@ async function startDaemon(config) {
57832
58380
  );
57833
58381
  }
57834
58382
  const shiftStore = createShiftStore({
57835
- filePath: import_node_path55.default.join(config.dataDir, "shift.json"),
58383
+ filePath: import_node_path60.default.join(config.dataDir, "shift.json"),
57836
58384
  ownerIdProvider: () => ownerPrincipalId,
57837
58385
  now: () => Date.now()
57838
58386
  });
@@ -57847,14 +58395,10 @@ async function startDaemon(config) {
57847
58395
  // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
57848
58396
  storeFactory: sessionStoreFactory,
57849
58397
  logger,
57850
- // 取证 probe(可选,CLAWD_SCREEN_IDLE_PROBE=1 时启用):manager turn_end 判定链
57851
- // 的所有决策点打到独立文件,跟 adapter 的 observeScreenIdle probe 共用同一份 file logger,
57852
- // 便于 grep sessionId 时 tui 层 + manager 层交叉时序都在同一文件里
57853
- ...screenIdleProbeLogger ? { screenIdleProbeLogger } : {},
57854
58398
  getAdapter,
57855
58399
  historyReader: history,
57856
58400
  dataDir: config.dataDir,
57857
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
58401
+ personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
57858
58402
  usersRoot,
57859
58403
  personaStore,
57860
58404
  ownerDisplayName,
@@ -57897,10 +58441,10 @@ async function startDaemon(config) {
57897
58441
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
57898
58442
  attachmentGroup: {
57899
58443
  onFileEdit: (input) => {
57900
- const absPath = import_node_path55.default.isAbsolute(input.relPath) ? input.relPath : import_node_path55.default.join(input.cwd, input.relPath);
58444
+ const absPath = import_node_path60.default.isAbsolute(input.relPath) ? input.relPath : import_node_path60.default.join(input.cwd, input.relPath);
57901
58445
  let size = 0;
57902
58446
  try {
57903
- size = import_node_fs42.default.statSync(absPath).size;
58447
+ size = import_node_fs47.default.statSync(absPath).size;
57904
58448
  } catch (err) {
57905
58449
  logger.warn("attachment.onFileEdit stat failed", {
57906
58450
  sessionId: input.sessionId,
@@ -57968,10 +58512,10 @@ async function startDaemon(config) {
57968
58512
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
57969
58513
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
57970
58514
  onReady: (tsid) => manager.dispatchReadyDetected(tsid),
57971
- // 屏幕真稳定 5s 的一次性信号 manager pending turn_duration flush turn_end
57972
- onScreenIdle: (tsid) => manager.notifyScreenIdle(tsid),
57973
- // 取证 probe(默认无条件启用;见 createFileOnlyLogger
57974
- screenIdleProbeLogger
58515
+ // 屏幕静止补权威 turn_end(修 turn_duration 尾随 text 覆盖 lastEventKind)
58516
+ onTurnIdle: (tsid) => manager.dispatchTurnIdle(tsid),
58517
+ // 复合条件闸:observer 还需静止多久才满 idleMs(屏幕静止后精确等这段剩余再补 turn_end
58518
+ getObserverWaitMs: (tsid, idleMs) => manager.observerIdleWaitMs(tsid, idleMs)
57975
58519
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
57976
58520
  registerAdapter("claude", claudeAdapter);
57977
58521
  registerAdapter("codex", new CodexAdapter({ logger, historyReader: new CodexHistoryReader() }));
@@ -58098,11 +58642,11 @@ async function startDaemon(config) {
58098
58642
  // 'persona/<pid>/owner',default 走 'default'。
58099
58643
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58100
58644
  // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
58101
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
58645
+ personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
58102
58646
  usersRoot
58103
58647
  },
58104
58648
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
58105
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
58649
+ personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
58106
58650
  // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
58107
58651
  usersRoot,
58108
58652
  // capability:list / delete handler 依赖
@@ -58122,6 +58666,9 @@ async function startDaemon(config) {
58122
58666
  inboxStore,
58123
58667
  // 联系人列表 store(device:connect / 自动反向落同一 store)
58124
58668
  contactStore,
58669
+ // <dataDir>/sshd 绝对路径 —— contact-ssh handlers 用它拼 authorized_keys / keys/ 子路径
58670
+ // Task 10 会加 SshdManager 起 sshd;handlers wire 提前挂 sshdDir 让 typecheck 过
58671
+ sshdDir: import_node_path60.default.join(config.dataDir, "sshd"),
58125
58672
  // inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
58126
58673
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58127
58674
  // contact:removed broadcast;复用 capability:tokenIssued 同款通路
@@ -58211,11 +58758,11 @@ async function startDaemon(config) {
58211
58758
  // 发布上线脚手架化 (spec 2026-06-03 §5.2):
58212
58759
  // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
58213
58760
  // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
58214
- appBuilderPersonaRoot: import_node_path55.default.join(config.dataDir, "personas", "persona-app-builder"),
58761
+ appBuilderPersonaRoot: import_node_path60.default.join(config.dataDir, "personas", "persona-app-builder"),
58215
58762
  // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
58216
- deployKitRoot: import_node_path55.default.join(config.dataDir, "deploy-kit"),
58763
+ deployKitRoot: import_node_path60.default.join(config.dataDir, "deploy-kit"),
58217
58764
  // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
58218
- resolvePersonaRoot: (personaId) => import_node_path55.default.join(config.dataDir, "personas", personaId),
58765
+ resolvePersonaRoot: (personaId) => import_node_path60.default.join(config.dataDir, "personas", personaId),
58219
58766
  // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
58220
58767
  // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
58221
58768
  // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
@@ -58258,7 +58805,7 @@ async function startDaemon(config) {
58258
58805
  }
58259
58806
  let sourceJsonlPath = "(no transcript yet \u2014 operate from the task description alone)";
58260
58807
  if (sourceFile && sourceFile.toolSessionId) {
58261
- sourceJsonlPath = import_node_path55.default.join(
58808
+ sourceJsonlPath = import_node_path60.default.join(
58262
58809
  import_node_os21.default.homedir(),
58263
58810
  ".claude",
58264
58811
  "projects",
@@ -58558,8 +59105,8 @@ async function startDaemon(config) {
58558
59105
  const lines = [
58559
59106
  `Tunnel: ${r.url}`,
58560
59107
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
58561
- `Frpc config: ${import_node_path55.default.join(config.dataDir, "frpc.toml")}`,
58562
- `Frpc log: ${import_node_path55.default.join(config.dataDir, "frpc.log")}`
59108
+ `Frpc config: ${import_node_path60.default.join(config.dataDir, "frpc.toml")}`,
59109
+ `Frpc log: ${import_node_path60.default.join(config.dataDir, "frpc.log")}`
58563
59110
  ];
58564
59111
  const width = Math.max(...lines.map((l) => l.length));
58565
59112
  const bar = "\u2550".repeat(width + 4);
@@ -58572,8 +59119,8 @@ ${bar}
58572
59119
 
58573
59120
  `);
58574
59121
  try {
58575
- const connectPath = import_node_path55.default.join(config.dataDir, "connect.txt");
58576
- import_node_fs42.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
59122
+ const connectPath = import_node_path60.default.join(config.dataDir, "connect.txt");
59123
+ import_node_fs47.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
58577
59124
  } catch {
58578
59125
  }
58579
59126
  } catch (err) {
@@ -58598,6 +59145,22 @@ ${bar}
58598
59145
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
58599
59146
  }
58600
59147
  }
59148
+ const sshdMgr = new SshdManager({
59149
+ dataDir: config.dataDir,
59150
+ port: config.sshdPort,
59151
+ logger,
59152
+ installProcessExitHandlers: true,
59153
+ onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info)
59154
+ });
59155
+ try {
59156
+ await sshdMgr.start();
59157
+ rebuildAuthorizedKeys(contactStore, import_node_path60.default.join(config.dataDir, "sshd"));
59158
+ logger.info("sshd: contact-ssh sandbox ready", { port: config.sshdPort });
59159
+ } catch (err) {
59160
+ logger.warn("sshd start failed; contact SSH grant will not work until fixed", {
59161
+ err: err.message
59162
+ });
59163
+ }
58601
59164
  void reportDevice();
58602
59165
  void fetchServerKey();
58603
59166
  const tickAttachmentGc = () => {
@@ -58632,6 +59195,11 @@ ${bar}
58632
59195
  if (tunnelMgr) {
58633
59196
  await tunnelMgr.stop();
58634
59197
  }
59198
+ await sshdMgr.stop().catch((err) => {
59199
+ logger.warn("shutdown.sshd-stop-failed", {
59200
+ error: err instanceof Error ? err.message : String(err)
59201
+ });
59202
+ });
58635
59203
  await wss.stop();
58636
59204
  stateMgr.delete();
58637
59205
  if (logClient) await logClient.dispose();
@@ -58645,9 +59213,9 @@ ${bar}
58645
59213
  };
58646
59214
  }
58647
59215
  function migrateDropPersonsDir(dataDir) {
58648
- const dir = import_node_path55.default.join(dataDir, "persons");
59216
+ const dir = import_node_path60.default.join(dataDir, "persons");
58649
59217
  try {
58650
- import_node_fs42.default.rmSync(dir, { recursive: true, force: true });
59218
+ import_node_fs47.default.rmSync(dir, { recursive: true, force: true });
58651
59219
  } catch {
58652
59220
  }
58653
59221
  }