@clawos-dev/clawd 0.2.199 → 0.2.201-beta.402.b896f3d

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: path76, errorMaps, issueData } = params;
746
+ const fullPath = [...path76, ...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, path76, key) {
1053
1058
  this._cachedPath = [];
1054
1059
  this.parent = parent;
1055
1060
  this.data = value;
1056
- this._path = path68;
1061
+ this._path = path76;
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 path76 = req.path;
6453
+ _req.url = typeof path76 === "string" ? path76 : 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(path76) {
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 < path76.length; i++) {
6625
+ const char = path76[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 path76 of paths) {
6757
+ const parts = parsePath(path76);
6706
6758
  if (parts.includes("*")) {
6707
- redactWildcardPath(obj, parts, censor, path68, remove);
6759
+ redactWildcardPath(obj, parts, censor, path76, 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, path76) => {
6845
+ const fullPath = [...pathArray.slice(0, pathLength), ...path76];
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 path76 of pathsToClone) {
6881
+ const parts = parsePath(path76);
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(path76) {
6934
+ if (typeof path76 !== "string") {
6883
6935
  throw new Error("Paths must be (non-empty) strings");
6884
6936
  }
6885
- if (path68 === "") {
6937
+ if (path76 === "") {
6886
6938
  throw new Error("Invalid redaction path ()");
6887
6939
  }
6888
- if (path68.includes("..")) {
6889
- throw new Error(`Invalid redaction path (${path68})`);
6940
+ if (path76.includes("..")) {
6941
+ throw new Error(`Invalid redaction path (${path76})`);
6890
6942
  }
6891
- if (path68.includes(",")) {
6892
- throw new Error(`Invalid redaction path (${path68})`);
6943
+ if (path76.includes(",")) {
6944
+ throw new Error(`Invalid redaction path (${path76})`);
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 < path76.length; i++) {
6950
+ const char = path76[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 (${path76})`);
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 (${path76})`);
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 path76 of paths) {
6977
+ validatePath(path76);
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, path76) => {
7146
+ return censor(value, [k2, ...path76]);
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 fs69 = require("fs");
7313
7365
  var EventEmitter3 = require("events");
7314
7366
  var inherits = require("util").inherits;
7315
- var path68 = require("path");
7367
+ var path76 = 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) fs69.mkdirSync(path76.dirname(file), { recursive: true });
7422
+ const fd = fs69.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
+ fs69.mkdir(path76.dirname(file), { recursive: true }, (err) => {
7378
7430
  if (err) return fileOpened(err);
7379
- fs61.open(file, flags, mode, fileOpened);
7431
+ fs69.open(file, flags, mode, fileOpened);
7380
7432
  });
7381
7433
  } else {
7382
- fs61.open(file, flags, mode, fileOpened);
7434
+ fs69.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 = () => fs69.writeSync(this.fd, this._writingBuf);
7476
+ fsWrite = () => fs69.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 fs69.writeSync(this.fd, this._writingBuf);
7434
7486
  }
7435
- return fs61.writeSync(this.fd, this._writingBuf, "utf8");
7487
+ return fs69.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 fs69.write(this.fd, this._writingBuf, this.release);
7440
7492
  }
7441
- return fs61.write(this.fd, this._writingBuf, "utf8", this.release);
7493
+ return fs69.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
+ fs69.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
+ fs69.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
+ fs69.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) ? fs69.writeSync(this.fd, buf) : fs69.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
+ fs69.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 = fs69.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) ? fs69.writeSync(this.fd, this._writingBuf) : fs69.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
+ fs69.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 = fs69.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
+ fs69.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
+ fs69.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
+ fs69.close(sonic.fd, done);
7874
7926
  } else {
7875
7927
  done();
7876
7928
  }
@@ -10234,7 +10286,7 @@ var require_multistream = __commonJS({
10234
10286
  var require_pino = __commonJS({
10235
10287
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/pino.js"(exports2, module2) {
10236
10288
  "use strict";
10237
- var os23 = require("os");
10289
+ var os24 = require("os");
10238
10290
  var stdSerializers = require_pino_std_serializers();
10239
10291
  var caller = require_caller();
10240
10292
  var redaction = require_redaction();
@@ -10281,7 +10333,7 @@ var require_pino = __commonJS({
10281
10333
  } = symbols;
10282
10334
  var { epochTime, nullTime } = time;
10283
10335
  var { pid } = process;
10284
- var hostname = os23.hostname();
10336
+ var hostname = os24.hostname();
10285
10337
  var defaultErrorSerializer = stdSerializers.err;
10286
10338
  var defaultOptions = {
10287
10339
  level: "info",
@@ -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(path76, added, removed, oldPosInc, options) {
11061
+ var last = path76.lastComponent;
11010
11062
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
11011
11063
  return {
11012
- oldPos: path68.oldPos + oldPosInc,
11064
+ oldPos: path76.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: path76.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 path76 = 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 (!path76 || content == null) return null;
11543
+ const entry = { path: path76, content };
11492
11544
  if (typeof rec3.mtimeMs === "number") entry.mtimeMs = rec3.mtimeMs;
11493
11545
  return entry;
11494
11546
  }).filter((m2) => m2 !== null);
@@ -11952,6 +12004,14 @@ var init_claude_history = __esm({
11952
12004
  }
11953
12005
  });
11954
12006
 
12007
+ // ../protocol/src/index.ts
12008
+ var init_src = __esm({
12009
+ "../protocol/src/index.ts"() {
12010
+ "use strict";
12011
+ init_runtime();
12012
+ }
12013
+ });
12014
+
11955
12015
  // src/tools/claude.ts
11956
12016
  function probeViaWhich() {
11957
12017
  try {
@@ -12300,10 +12360,10 @@ function parseAttachment(obj) {
12300
12360
  const memories = raw.map((m2) => {
12301
12361
  if (!m2 || typeof m2 !== "object") return null;
12302
12362
  const rec3 = m2;
12303
- const path68 = typeof rec3.path === "string" ? rec3.path : null;
12363
+ const path76 = typeof rec3.path === "string" ? rec3.path : null;
12304
12364
  const content = typeof rec3.content === "string" ? rec3.content : null;
12305
- if (!path68 || content == null) return null;
12306
- const out = { path: path68, content };
12365
+ if (!path76 || content == null) return null;
12366
+ const out = { path: path76, content };
12307
12367
  if (typeof rec3.mtimeMs === "number") out.mtimeMs = rec3.mtimeMs;
12308
12368
  return out;
12309
12369
  }).filter((m2) => m2 !== null);
@@ -26775,6 +26835,121 @@ var require_dist = __commonJS({
26775
26835
  }
26776
26836
  });
26777
26837
 
26838
+ // src/dispatch/peer-forward.ts
26839
+ function wsUrlToHttp(url) {
26840
+ if (url.startsWith("wss://")) return "https://" + url.slice("wss://".length);
26841
+ if (url.startsWith("ws://")) return "http://" + url.slice("ws://".length);
26842
+ return url;
26843
+ }
26844
+ async function forwardDispatchToPeer(args) {
26845
+ const f = args.fetchImpl ?? fetch;
26846
+ const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
26847
+ const url = `${base}/rpc/personaDispatch:run`;
26848
+ let res;
26849
+ try {
26850
+ res = await f(url, {
26851
+ method: "POST",
26852
+ headers: {
26853
+ "content-type": "application/json",
26854
+ authorization: `Bearer ${args.contact.connectToken}`
26855
+ },
26856
+ // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
26857
+ body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
26858
+ });
26859
+ } catch (err) {
26860
+ const msg = err instanceof Error ? err.message : String(err);
26861
+ return { kind: "failure", reason: `forward to peer failed: ${msg}` };
26862
+ }
26863
+ let json;
26864
+ try {
26865
+ json = await res.json();
26866
+ } catch {
26867
+ return {
26868
+ kind: "failure",
26869
+ reason: `peer returned non-JSON response (HTTP ${res.status})`
26870
+ };
26871
+ }
26872
+ if (json.ok === false) {
26873
+ return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
26874
+ }
26875
+ return json.result.outcome;
26876
+ }
26877
+ async function forwardInboxPostToPeer(args) {
26878
+ const f = args.fetchImpl ?? fetch;
26879
+ const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
26880
+ const url = `${base}/rpc/inbox:postMessage`;
26881
+ let res;
26882
+ try {
26883
+ res = await f(url, {
26884
+ method: "POST",
26885
+ headers: {
26886
+ "content-type": "application/json",
26887
+ authorization: `Bearer ${args.contact.connectToken}`
26888
+ },
26889
+ body: JSON.stringify({
26890
+ id: args.id,
26891
+ text: args.text,
26892
+ createdAt: args.createdAt,
26893
+ ...args.origin ? { origin: args.origin } : {}
26894
+ })
26895
+ });
26896
+ } catch (err) {
26897
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26898
+ }
26899
+ let json = null;
26900
+ try {
26901
+ json = await res.json();
26902
+ } catch {
26903
+ return { ok: false, error: `peer non-JSON (HTTP ${res.status})` };
26904
+ }
26905
+ if (res.status < 200 || res.status >= 300 || json?.ok === false) {
26906
+ return { ok: false, error: json?.error ?? `HTTP ${res.status}` };
26907
+ }
26908
+ return { ok: true };
26909
+ }
26910
+ async function forwardContactSshKeyIssueToPeer(args) {
26911
+ const f = args.fetchImpl ?? fetch;
26912
+ const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
26913
+ const url = `${base}/rpc/contact:sshKey:issue`;
26914
+ let res;
26915
+ try {
26916
+ res = await f(url, {
26917
+ method: "POST",
26918
+ headers: {
26919
+ "content-type": "application/json",
26920
+ authorization: `Bearer ${args.contact.connectToken}`
26921
+ },
26922
+ body: JSON.stringify({ deviceId: args.selfDeviceIdForRequest })
26923
+ });
26924
+ } catch (err) {
26925
+ return {
26926
+ ok: false,
26927
+ code: "NETWORK",
26928
+ message: err instanceof Error ? err.message : String(err)
26929
+ };
26930
+ }
26931
+ let json = null;
26932
+ try {
26933
+ json = await res.json();
26934
+ } catch {
26935
+ return { ok: false, code: "PROTOCOL", message: `peer non-JSON (HTTP ${res.status})` };
26936
+ }
26937
+ if (!json) {
26938
+ return { ok: false, code: "PROTOCOL", message: `peer returned null (HTTP ${res.status})` };
26939
+ }
26940
+ if (json.ok === false) {
26941
+ const j = json;
26942
+ return { ok: false, code: j.error ?? "UNKNOWN", message: j.message ?? "" };
26943
+ }
26944
+ const r = json.result;
26945
+ return { ok: true, privateKeyPem: r.privateKeyPem, publicKeyLine: r.publicKeyLine };
26946
+ }
26947
+ var init_peer_forward = __esm({
26948
+ "src/dispatch/peer-forward.ts"() {
26949
+ "use strict";
26950
+ }
26951
+ });
26952
+
26778
26953
  // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js
26779
26954
  var require_constants2 = __commonJS({
26780
26955
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js"(exports2, module2) {
@@ -28976,9 +29151,9 @@ var require_websocket = __commonJS({
28976
29151
  var EventEmitter3 = require("events");
28977
29152
  var https = require("https");
28978
29153
  var http3 = require("http");
28979
- var net3 = require("net");
29154
+ var net4 = require("net");
28980
29155
  var tls = require("tls");
28981
- var { randomBytes, createHash: createHash3 } = require("crypto");
29156
+ var { randomBytes, createHash: createHash2 } = require("crypto");
28982
29157
  var { Duplex, Readable: Readable3 } = require("stream");
28983
29158
  var { URL: URL2 } = require("url");
28984
29159
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -29638,7 +29813,7 @@ var require_websocket = __commonJS({
29638
29813
  abortHandshake(websocket, socket, "Invalid Upgrade header");
29639
29814
  return;
29640
29815
  }
29641
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
29816
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
29642
29817
  if (res.headers["sec-websocket-accept"] !== digest) {
29643
29818
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
29644
29819
  return;
@@ -29710,12 +29885,12 @@ var require_websocket = __commonJS({
29710
29885
  }
29711
29886
  function netConnect(options) {
29712
29887
  options.path = options.socketPath;
29713
- return net3.connect(options);
29888
+ return net4.connect(options);
29714
29889
  }
29715
29890
  function tlsConnect(options) {
29716
29891
  options.path = void 0;
29717
29892
  if (!options.servername && options.servername !== "") {
29718
- options.servername = net3.isIP(options.host) ? "" : options.host;
29893
+ options.servername = net4.isIP(options.host) ? "" : options.host;
29719
29894
  }
29720
29895
  return tls.connect(options);
29721
29896
  }
@@ -30005,7 +30180,7 @@ var require_websocket_server = __commonJS({
30005
30180
  var EventEmitter3 = require("events");
30006
30181
  var http3 = require("http");
30007
30182
  var { Duplex } = require("stream");
30008
- var { createHash: createHash3 } = require("crypto");
30183
+ var { createHash: createHash2 } = require("crypto");
30009
30184
  var extension2 = require_extension();
30010
30185
  var PerMessageDeflate2 = require_permessage_deflate();
30011
30186
  var subprotocol2 = require_subprotocol();
@@ -30306,7 +30481,7 @@ var require_websocket_server = __commonJS({
30306
30481
  );
30307
30482
  }
30308
30483
  if (this._state > RUNNING) return abortHandshake(socket, 503);
30309
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
30484
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
30310
30485
  const headers = [
30311
30486
  "HTTP/1.1 101 Switching Protocols",
30312
30487
  "Upgrade: websocket",
@@ -30391,6 +30566,23 @@ var require_websocket_server = __commonJS({
30391
30566
  }
30392
30567
  });
30393
30568
 
30569
+ // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
30570
+ var import_stream, import_extension, import_permessage_deflate, import_receiver, import_sender, import_subprotocol, import_websocket, import_websocket_server, wrapper_default;
30571
+ var init_wrapper = __esm({
30572
+ "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs"() {
30573
+ "use strict";
30574
+ import_stream = __toESM(require_stream(), 1);
30575
+ import_extension = __toESM(require_extension(), 1);
30576
+ import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
30577
+ import_receiver = __toESM(require_receiver(), 1);
30578
+ import_sender = __toESM(require_sender(), 1);
30579
+ import_subprotocol = __toESM(require_subprotocol(), 1);
30580
+ import_websocket = __toESM(require_websocket(), 1);
30581
+ import_websocket_server = __toESM(require_websocket_server(), 1);
30582
+ wrapper_default = import_websocket.default;
30583
+ }
30584
+ });
30585
+
30394
30586
  // ../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js
30395
30587
  var require_process_nextick_args = __commonJS({
30396
30588
  "../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js"(exports2, module2) {
@@ -33268,8 +33460,8 @@ var require_utils = __commonJS({
33268
33460
  var result = transform[inputType][outputType](input);
33269
33461
  return result;
33270
33462
  };
33271
- exports2.resolve = function(path68) {
33272
- var parts = path68.split("/");
33463
+ exports2.resolve = function(path76) {
33464
+ var parts = path76.split("/");
33273
33465
  var result = [];
33274
33466
  for (var index = 0; index < parts.length; index++) {
33275
33467
  var part = parts[index];
@@ -39122,18 +39314,18 @@ var require_object = __commonJS({
39122
39314
  var object = new ZipObject(name, zipObjectContent, o);
39123
39315
  this.files[name] = object;
39124
39316
  };
39125
- var parentFolder = function(path68) {
39126
- if (path68.slice(-1) === "/") {
39127
- path68 = path68.substring(0, path68.length - 1);
39317
+ var parentFolder = function(path76) {
39318
+ if (path76.slice(-1) === "/") {
39319
+ path76 = path76.substring(0, path76.length - 1);
39128
39320
  }
39129
- var lastSlash = path68.lastIndexOf("/");
39130
- return lastSlash > 0 ? path68.substring(0, lastSlash) : "";
39321
+ var lastSlash = path76.lastIndexOf("/");
39322
+ return lastSlash > 0 ? path76.substring(0, lastSlash) : "";
39131
39323
  };
39132
- var forceTrailingSlash = function(path68) {
39133
- if (path68.slice(-1) !== "/") {
39134
- path68 += "/";
39324
+ var forceTrailingSlash = function(path76) {
39325
+ if (path76.slice(-1) !== "/") {
39326
+ path76 += "/";
39135
39327
  }
39136
- return path68;
39328
+ return path76;
39137
39329
  };
39138
39330
  var folderAdd = function(name, createFolders) {
39139
39331
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -40135,7 +40327,7 @@ var require_lib3 = __commonJS({
40135
40327
  // src/run-case/recorder.ts
40136
40328
  function startRunCaseRecorder(opts) {
40137
40329
  const now = opts.now ?? Date.now;
40138
- const dir = import_node_path56.default.dirname(opts.recordPath);
40330
+ const dir = import_node_path63.default.dirname(opts.recordPath);
40139
40331
  let stream = null;
40140
40332
  let closing = false;
40141
40333
  let closedSettled = false;
@@ -40149,8 +40341,8 @@ function startRunCaseRecorder(opts) {
40149
40341
  });
40150
40342
  const ensureStream = () => {
40151
40343
  if (stream) return stream;
40152
- import_node_fs43.default.mkdirSync(dir, { recursive: true });
40153
- stream = import_node_fs43.default.createWriteStream(opts.recordPath, { flags: "a" });
40344
+ import_node_fs50.default.mkdirSync(dir, { recursive: true });
40345
+ stream = import_node_fs50.default.createWriteStream(opts.recordPath, { flags: "a" });
40154
40346
  stream.on("close", () => closedResolve());
40155
40347
  return stream;
40156
40348
  };
@@ -40175,12 +40367,12 @@ function startRunCaseRecorder(opts) {
40175
40367
  };
40176
40368
  return { tap, close, closed };
40177
40369
  }
40178
- var import_node_fs43, import_node_path56;
40370
+ var import_node_fs50, import_node_path63;
40179
40371
  var init_recorder = __esm({
40180
40372
  "src/run-case/recorder.ts"() {
40181
40373
  "use strict";
40182
- import_node_fs43 = __toESM(require("fs"), 1);
40183
- import_node_path56 = __toESM(require("path"), 1);
40374
+ import_node_fs50 = __toESM(require("fs"), 1);
40375
+ import_node_path63 = __toESM(require("path"), 1);
40184
40376
  }
40185
40377
  });
40186
40378
 
@@ -40223,7 +40415,7 @@ var init_wire = __esm({
40223
40415
  // src/run-case/controller.ts
40224
40416
  async function runController(opts) {
40225
40417
  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-"));
40418
+ const cwd = opts.cwd ?? (0, import_node_fs51.mkdtempSync)(import_node_path64.default.join(import_node_os22.default.tmpdir(), "clawd-runcase-"));
40227
40419
  const ownsCwd = opts.cwd === void 0;
40228
40420
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
40229
40421
  const spawnCtx = { cwd };
@@ -40384,19 +40576,19 @@ async function runController(opts) {
40384
40576
  if (sigintHandler) process.off("SIGINT", sigintHandler);
40385
40577
  if (ownsCwd) {
40386
40578
  try {
40387
- (0, import_node_fs44.rmSync)(cwd, { recursive: true, force: true });
40579
+ (0, import_node_fs51.rmSync)(cwd, { recursive: true, force: true });
40388
40580
  } catch {
40389
40581
  }
40390
40582
  }
40391
40583
  return exitCode ?? 0;
40392
40584
  }
40393
- var import_node_fs44, import_node_os22, import_node_path57;
40585
+ var import_node_fs51, import_node_os22, import_node_path64;
40394
40586
  var init_controller = __esm({
40395
40587
  "src/run-case/controller.ts"() {
40396
40588
  "use strict";
40397
- import_node_fs44 = require("fs");
40589
+ import_node_fs51 = require("fs");
40398
40590
  import_node_os22 = __toESM(require("os"), 1);
40399
- import_node_path57 = __toESM(require("path"), 1);
40591
+ import_node_path64 = __toESM(require("path"), 1);
40400
40592
  init_claude();
40401
40593
  init_stdout_splitter();
40402
40594
  init_permission_stdio();
@@ -40492,6 +40684,163 @@ stdout \u4E8B\u4EF6\uFF08\u884C JSON\uFF09\uFF1A
40492
40684
  }
40493
40685
  });
40494
40686
 
40687
+ // src/sshd/sshd-cli-relay.ts
40688
+ var sshd_cli_relay_exports = {};
40689
+ __export(sshd_cli_relay_exports, {
40690
+ sshRelay: () => sshRelay
40691
+ });
40692
+ async function sshRelay(argv) {
40693
+ const args = parseSshRelayArgs(argv);
40694
+ if (args.help) {
40695
+ process.stdout.write(SSH_RELAY_HELP);
40696
+ return 0;
40697
+ }
40698
+ if (!args.peerDeviceId) {
40699
+ process.stderr.write("clawd ssh-relay: missing <peer-device-id>\n" + SSH_RELAY_HELP);
40700
+ return 2;
40701
+ }
40702
+ const dataDir = args.dataDir ?? import_node_path65.default.join(import_node_os23.default.homedir(), ".clawd");
40703
+ const contact = findContact(dataDir, args.peerDeviceId);
40704
+ if (!contact) {
40705
+ process.stderr.write(`clawd ssh-relay: contact ${args.peerDeviceId} not found in ${dataDir}/contacts.json
40706
+ `);
40707
+ return 2;
40708
+ }
40709
+ if (!contact.connectToken) {
40710
+ process.stderr.write(
40711
+ `clawd ssh-relay: contact ${args.peerDeviceId} has no connectToken (auto-reverse \u672A\u6362\u7968)
40712
+ `
40713
+ );
40714
+ return 2;
40715
+ }
40716
+ const baseHttp = wsUrlToHttp(contact.remoteUrl).replace(/\/+$/, "");
40717
+ const wsBase = baseHttp.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
40718
+ const url = `${wsBase}/rpc/ssh-tunnel`;
40719
+ return new Promise((resolve6) => {
40720
+ const ws = new import_websocket.default(url, {
40721
+ headers: {
40722
+ Authorization: `Bearer ${contact.connectToken}`
40723
+ }
40724
+ });
40725
+ let exitCode = 0;
40726
+ let settled = false;
40727
+ const settle = (code) => {
40728
+ if (settled) return;
40729
+ settled = true;
40730
+ exitCode = code;
40731
+ try {
40732
+ ws.close();
40733
+ } catch {
40734
+ }
40735
+ resolve6(exitCode);
40736
+ };
40737
+ ws.on("open", () => {
40738
+ process.stdin.on("data", (chunk) => {
40739
+ if (ws.readyState === ws.OPEN) {
40740
+ ws.send(chunk, { binary: true });
40741
+ }
40742
+ });
40743
+ process.stdin.on("end", () => {
40744
+ try {
40745
+ ws.close(1e3, "stdin end");
40746
+ } catch {
40747
+ }
40748
+ });
40749
+ process.stdin.on("error", () => settle(1));
40750
+ });
40751
+ ws.on("message", (data, isBinary) => {
40752
+ void isBinary;
40753
+ if (Buffer.isBuffer(data)) {
40754
+ process.stdout.write(data);
40755
+ } else if (Array.isArray(data)) {
40756
+ process.stdout.write(Buffer.concat(data));
40757
+ } else if (data instanceof ArrayBuffer) {
40758
+ process.stdout.write(Buffer.from(data));
40759
+ }
40760
+ });
40761
+ ws.on("close", (code) => {
40762
+ settle(code === 1e3 ? 0 : 1);
40763
+ });
40764
+ ws.on("error", (err) => {
40765
+ process.stderr.write(`clawd ssh-relay: ws error ${err.message}
40766
+ `);
40767
+ settle(1);
40768
+ });
40769
+ const onSignal = () => settle(0);
40770
+ process.once("SIGINT", onSignal);
40771
+ process.once("SIGTERM", onSignal);
40772
+ });
40773
+ }
40774
+ function parseSshRelayArgs(argv) {
40775
+ const out = {};
40776
+ for (let i = 0; i < argv.length; i++) {
40777
+ const a = argv[i];
40778
+ if (a === "--help" || a === "-h") {
40779
+ out.help = true;
40780
+ continue;
40781
+ }
40782
+ if (a === "--data-dir") {
40783
+ out.dataDir = argv[++i];
40784
+ continue;
40785
+ }
40786
+ if (a.startsWith("--")) {
40787
+ throw new Error(`unknown flag: ${a}`);
40788
+ }
40789
+ if (!out.peerDeviceId) {
40790
+ out.peerDeviceId = a;
40791
+ }
40792
+ }
40793
+ return out;
40794
+ }
40795
+ function findContact(dataDir, deviceId) {
40796
+ const file = import_node_path65.default.join(dataDir, "contacts.json");
40797
+ let raw;
40798
+ try {
40799
+ raw = import_node_fs52.default.readFileSync(file, "utf8");
40800
+ } catch {
40801
+ return null;
40802
+ }
40803
+ let json;
40804
+ try {
40805
+ json = JSON.parse(raw);
40806
+ } catch {
40807
+ return null;
40808
+ }
40809
+ const arr = json?.contacts;
40810
+ if (!Array.isArray(arr)) return null;
40811
+ for (const entry of arr) {
40812
+ const r = ContactSchema.safeParse(entry);
40813
+ if (r.success && r.data.deviceId === deviceId) return r.data;
40814
+ }
40815
+ return null;
40816
+ }
40817
+ var import_node_fs52, import_node_os23, import_node_path65, SSH_RELAY_HELP;
40818
+ var init_sshd_cli_relay = __esm({
40819
+ "src/sshd/sshd-cli-relay.ts"() {
40820
+ "use strict";
40821
+ import_node_fs52 = __toESM(require("fs"), 1);
40822
+ import_node_os23 = __toESM(require("os"), 1);
40823
+ import_node_path65 = __toESM(require("path"), 1);
40824
+ init_wrapper();
40825
+ init_src();
40826
+ init_peer_forward();
40827
+ SSH_RELAY_HELP = `clawd ssh-relay <peer-device-id> [options]
40828
+
40829
+ WebSocket relay to a peer daemon's /rpc/ssh-tunnel, exposing raw SSH bytes on
40830
+ stdio. Meant to be used as SSH ProxyCommand.
40831
+
40832
+ Options:
40833
+ --data-dir <path> \u6570\u636E\u76EE\u5F55\uFF08\u9ED8\u8BA4 ~/.clawd\uFF09
40834
+ --help / -h \u663E\u793A\u5E2E\u52A9
40835
+
40836
+ Example:
40837
+ ssh -o ProxyCommand='clawd ssh-relay <peer-device-id>' \\
40838
+ -i ~/.clawd/contact-ssh-keys/<peer-device-id>.ed25519 \\
40839
+ $USER@127.0.0.1
40840
+ `;
40841
+ }
40842
+ });
40843
+
40495
40844
  // src/config.ts
40496
40845
  var import_node_fs = __toESM(require("fs"), 1);
40497
40846
  var import_node_os = __toESM(require("os"), 1);
@@ -40499,6 +40848,7 @@ var import_node_path = __toESM(require("path"), 1);
40499
40848
  init_protocol();
40500
40849
  var DEFAULT_PORT = 18790;
40501
40850
  var DEFAULT_HOST = "127.0.0.1";
40851
+ var DEFAULT_SSHD_PORT = 22422;
40502
40852
  var DEFAULT_CLAWOS_API = "https://api.clawos.chat";
40503
40853
  var DEFAULT_LOG_ENDPOINT = "https://clawd-prod.cn-hangzhou.log.aliyuncs.com/logstores/app-logs/track";
40504
40854
  function resolveLogShipping(raw, cliNoShipping) {
@@ -40566,6 +40916,14 @@ function parseArgs(argv) {
40566
40916
  case "--no-log-shipping":
40567
40917
  out.noLogShipping = true;
40568
40918
  break;
40919
+ case "--sshd-port": {
40920
+ const n = Number.parseInt(next() ?? "", 10);
40921
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) {
40922
+ throw new Error(`invalid --sshd-port value: ${argv[i]}`);
40923
+ }
40924
+ out.sshdPort = n;
40925
+ break;
40926
+ }
40569
40927
  default:
40570
40928
  if (a.startsWith("--")) throw new Error(`unknown flag: ${a}`);
40571
40929
  break;
@@ -40615,6 +40973,7 @@ function resolveConfig(opts) {
40615
40973
  fileCfg.logShipping,
40616
40974
  args.noLogShipping ?? false
40617
40975
  );
40976
+ const sshdPort = args.sshdPort ?? (typeof fileCfg.sshdPort === "number" ? fileCfg.sshdPort : DEFAULT_SSHD_PORT);
40618
40977
  return {
40619
40978
  port,
40620
40979
  host,
@@ -40628,7 +40987,8 @@ function resolveConfig(opts) {
40628
40987
  frpcBinary,
40629
40988
  mode,
40630
40989
  previewPorts,
40631
- logShipping
40990
+ logShipping,
40991
+ sshdPort
40632
40992
  };
40633
40993
  }
40634
40994
  var HELP_TEXT = `clawd [options]
@@ -40640,6 +41000,7 @@ var HELP_TEXT = `clawd [options]
40640
41000
  --clawos-api <url> tunnel register \u63A5\u53E3\u7684 base url\uFF08\u9ED8\u8BA4 https://api.clawos.chat\uFF09
40641
41001
  --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
41002
  --no-log-shipping \u7981\u7528 SLS \u65E5\u5FD7\u4E0A\u884C\uFF08\u8986\u76D6 config.json logShipping.mode=off\uFF09
41003
+ --sshd-port <n> Contact SSH \u53CD\u5411\u8BBF\u95EE loopback \u7AEF\u53E3\uFF0C\u9ED8\u8BA4 22422
40643
41004
  --help / -h \u663E\u793A\u5E2E\u52A9
40644
41005
  --version / -v \u663E\u793A\u7248\u672C
40645
41006
 
@@ -40648,6 +41009,10 @@ Subcommands:
40648
41009
  \u72EC\u7ACB CC \u63A7\u5236\u5668\u8FDB\u7A0B\uFF1A\u5916\u90E8 AI \u901A\u8FC7 stdin/stdout \u884C JSON \u534F\u8BAE
40649
41010
  \u9A71\u52A8 CC \u591A\u8F6E\u5BF9\u8BDD\u4E0E\u6743\u9650\u51B3\u7B56\uFF0CIPC \u5168\u7A0B\u5F55\u5230 --record JSONL\u3002
40650
41011
  \u8BE6\u89C1 'clawd run-case --help'
41012
+ ssh-relay <peer-device-id> [--data-dir <path>]
41013
+ WebSocket relay to peer daemon's /rpc/ssh-tunnel\uFF0C\u4F5C\u4E3A SSH
41014
+ ProxyCommand \u4F7F\u7528\uFF08Contact SSH tunnel\uFF0CPR#2\uFF09\u3002
41015
+ \u8BE6\u89C1 'clawd ssh-relay --help'
40651
41016
 
40652
41017
  Env (advanced):
40653
41018
  CLAWD_FRPC_BIN \u81EA\u5E26 frpc \u4E8C\u8FDB\u5236\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u6309\u9700\u4E0B\u8F7D\u5230 ~/.clawd/bin/frpc\uFF09
@@ -40658,8 +41023,8 @@ Env (advanced):
40658
41023
  `;
40659
41024
 
40660
41025
  // src/index.ts
40661
- var import_node_path55 = __toESM(require("path"), 1);
40662
- var import_node_fs42 = __toESM(require("fs"), 1);
41026
+ var import_node_path62 = __toESM(require("path"), 1);
41027
+ var import_node_fs49 = __toESM(require("fs"), 1);
40663
41028
  var import_node_os21 = __toESM(require("os"), 1);
40664
41029
 
40665
41030
  // ../node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/stringify.js
@@ -40779,18 +41144,6 @@ function createLogger(opts = {}) {
40779
41144
  );
40780
41145
  return wrap(base);
40781
41146
  }
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
41147
  function pinoLevelToString(n) {
40795
41148
  if (typeof n !== "number") return null;
40796
41149
  if (n >= 50) return "error";
@@ -41370,6 +41723,7 @@ function composeGuestSandbox(base, userWorkDir, spawnCwd) {
41370
41723
  fsv.denyWrite = unionArr((fsv.denyWrite ?? []).filter((p2) => p2 !== "~/"), [spawnCwd]);
41371
41724
  fsv.allowRead = unionArr(fsv.allowRead, [userWorkDir]);
41372
41725
  fsv.allowWrite = unionArr(fsv.allowWrite, [userWorkDir]);
41726
+ fsv.allowRead = unionArr(fsv.allowRead, ["~/.clawd/contact-ssh-keys"]);
41373
41727
  return s;
41374
41728
  }
41375
41729
 
@@ -41510,6 +41864,31 @@ deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731
41510
41864
  - \u5931\u8D25\uFF1A\`{ isFailure: true, reason: "..." }\`
41511
41865
  - \u62A5\u544A\u5B8C\u540E\u4F60\u7684 session \u5C31\u7ED3\u675F\u4E86\uFF0C\u4E0D\u8981\u7EE7\u7EED\u8F93\u51FA`;
41512
41866
 
41867
+ // src/sshd/contact-ssh-prompt.ts
41868
+ var CONTACT_SSH_SYSTEM_PROMPT_HINT = `## \u8DE8\u8BBE\u5907\u6587\u4EF6\u8BBF\u95EE\uFF08Contact SSH\uFF09
41869
+
41870
+ \u7528\u6237\u53EF\u80FD\u6709\u591A\u53F0 clawd \u8BBE\u5907\u4E92\u76F8\u6388\u6743\u3002\u4F60\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u8BBF\u95EE\u5DF2\u6388\u6743\u5BF9\u7AEF\uFF08\u4E0B\u79F0 A\uFF09\u7684\u65B9\u6CD5\uFF1A
41871
+
41872
+ 1. \u5217\u51FA\u53EF\u62E8\u7684\u5BF9\u7AEF deviceId\uFF1A\`ls ~/.clawd/contact-ssh-keys/\`
41873
+ \uFF08\u6BCF\u4E2A \`<deviceId>.ed25519\` \u5C31\u662F\u4E00\u628A privkey\uFF0C\u53BB\u6389\u540E\u7F00\u5C31\u662F deviceId\uFF09
41874
+ 2. \u7528 SSH \u62E8\u53F7\uFF08\u628A \`<A>\` \u6362\u6210\u5B9E\u9645 deviceId\uFF09\uFF1A
41875
+ \`\`\`bash
41876
+ ssh -o ProxyCommand='clawd ssh-relay <A>' \\
41877
+ -i ~/.clawd/contact-ssh-keys/<A>.ed25519 \\
41878
+ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \\
41879
+ $USER@127.0.0.1 <command>
41880
+ # \u4F8B\u5982\uFF1A... $USER@127.0.0.1 cat /Users/xxx/some/file.md
41881
+ # ... $USER@127.0.0.1 ls -la /Users/xxx/proj
41882
+ \`\`\`
41883
+
41884
+ **A \u6388\u6743\u54EA\u4E9B\u76EE\u5F55\u4E0D\u7528\u4F60\u63D0\u524D\u8BB0**\uFF1AA \u4FA7\u7684 sshd \u62E8\u53F7\u4F1A\u5F3A\u5236\u8D70 sandbox jail\uFF0C\u8FDB\u6C99\u7BB1\u524D\u4F1A\u628A\u6388\u6743\u76EE\u5F55\u5217\u8868\u5199\u5230 stderr\uFF08\u683C\u5F0F \`[clawd-ssh-jail] Access allowed to these directories: ...\`\uFF09\u3002\u4F60\u5373\u4F7F\u731C\u9519\u8DEF\u5F84\u4E5F\u4F1A\u88AB sandbox deny\uFF0C\u4F46 stderr \u91CC\u5C31\u81EA\u5E26\u7B54\u6848\uFF0C\u8BD5\u9519\u4E00\u6B21\u5C31\u5B66\u5230 A \u6388\u6743\u4E86\u54EA\u4E9B\u76EE\u5F55\uFF0C\u4E0B\u6B21\u76F4\u63A5\u7528\u5BF9\u7684\u8DEF\u5F84\u3002
41885
+
41886
+ **\u6CE8\u610F**\uFF1A
41887
+ - \u6388\u6743\u76EE\u5F55\u6570\u636E\u6E90\u5B9E\u65F6\uFF08jail \u6BCF\u6B21\u8BFB A \u4FA7\u6700\u65B0\u914D\u7F6E\uFF09\uFF0CA \u6539\u4E86\u6388\u6743\u540E\u4F60\u4E0B\u4E00\u6B21\u62E8\u53F7\u7ACB\u5373\u611F\u77E5
41888
+ - \u6392\u969C\uFF1A\u672C\u673A debug \u65E5\u5FD7\u5728 \`~/.clawd/contact-ssh.log\`\uFF08\u7528\u6237\u53EF \`tail -f\` \u770B\u6BCF\u4E00\u6B65\uFF09\uFF0C\u4F60\u51FA\u9519\u65F6\u544A\u8BC9\u7528\u6237\u67E5\u8FD9\u4E2A\u6587\u4EF6
41889
+ - \u5982\u679C \`ls ~/.clawd/contact-ssh-keys/\` \u4E3A\u7A7A\uFF0C\u8BF4\u660E\u8FD8\u6CA1\u6709\u5BF9\u7AEF\u6388\u6743\u4F60\u6216 key \u8FD8\u6CA1\u62C9\u5230\uFF08B \u4FA7 daemon \u6BCF 60s \u4E00\u6B21\u81EA\u52A8\u62C9\uFF09\uFF0C\u5982\u5B9E\u544A\u8BC9\u7528\u6237
41890
+ `;
41891
+
41513
41892
  // src/session/reducer.ts
41514
41893
  function cloneState(s) {
41515
41894
  return {
@@ -41646,6 +42025,9 @@ function buildSpawnContext(state, deps) {
41646
42025
  if (daemonUrl) {
41647
42026
  ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + ATTACHMENT_SHARING_HINT;
41648
42027
  }
42028
+ if (meta?.personaMode === "guest") {
42029
+ ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + CONTACT_SSH_SYSTEM_PROMPT_HINT;
42030
+ }
41649
42031
  if (meta?.extraSettings) {
41650
42032
  ctx.extraSettings = meta.extraSettings;
41651
42033
  }
@@ -42956,18 +43338,9 @@ var SessionManager = class {
42956
43338
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
42957
43339
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
42958
43340
  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();
43341
+ // observeScreenIdle 复合条件闸用:sessionIdobserver 上次喂出业务事件的时刻(deps.now())。
43342
+ // observerIdleWaitMs 据此判 observer 是否也静止 idleMs(屏幕静止 AND observer 静止才补 turn_end)。
43343
+ lastObserverEventAt = /* @__PURE__ */ new Map();
42971
43344
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
42972
43345
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
42973
43346
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43307,14 +43680,6 @@ var SessionManager = class {
43307
43680
  routeFromRunner(frame, target) {
43308
43681
  const compressed = compressFrameForWire(frame);
43309
43682
  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
43683
  if (compressed.type === "session:event" || compressed.type === "session:status") {
43319
43684
  const sid = compressed.sessionId;
43320
43685
  if (sid) {
@@ -43588,7 +43953,7 @@ var SessionManager = class {
43588
43953
  this.runners.delete(args.sessionId);
43589
43954
  this.realUuidBySynth.delete(args.sessionId);
43590
43955
  this.lastUiSizeBySessionId.delete(args.sessionId);
43591
- this.clearPendingTurnEnd(args.sessionId);
43956
+ this.lastObserverEventAt.delete(args.sessionId);
43592
43957
  return { response: { sessionId: args.sessionId }, broadcast };
43593
43958
  }
43594
43959
  this.deleteOwned(args.sessionId);
@@ -43618,7 +43983,6 @@ var SessionManager = class {
43618
43983
  async stop(args) {
43619
43984
  const runner = this.runners.get(args.sessionId);
43620
43985
  if (!runner) return { response: { ok: true }, broadcast: [] };
43621
- this.clearPendingTurnEnd(args.sessionId);
43622
43986
  const { broadcast } = this.withCollector(() => {
43623
43987
  runner.input({ kind: "command", command: { kind: "stop" } });
43624
43988
  });
@@ -43752,7 +44116,6 @@ var SessionManager = class {
43752
44116
  newSession(args) {
43753
44117
  const existingFile = this.getFile(args.sessionId);
43754
44118
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
43755
- this.clearPendingTurnEnd(args.sessionId);
43756
44119
  const runner = this.runners.get(args.sessionId);
43757
44120
  if (runner) {
43758
44121
  const { value, broadcast } = this.withCollector(() => {
@@ -43853,7 +44216,6 @@ var SessionManager = class {
43853
44216
  for (const r of this.runners.values()) {
43854
44217
  r.input({ kind: "command", command: { kind: "stop" } });
43855
44218
  }
43856
- this.pendingTurnDurationSignals.clear();
43857
44219
  }
43858
44220
  // 给 observer 用:拿已存在的 runner
43859
44221
  getActive(sessionId) {
@@ -44070,7 +44432,7 @@ var SessionManager = class {
44070
44432
  this.runners.delete(args.sessionId);
44071
44433
  this.realUuidBySynth.delete(args.sessionId);
44072
44434
  this.lastUiSizeBySessionId.delete(args.sessionId);
44073
- this.clearPendingTurnEnd(args.sessionId);
44435
+ this.lastObserverEventAt.delete(args.sessionId);
44074
44436
  return { response: { sessionId: args.sessionId }, broadcast };
44075
44437
  }
44076
44438
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44316,93 +44678,23 @@ var SessionManager = class {
44316
44678
  return;
44317
44679
  }
44318
44680
  }
44681
+ this.lastObserverEventAt.set(sessionId, (this.deps.now ?? Date.now)());
44319
44682
  let feedEvents = outEvents;
44320
44683
  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 {
44684
+ const ev = this.peekTurnEvidence(runner);
44685
+ if (!ev.turnHasContent) {
44338
44686
  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
- }
44687
+ this.deps.logger?.info("[TE-PROBE] drop spurious observer turn_end", {
44688
+ sessionId,
44689
+ src: "observer",
44690
+ ...ev,
44691
+ batchKinds: outEvents.map((e) => e.kind)
44692
+ });
44351
44693
  }
44352
44694
  }
44353
44695
  if (feedEvents.length === 0) return;
44354
44696
  runner.feedObserverEvents(feedEvents);
44355
44697
  }
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
44698
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44407
44699
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44408
44700
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -44661,6 +44953,70 @@ var SessionManager = class {
44661
44953
  if (!runner) return;
44662
44954
  runner.input({ kind: "ready-detected" });
44663
44955
  }
44956
+ /**
44957
+ * ClaudeTuiAdapter onTurnIdle callback:屏幕内容静止时**复发**本轮已出现过的权威 turn_end。
44958
+ * 本意:turn_duration 写盘早于尾段正文 → observer 把尾随 text 推进 buffer 盖掉 lastEventKind →
44959
+ * spinner 不熄;屏幕静止时再补一条 turn_end 排到尾随 text 之后。
44960
+ *
44961
+ * Fix A(修 bug1 "UI 还在变却显示结束" / bug2 "发消息后无 spinner"):补偿**只复发不 originate**——
44962
+ * 仅当本轮已出现过 turn_end(turnEndSeenThisTurn,即真有过尾随 text 覆盖场景)才补。turnEndSeenThisTurn
44963
+ * ===false 说明本轮 CC 从未结束过(仍在工作 / 刚发消息没产出 / 漏检弹框等用户),屏幕静止 ≠ turn 结束,
44964
+ * 凭空 inject turn_end 会误灭 spinner——故跳过,让真正的 turn_duration 来时再正常收。
44965
+ * 仅 tui 模式;runner 缺失 noop。turn_end 无 uuid 不参与 dedup。
44966
+ */
44967
+ dispatchTurnIdle(toolSessionId) {
44968
+ if (this.deps.mode !== "tui") return;
44969
+ const sid = this.sessionIdByToolSid(toolSessionId);
44970
+ const runner = sid ? this.runners.get(sid) : void 0;
44971
+ if (!runner) return;
44972
+ const ev = this.peekTurnEvidence(runner);
44973
+ const willInject = ev.turnEndSeenThisTurn;
44974
+ this.deps.logger?.info("[TE-PROBE] screen-idle compensation", {
44975
+ sessionId: sid,
44976
+ src: "screen-idle",
44977
+ willInject,
44978
+ ...ev
44979
+ });
44980
+ if (!willInject) return;
44981
+ runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44982
+ }
44983
+ /**
44984
+ * 读 runner 当前 turn 态快照,供两个 turn_end 注入源(屏幕静止补偿 / observer turn_duration)
44985
+ * 判断误发 + 打 [TE-PROBE] 日志。
44986
+ * turnEndSeenThisTurn:从 buffer 末尾回扫到最近 user_text 期间是否已出现过 turn_end
44987
+ * (已出现=本轮真结束过、合法尾随;未出现=本轮还没结束过)→ Fix A 复发闸。
44988
+ * turnHasContent:末条是否 assistant 产出(非 user_text/turn_end/空)→ Fix B 空 turn 守卫闸。
44989
+ */
44990
+ peekTurnEvidence(runner) {
44991
+ const st = runner.getState();
44992
+ const buf = st.buffer;
44993
+ const lastEventKindBefore = buf.length > 0 ? buf[buf.length - 1].event.kind : null;
44994
+ let turnEndSeenThisTurn = false;
44995
+ for (let i = buf.length - 1; i >= 0; i--) {
44996
+ const k2 = buf[i].event.kind;
44997
+ if (k2 === "user_text") break;
44998
+ if (k2 === "turn_end") {
44999
+ turnEndSeenThisTurn = true;
45000
+ break;
45001
+ }
45002
+ }
45003
+ const turnHasContent = lastEventKindBefore !== null && lastEventKindBefore !== "user_text" && lastEventKindBefore !== "turn_end";
45004
+ return { turnOpenBefore: st.turnOpen, lastEventKindBefore, turnEndSeenThisTurn, turnHasContent };
45005
+ }
45006
+ /**
45007
+ * observer 还需静止多久(ms)才满 idleMs,0 = 已满。observeScreenIdle 复合条件闸:屏幕静止后
45008
+ * 精确等这段剩余再补 turn_end —— turn_duration 写盘早于尾段正文,observer 把尾随 text poll 落盘
45009
+ * 期间屏幕可能已静止,仅看屏幕会早 fire(补的 turn_end 盖不到尾随 text 之后)。
45010
+ * 找不到 runner / 从无事件 → 0(不阻塞 fire)。idleMs 由装配处传 SCREEN_IDLE_MS。
45011
+ */
45012
+ observerIdleWaitMs(toolSessionId, idleMs) {
45013
+ const sid = this.sessionIdByToolSid(toolSessionId);
45014
+ if (!sid) return 0;
45015
+ const last = this.lastObserverEventAt.get(sid);
45016
+ if (last === void 0) return 0;
45017
+ const elapsed = (this.deps.now ?? Date.now)() - last;
45018
+ return Math.max(0, idleMs - elapsed);
45019
+ }
44664
45020
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
44665
45021
  sessionIdByToolSid(toolSessionId) {
44666
45022
  for (const [sid, runner] of this.runners) {
@@ -45809,11 +46165,7 @@ function tryLoadShareUi(logger) {
45809
46165
 
45810
46166
  // src/visitor/visitor-token.ts
45811
46167
  var import_node_crypto4 = __toESM(require("crypto"), 1);
45812
-
45813
- // ../protocol/src/index.ts
45814
- init_runtime();
45815
-
45816
- // src/visitor/visitor-token.ts
46168
+ init_src();
45817
46169
  function hmac(secret, body) {
45818
46170
  return import_node_crypto4.default.createHmac("sha256", secret).update(body).digest("base64url");
45819
46171
  }
@@ -46170,8 +46522,8 @@ function turnStartInput(text) {
46170
46522
  const items = [];
46171
46523
  let leftover = text;
46172
46524
  for (const m2 of text.matchAll(SKILL_RE)) {
46173
- const [marker, name, path68] = m2;
46174
- items.push({ type: "skill", name, path: path68 });
46525
+ const [marker, name, path76] = m2;
46526
+ items.push({ type: "skill", name, path: path76 });
46175
46527
  leftover = leftover.replace(marker, "");
46176
46528
  }
46177
46529
  for (const m2 of text.matchAll(ATTACHMENT_RE2)) {
@@ -46403,7 +46755,6 @@ var CodexAdapter = class {
46403
46755
  };
46404
46756
 
46405
46757
  // src/tools/claude-tui.ts
46406
- var import_node_crypto5 = require("crypto");
46407
46758
  var import_node_fs16 = __toESM(require("fs"), 1);
46408
46759
  var import_node_os7 = __toESM(require("os"), 1);
46409
46760
  var import_node_path14 = __toESM(require("path"), 1);
@@ -47214,56 +47565,22 @@ function observeScreenIdle(surface, opts) {
47214
47565
  timer = null;
47215
47566
  if (disposed) return;
47216
47567
  if (opts.getPopupVisible()) {
47217
- opts.probeLogger?.info("screen-idle fire suppressed: popup visible", {
47218
- label: opts.probeLabel
47219
- });
47220
47568
  timer = setTimeout(fire, opts.idleMs);
47221
47569
  return;
47222
47570
  }
47223
47571
  const obsWait = opts.getObserverWaitMs?.() ?? 0;
47224
47572
  if (obsWait > 0) {
47225
- opts.probeLogger?.info("screen-idle fire suppressed: observer not idle", {
47226
- label: opts.probeLabel,
47227
- obsWait
47228
- });
47229
47573
  timer = setTimeout(fire, Math.max(obsWait, REWAIT_MIN_MS));
47230
47574
  return;
47231
47575
  }
47232
- if (armed) {
47233
- opts.probeLogger?.debug("screen-idle fire noop: already armed", {
47234
- label: opts.probeLabel
47235
- });
47236
- return;
47237
- }
47576
+ if (armed) return;
47238
47577
  armed = true;
47239
- opts.probeLogger?.info("screen-idle fire triggered \u2192 armed=true, calling onIdle", {
47240
- label: opts.probeLabel
47241
- });
47242
47578
  opts.onIdle();
47243
47579
  };
47244
47580
  const unsub = surface.onTick((lines) => {
47245
47581
  if (disposed) return;
47246
47582
  const snap = snapOf(lines);
47247
47583
  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
47584
  lastSnap = snap;
47268
47585
  armed = false;
47269
47586
  clear();
@@ -47274,38 +47591,9 @@ function observeScreenIdle(surface, opts) {
47274
47591
  disposed = true;
47275
47592
  unsub();
47276
47593
  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
47594
  }
47291
47595
  };
47292
47596
  }
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
47597
  var BYPASS_SETTLE_MS = 300;
47310
47598
  var SCREEN_IDLE_MS = 5e3;
47311
47599
  function createBootGate(pty, logger) {
@@ -47380,42 +47668,11 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47380
47668
  // 用于 spawn / PtyChildProcess 链路打日志
47381
47669
  tuiLogger;
47382
47670
  tuiOpts;
47383
- /**
47384
- * per-toolSessionId 的 tui 观察者句柄,仅用于 turn_end gate 查询(`canAcceptTurnEnd`)。
47385
- * onIdle / onPopupTransition 等回调仍走原有闭包(不复用这份 map),本 map 只承担
47386
- * "manager 需要跨模块查屏幕/弹框状态"这单一职责。
47387
- */
47388
- tuiStates = /* @__PURE__ */ new Map();
47389
47671
  constructor(opts = {}) {
47390
47672
  super(opts);
47391
47673
  this.tuiLogger = opts.logger;
47392
47674
  this.tuiOpts = opts;
47393
47675
  }
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
47676
  spawn(ctx) {
47420
47677
  const args = buildTuiSpawnArgs(ctx, jsonlExistsForCtx(ctx));
47421
47678
  const cmd = process.env.CLAUDE_BIN ?? "claude";
@@ -47473,26 +47730,18 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47473
47730
  const screenIdleObserver = observeScreenIdle(surface, {
47474
47731
  idleMs: SCREEN_IDLE_MS,
47475
47732
  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);
47733
+ if (!ctx.toolSessionId || !this.tuiOpts.onTurnIdle) return;
47734
+ this.tuiLogger?.debug("screen-idle \u2192 turn_end", { toolSessionId: ctx.toolSessionId });
47735
+ this.tuiOpts.onTurnIdle(ctx.toolSessionId);
47479
47736
  },
47480
47737
  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
- } : {}
47738
+ // observer 还需静止多久才满 SCREEN_IDLE_MS(复合条件 AND):屏幕静止后精确等这段剩余再补
47739
+ // turn_end,确保它排在尾段 text + turn_duration 全部 poll 落盘之后 = buffer 末条。
47740
+ getObserverWaitMs: () => ctx.toolSessionId ? this.tuiOpts.getObserverWaitMs?.(ctx.toolSessionId, SCREEN_IDLE_MS) ?? 0 : 0
47486
47741
  });
47487
47742
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceRegister) {
47488
47743
  this.tuiOpts.onSurfaceRegister(ctx.toolSessionId, surface);
47489
47744
  }
47490
- if (ctx.toolSessionId) {
47491
- this.tuiStates.set(ctx.toolSessionId, {
47492
- screenIdle: screenIdleObserver,
47493
- popup: popupObserver
47494
- });
47495
- }
47496
47745
  let chunkSeq = 0;
47497
47746
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
47498
47747
  this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
@@ -47538,9 +47787,6 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47538
47787
  readyObserver.dispose();
47539
47788
  popupObserver.dispose();
47540
47789
  screenIdleObserver.dispose();
47541
- if (ctx.toolSessionId) {
47542
- this.tuiStates.delete(ctx.toolSessionId);
47543
- }
47544
47790
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceUnregister) {
47545
47791
  this.tuiOpts.onSurfaceUnregister(ctx.toolSessionId);
47546
47792
  }
@@ -47823,7 +48069,7 @@ async function writeInboxMcpConfig(args) {
47823
48069
  // src/shift/store.ts
47824
48070
  var import_promises = __toESM(require("fs/promises"), 1);
47825
48071
  var import_node_path19 = __toESM(require("path"), 1);
47826
- var import_node_crypto6 = require("crypto");
48072
+ var import_node_crypto5 = require("crypto");
47827
48073
 
47828
48074
  // src/shift/constants.ts
47829
48075
  var MAX_RUNS_PER_SHIFT = 30;
@@ -47919,7 +48165,7 @@ function createShiftStore(deps) {
47919
48165
  const nextRunAtMs = computeNextRunAtMs(input.schedule, now) ?? void 0;
47920
48166
  const shift = {
47921
48167
  ...input,
47922
- id: (0, import_node_crypto6.randomUUID)(),
48168
+ id: (0, import_node_crypto5.randomUUID)(),
47923
48169
  createdAtMs: now,
47924
48170
  updatedAtMs: now,
47925
48171
  state: { nextRunAtMs },
@@ -48436,78 +48682,8 @@ function buildPersonaDispatchHandlers(deps) {
48436
48682
  };
48437
48683
  }
48438
48684
 
48439
- // src/dispatch/peer-forward.ts
48440
- function wsUrlToHttp(url) {
48441
- if (url.startsWith("wss://")) return "https://" + url.slice("wss://".length);
48442
- if (url.startsWith("ws://")) return "http://" + url.slice("ws://".length);
48443
- return url;
48444
- }
48445
- async function forwardDispatchToPeer(args) {
48446
- const f = args.fetchImpl ?? fetch;
48447
- const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
48448
- const url = `${base}/rpc/personaDispatch:run`;
48449
- let res;
48450
- try {
48451
- res = await f(url, {
48452
- method: "POST",
48453
- headers: {
48454
- "content-type": "application/json",
48455
- authorization: `Bearer ${args.contact.connectToken}`
48456
- },
48457
- // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
48458
- body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
48459
- });
48460
- } catch (err) {
48461
- const msg = err instanceof Error ? err.message : String(err);
48462
- return { kind: "failure", reason: `forward to peer failed: ${msg}` };
48463
- }
48464
- let json;
48465
- try {
48466
- json = await res.json();
48467
- } catch {
48468
- return {
48469
- kind: "failure",
48470
- reason: `peer returned non-JSON response (HTTP ${res.status})`
48471
- };
48472
- }
48473
- if (json.ok === false) {
48474
- return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
48475
- }
48476
- return json.result.outcome;
48477
- }
48478
- async function forwardInboxPostToPeer(args) {
48479
- const f = args.fetchImpl ?? fetch;
48480
- const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
48481
- const url = `${base}/rpc/inbox:postMessage`;
48482
- let res;
48483
- try {
48484
- res = await f(url, {
48485
- method: "POST",
48486
- headers: {
48487
- "content-type": "application/json",
48488
- authorization: `Bearer ${args.contact.connectToken}`
48489
- },
48490
- body: JSON.stringify({
48491
- id: args.id,
48492
- text: args.text,
48493
- createdAt: args.createdAt,
48494
- ...args.origin ? { origin: args.origin } : {}
48495
- })
48496
- });
48497
- } catch (err) {
48498
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
48499
- }
48500
- let json = null;
48501
- try {
48502
- json = await res.json();
48503
- } catch {
48504
- return { ok: false, error: `peer non-JSON (HTTP ${res.status})` };
48505
- }
48506
- if (res.status < 200 || res.status >= 300 || json?.ok === false) {
48507
- return { ok: false, error: json?.error ?? `HTTP ${res.status}` };
48508
- }
48509
- return { ok: true };
48510
- }
48685
+ // src/index.ts
48686
+ init_peer_forward();
48511
48687
 
48512
48688
  // src/tools/codex-history.ts
48513
48689
  var import_node_child_process5 = require("child_process");
@@ -48695,13 +48871,13 @@ function mapSkillsListResponse(res) {
48695
48871
  const r = s ?? {};
48696
48872
  const name = str3(r.name);
48697
48873
  if (!name) continue;
48698
- const path68 = str3(r.path);
48874
+ const path76 = str3(r.path);
48699
48875
  const description = str3(r.description);
48700
48876
  const isPlugin = name.includes(":");
48701
48877
  out.push({
48702
48878
  name,
48703
48879
  source: isPlugin ? "plugin" : "project",
48704
- ...path68 ? { path: path68 } : {},
48880
+ ...path76 ? { path: path76 } : {},
48705
48881
  ...description ? { description } : {},
48706
48882
  ...isPlugin ? { plugin: name.split(":")[0] } : {}
48707
48883
  });
@@ -49350,18 +49526,8 @@ function listRegistered() {
49350
49526
  return [...registry.keys()];
49351
49527
  }
49352
49528
 
49353
- // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
49354
- var import_stream = __toESM(require_stream(), 1);
49355
- var import_extension = __toESM(require_extension(), 1);
49356
- var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
49357
- var import_receiver = __toESM(require_receiver(), 1);
49358
- var import_sender = __toESM(require_sender(), 1);
49359
- var import_subprotocol = __toESM(require_subprotocol(), 1);
49360
- var import_websocket = __toESM(require_websocket(), 1);
49361
- var import_websocket_server = __toESM(require_websocket_server(), 1);
49362
- var wrapper_default = import_websocket.default;
49363
-
49364
49529
  // src/transport/local-ws-server.ts
49530
+ init_wrapper();
49365
49531
  var import_node_http2 = __toESM(require("http"), 1);
49366
49532
 
49367
49533
  // src/transport/preview-proxy.ts
@@ -49437,6 +49603,18 @@ var LocalWsServer = class {
49437
49603
  const httpServer = import_node_http2.default.createServer((req, res) => this.handleHttpRequest(req, res));
49438
49604
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
49439
49605
  httpServer.on("upgrade", (req, socket, head) => {
49606
+ if (this.opts.sshTunnelUpgradeHandler) {
49607
+ const [urlPath] = (req.url ?? "").split("?");
49608
+ if (urlPath === "/rpc/ssh-tunnel") {
49609
+ void this.opts.sshTunnelUpgradeHandler(
49610
+ req,
49611
+ socket,
49612
+ head,
49613
+ wss
49614
+ );
49615
+ return;
49616
+ }
49617
+ }
49440
49618
  if (req.url?.startsWith("/preview/")) {
49441
49619
  const pathname = (() => {
49442
49620
  try {
@@ -50009,6 +50187,7 @@ function constantTimeEqual2(a, b2) {
50009
50187
  }
50010
50188
 
50011
50189
  // src/transport/connection-context.ts
50190
+ init_src();
50012
50191
  function ownerContext(ownerPrincipalId, displayName) {
50013
50192
  return {
50014
50193
  principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
@@ -50062,6 +50241,7 @@ async function authenticate(token, deps) {
50062
50241
  // src/permission/capability-store.ts
50063
50242
  var fs28 = __toESM(require("fs"), 1);
50064
50243
  var path28 = __toESM(require("path"), 1);
50244
+ init_src();
50065
50245
  var CAPABILITIES_FILE_NAME = "capabilities.json";
50066
50246
  var FILE_VERSION = 1;
50067
50247
  var CapabilityStore = class {
@@ -50239,6 +50419,7 @@ function cleanupGuestSessionsForCapability(cap, factory) {
50239
50419
  // src/inbox/inbox-store.ts
50240
50420
  var fs30 = __toESM(require("fs"), 1);
50241
50421
  var path29 = __toESM(require("path"), 1);
50422
+ init_src();
50242
50423
  var INBOX_SUBDIR = "inbox";
50243
50424
  var InboxStore = class {
50244
50425
  constructor(dataDir) {
@@ -50429,6 +50610,7 @@ var InboxManager = class {
50429
50610
  // src/state/contact-store.ts
50430
50611
  var fs31 = __toESM(require("fs"), 1);
50431
50612
  var path30 = __toESM(require("path"), 1);
50613
+ init_src();
50432
50614
  var FILE_NAME = "contacts.json";
50433
50615
  var ContactStore = class {
50434
50616
  constructor(dataDir) {
@@ -50495,6 +50677,23 @@ var ContactStore = class {
50495
50677
  this.flush();
50496
50678
  return true;
50497
50679
  }
50680
+ /**
50681
+ * 更新单条 contact 的 SSH 授权(PR: contact-ssh-sandbox)。对齐 setPin pattern:
50682
+ * store 只做原始 mutation,不做业务校验(如"sshAllowed=false 时清空 exposedDirs")——
50683
+ * 那是 handler / UI 的责任。数组语义是完全替换(不 append)。
50684
+ * @returns 是否命中:deviceId 不存在返 false;命中即 flush.
50685
+ */
50686
+ setSshAccess(deviceId, opts) {
50687
+ const existing = this.contacts.get(deviceId);
50688
+ if (!existing) return false;
50689
+ this.contacts.set(deviceId, {
50690
+ ...existing,
50691
+ sshAllowed: opts.sshAllowed,
50692
+ exposedDirs: opts.exposedDirs
50693
+ });
50694
+ this.flush();
50695
+ return true;
50696
+ }
50498
50697
  flush() {
50499
50698
  const file = path30.join(this.dataDir, FILE_NAME);
50500
50699
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
@@ -50516,6 +50715,7 @@ var ContactStore = class {
50516
50715
  };
50517
50716
 
50518
50717
  // src/contact/connect-remote.ts
50718
+ init_wrapper();
50519
50719
  var crypto6 = __toESM(require("crypto"), 1);
50520
50720
  var HANDSHAKE_TIMEOUT_MS = 5e3;
50521
50721
  var RPC_TIMEOUT_MS = 5e3;
@@ -50642,7 +50842,9 @@ async function autoReverseContact(args) {
50642
50842
  connectToken: "",
50643
50843
  grants,
50644
50844
  addedAt: now(),
50645
- pinnedAt: null
50845
+ pinnedAt: null,
50846
+ sshAllowed: false,
50847
+ exposedDirs: []
50646
50848
  };
50647
50849
  args.store.upsert(base);
50648
50850
  args.broadcast({ type: "contact:added", contact: base });
@@ -50659,6 +50861,9 @@ async function autoReverseContact(args) {
50659
50861
  args.broadcast({ type: "contact:added", contact: upgraded });
50660
50862
  }
50661
50863
 
50864
+ // src/index.ts
50865
+ init_src();
50866
+
50662
50867
  // src/migrations/2026-05-20-flatten-sessions.ts
50663
50868
  var fs32 = __toESM(require("fs"), 1);
50664
50869
  var path31 = __toESM(require("path"), 1);
@@ -50871,7 +51076,7 @@ function lookupMime(filePathOrName) {
50871
51076
  }
50872
51077
 
50873
51078
  // src/attachment/sign-url.ts
50874
- var import_node_crypto7 = __toESM(require("crypto"), 1);
51079
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
50875
51080
  var HMAC_ALGO = "sha256";
50876
51081
  function base64urlEncode(buf) {
50877
51082
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -50888,7 +51093,7 @@ function decodeAbsPathFromUrl(encoded) {
50888
51093
  }
50889
51094
  function computeSig(secret, absPath, e) {
50890
51095
  const msg = e === null ? absPath : `${absPath}|${e}`;
50891
- return import_node_crypto7.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
51096
+ return import_node_crypto6.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
50892
51097
  }
50893
51098
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
50894
51099
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -50923,7 +51128,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50923
51128
  if (provided.length !== expected.length) {
50924
51129
  return { ok: false, code: "BAD_SIG" };
50925
51130
  }
50926
- if (!import_node_crypto7.default.timingSafeEqual(provided, expected)) {
51131
+ if (!import_node_crypto6.default.timingSafeEqual(provided, expected)) {
50927
51132
  return { ok: false, code: "BAD_SIG" };
50928
51133
  }
50929
51134
  if (e !== null && now() / 1e3 > e) {
@@ -50935,7 +51140,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
50935
51140
  // src/attachment/upload.ts
50936
51141
  var import_node_fs25 = __toESM(require("fs"), 1);
50937
51142
  var import_node_path25 = __toESM(require("path"), 1);
50938
- var import_node_crypto8 = __toESM(require("crypto"), 1);
51143
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
50939
51144
  var import_promises2 = require("stream/promises");
50940
51145
  var UploadError = class extends Error {
50941
51146
  constructor(code, message) {
@@ -50959,11 +51164,11 @@ async function writeUploadedAttachment(args) {
50959
51164
  } catch (err) {
50960
51165
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
50961
51166
  }
50962
- const hasher = import_node_crypto8.default.createHash("sha256");
51167
+ const hasher = import_node_crypto7.default.createHash("sha256");
50963
51168
  let actualSize = 0;
50964
51169
  const tmpPath = import_node_path25.default.join(
50965
51170
  attachmentsRoot,
50966
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto8.default.randomBytes(4).toString("hex")}`
51171
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto7.default.randomBytes(4).toString("hex")}`
50967
51172
  );
50968
51173
  try {
50969
51174
  await (0, import_promises2.pipeline)(
@@ -51041,6 +51246,7 @@ var import_promises3 = __toESM(require("fs/promises"), 1);
51041
51246
  var import_node_path26 = __toESM(require("path"), 1);
51042
51247
  var import_node_os12 = __toESM(require("os"), 1);
51043
51248
  var import_jszip = __toESM(require_lib3(), 1);
51249
+ init_src();
51044
51250
  var ImportError = class extends Error {
51045
51251
  constructor(code, message) {
51046
51252
  super(message);
@@ -51839,7 +52045,7 @@ function runAttachmentGc(args) {
51839
52045
  // src/attachment/group.ts
51840
52046
  var import_node_fs28 = __toESM(require("fs"), 1);
51841
52047
  var import_node_path29 = __toESM(require("path"), 1);
51842
- var import_node_crypto9 = __toESM(require("crypto"), 1);
52048
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
51843
52049
  init_protocol();
51844
52050
  var GroupFileStore = class {
51845
52051
  dataDir;
@@ -51928,7 +52134,7 @@ var GroupFileStore = class {
51928
52134
  entries[idx] = next;
51929
52135
  } else {
51930
52136
  next = {
51931
- id: `gf-${import_node_crypto9.default.randomBytes(6).toString("base64url")}`,
52137
+ id: `gf-${import_node_crypto8.default.randomBytes(6).toString("base64url")}`,
51932
52138
  relPath: input.relPath,
51933
52139
  from: input.from,
51934
52140
  label: input.label,
@@ -52047,7 +52253,7 @@ function readDaemonSourceFromEnv(env = process.env) {
52047
52253
  // src/tunnel/tunnel-manager.ts
52048
52254
  var import_node_fs33 = __toESM(require("fs"), 1);
52049
52255
  var import_node_path34 = __toESM(require("path"), 1);
52050
- var import_node_crypto10 = __toESM(require("crypto"), 1);
52256
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
52051
52257
  var import_node_child_process9 = require("child_process");
52052
52258
 
52053
52259
  // src/tunnel/tunnel-store.ts
@@ -52546,7 +52752,7 @@ var TunnelManager = class {
52546
52752
  override: this.deps.frpcBinaryOverride ?? void 0
52547
52753
  });
52548
52754
  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")}`;
52755
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto9.default.randomBytes(3).toString("hex")}`;
52550
52756
  const toml = buildFrpcToml({
52551
52757
  serverAddr: t.frpsHost,
52552
52758
  serverPort: t.frpsPort,
@@ -52642,29 +52848,795 @@ async function waitForFrpcReady(proc, timeoutMs) {
52642
52848
  });
52643
52849
  }
52644
52850
 
52851
+ // src/sshd/sshd-manager.ts
52852
+ var import_node_fs36 = __toESM(require("fs"), 1);
52853
+ var import_node_path37 = __toESM(require("path"), 1);
52854
+ var import_node_child_process11 = require("child_process");
52855
+
52856
+ // src/sshd/sshd-config.ts
52857
+ function buildSshdConfig(input) {
52858
+ const lines = [
52859
+ `ListenAddress ${input.listenAddress}`,
52860
+ `Port ${input.port}`,
52861
+ `HostKey ${input.hostKeyPath}`,
52862
+ `PidFile ${input.pidFilePath}`,
52863
+ `AuthorizedKeysFile ${input.authorizedKeysFile}`,
52864
+ `PubkeyAuthentication yes`,
52865
+ `PasswordAuthentication no`,
52866
+ `ChallengeResponseAuthentication no`,
52867
+ `KbdInteractiveAuthentication no`,
52868
+ `PermitRootLogin no`,
52869
+ `StrictModes no`,
52870
+ `UsePAM no`,
52871
+ `LogLevel INFO`,
52872
+ `Subsystem sftp internal-sftp`
52873
+ ];
52874
+ return lines.join("\n") + "\n";
52875
+ }
52876
+
52877
+ // src/sshd/sshd-process.ts
52878
+ var import_node_fs34 = __toESM(require("fs"), 1);
52879
+ var import_node_path35 = __toESM(require("path"), 1);
52880
+ var import_node_child_process10 = require("child_process");
52881
+ function sshdPidFilePath(dataDir) {
52882
+ return import_node_path35.default.join(dataDir, "sshd", "sshd.pid");
52883
+ }
52884
+ function writeSshdPid(dataDir, pid) {
52885
+ try {
52886
+ const p2 = sshdPidFilePath(dataDir);
52887
+ import_node_fs34.default.mkdirSync(import_node_path35.default.dirname(p2), { recursive: true, mode: 448 });
52888
+ import_node_fs34.default.writeFileSync(p2, String(pid), { mode: 384 });
52889
+ } catch {
52890
+ }
52891
+ }
52892
+ function clearSshdPid(dataDir) {
52893
+ try {
52894
+ import_node_fs34.default.unlinkSync(sshdPidFilePath(dataDir));
52895
+ } catch {
52896
+ }
52897
+ }
52898
+ function defaultIsPidAlive2(pid) {
52899
+ if (!Number.isFinite(pid) || pid <= 0) return false;
52900
+ try {
52901
+ process.kill(pid, 0);
52902
+ return true;
52903
+ } catch (err) {
52904
+ const code = err.code;
52905
+ return code === "EPERM";
52906
+ }
52907
+ }
52908
+ function defaultReadPidFile2(file) {
52909
+ try {
52910
+ return import_node_fs34.default.readFileSync(file, "utf8");
52911
+ } catch {
52912
+ return null;
52913
+ }
52914
+ }
52915
+ function defaultKillPid2(pid, signal) {
52916
+ try {
52917
+ process.kill(pid, signal);
52918
+ } catch {
52919
+ }
52920
+ }
52921
+ function defaultSleep2(ms) {
52922
+ return new Promise((r) => setTimeout(r, ms));
52923
+ }
52924
+ async function killStaleSshd(deps) {
52925
+ const pidFile = sshdPidFilePath(deps.dataDir);
52926
+ const configPath = import_node_path35.default.join(deps.dataDir, "sshd", "sshd_config");
52927
+ const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile2;
52928
+ const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive2;
52929
+ const killPid = deps.killPidImpl ?? defaultKillPid2;
52930
+ const scanPids = deps.scanSshdPidsImpl ?? ((cp) => defaultScanSshdPidsByCmdline(cp, deps.logger));
52931
+ const sleep2 = deps.sleepImpl ?? defaultSleep2;
52932
+ const victims = /* @__PURE__ */ new Set();
52933
+ const raw = readPidFile(pidFile);
52934
+ if (raw) {
52935
+ const pid = parseInt(raw.trim(), 10);
52936
+ if (Number.isFinite(pid) && pid > 0 && pid !== deps.ownPid && isAlive(pid)) {
52937
+ victims.add(pid);
52938
+ }
52939
+ }
52940
+ try {
52941
+ const scanned = await scanPids(configPath);
52942
+ for (const pid of scanned) {
52943
+ if (pid > 0 && pid !== deps.ownPid && isAlive(pid)) victims.add(pid);
52944
+ }
52945
+ } catch (e) {
52946
+ deps.logger?.warn("sshd: stale-sshd cmdline scan failed", { err: e.message });
52947
+ }
52948
+ if (victims.size === 0) {
52949
+ try {
52950
+ import_node_fs34.default.unlinkSync(pidFile);
52951
+ } catch {
52952
+ }
52953
+ return;
52954
+ }
52955
+ for (const pid of victims) {
52956
+ deps.logger?.warn("sshd: killing stale sshd before respawn", { pid });
52957
+ killPid(pid, "SIGKILL");
52958
+ }
52959
+ await sleep2(deps.reapWaitMs ?? 300);
52960
+ try {
52961
+ import_node_fs34.default.unlinkSync(pidFile);
52962
+ } catch {
52963
+ }
52964
+ }
52965
+ async function defaultScanSshdPidsByCmdline(configPath, logger) {
52966
+ if (process.platform === "win32") return [];
52967
+ return new Promise((resolve6) => {
52968
+ const ps = (0, import_node_child_process10.spawn)("ps", ["-axo", "pid=,command="], { stdio: ["ignore", "pipe", "ignore"] });
52969
+ let buf = "";
52970
+ ps.stdout.on("data", (c) => {
52971
+ buf += c.toString();
52972
+ });
52973
+ ps.on("exit", () => {
52974
+ const pids = [];
52975
+ for (const line of buf.split("\n")) {
52976
+ const m2 = /^\s*(\d+)\s+(.*)$/.exec(line);
52977
+ if (!m2) continue;
52978
+ const cmd = m2[2];
52979
+ if (!/\bsshd\b/.test(cmd)) continue;
52980
+ if (!cmd.includes(configPath)) continue;
52981
+ const pid = parseInt(m2[1], 10);
52982
+ if (Number.isFinite(pid) && pid > 0) pids.push(pid);
52983
+ }
52984
+ resolve6(pids);
52985
+ });
52986
+ ps.on("error", (e) => {
52987
+ logger?.warn("sshd: ps scan failed", { err: e.message });
52988
+ resolve6([]);
52989
+ });
52990
+ });
52991
+ }
52992
+
52993
+ // src/sshd/jail-script.ts
52994
+ var import_node_fs35 = __toESM(require("fs"), 1);
52995
+ var import_node_path36 = __toESM(require("path"), 1);
52996
+ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
52997
+ # clawd-ssh-jail — SSH reverse access sandbox wrapper (managed by clawd; do not edit)
52998
+ #
52999
+ # 由 sshd authorized_keys 的 command= 强制入口调用。
53000
+ # 用法: sshd 会以 \`clawd-ssh-jail <deviceId>\` 起本脚本;$SSH_ORIGINAL_COMMAND = client
53001
+ # 真实请求(interactive shell 时为空)。
53002
+ #
53003
+ # 职责:
53004
+ # 1. 读 ~/.clawd/contacts.json 找 contact.exposedDirs
53005
+ # 2. macOS 用 sandbox-exec + sbpl; Linux 用 bwrap
53006
+ # 3. exec 沙箱 shell
53007
+
53008
+ set -euo pipefail
53009
+
53010
+ # 用户可读审计日志:追加到 ~/.clawd/contact-ssh.log(跟 daemon TS 侧 contact-ssh-log.ts 共写)
53011
+ CONTACT_SSH_LOG="\${HOME}/.clawd/contact-ssh.log"
53012
+ log_line() {
53013
+ # 参数: level tag msg
53014
+ local ts
53015
+ ts=$(date -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")
53016
+ printf '[%s] [%s] [%s] %s\n' "$ts" "$1" "$2" "$3" >> "$CONTACT_SSH_LOG" 2>/dev/null || true
53017
+ }
53018
+
53019
+ DEVICE_ID="\${1:-}"
53020
+ if [ -z "$DEVICE_ID" ]; then
53021
+ log_line ERROR jail.entered "clawd-ssh-jail 缺 deviceId 参数"
53022
+ echo "clawd-ssh-jail: missing deviceId" >&2
53023
+ exit 1
53024
+ fi
53025
+
53026
+ log_line INFO jail.entered "clawd-ssh-jail 起来 device=\${DEVICE_ID} cmd=\${SSH_ORIGINAL_COMMAND:-<interactive-shell>}"
53027
+
53028
+ CONTACTS="\${HOME}/.clawd/contacts.json"
53029
+ if [ ! -f "$CONTACTS" ]; then
53030
+ log_line ERROR jail.entered "contacts.json 不存在,无法查 exposedDirs"
53031
+ echo "clawd-ssh-jail: contacts.json missing" >&2
53032
+ exit 1
53033
+ fi
53034
+
53035
+ # 读 contact 的 exposedDirs (mac/linux 都自带 python3)
53036
+ EXPOSED_JSON=$(python3 -c "
53037
+ import json, sys
53038
+ with open('$CONTACTS') as f:
53039
+ data = json.load(f)
53040
+ for c in data.get('contacts', []):
53041
+ if c.get('deviceId') == '$DEVICE_ID':
53042
+ if not c.get('sshAllowed'):
53043
+ print('DENIED', file=sys.stderr); sys.exit(2)
53044
+ for d in c.get('exposedDirs', []):
53045
+ print(d)
53046
+ sys.exit(0)
53047
+ sys.exit(3)
53048
+ ")
53049
+
53050
+ if [ -z "$EXPOSED_JSON" ]; then
53051
+ log_line ERROR jail.entered "contact \${DEVICE_ID} 不在 store 或 exposedDirs 为空"
53052
+ echo "clawd-ssh-jail: contact not found or no exposed dirs" >&2
53053
+ exit 1
53054
+ fi
53055
+ log_line INFO jail.entered "exposedDirs 白名单已加载,进沙箱 exec shell"
53056
+
53057
+ # 进沙箱前告知 SSH client 授权目录列表(发到 stderr)—— CC 或其他 SSH client 试错一次就能
53058
+ # 学到 exposedDirs 具体值,无需 A 侧向 B 侧同步(授权数据实时的 single source of truth)。
53059
+ # 每次拨号运行时读,A 改 exposedDirs 后 B 侧下一次拨号立即感知。
53060
+ echo "[clawd-ssh-jail] Access allowed to these directories:" >&2
53061
+ while IFS= read -r d; do
53062
+ echo " - $d" >&2
53063
+ done <<< "$EXPOSED_JSON"
53064
+ echo "[clawd-ssh-jail] Anything outside these paths will be sandbox-denied." >&2
53065
+
53066
+ # 校验路径安全(bash 侧二次防御)
53067
+ while IFS= read -r line; do
53068
+ case "$line" in
53069
+ /*) : ;;
53070
+ *) echo "clawd-ssh-jail: bad path: $line" >&2; exit 1 ;;
53071
+ esac
53072
+ case "$line" in
53073
+ *[\"\'\`\$\;\|\&\(\)\{\}\[\]\<\>\*\?]*)
53074
+ echo "clawd-ssh-jail: unsafe path: $line" >&2; exit 1 ;;
53075
+ esac
53076
+ done <<< "$EXPOSED_JSON"
53077
+
53078
+ CMD="\${SSH_ORIGINAL_COMMAND:-}"
53079
+ if [ -z "$CMD" ]; then
53080
+ SHELL_CMD=(bash --login)
53081
+ else
53082
+ SHELL_CMD=(bash -c "$CMD")
53083
+ fi
53084
+
53085
+ case "$(uname -s)" in
53086
+ Darwin)
53087
+ POLICY="(version 1)
53088
+ (deny default)
53089
+ (allow process*)
53090
+ (allow signal (target self))
53091
+ (allow sysctl-read)
53092
+ (allow mach-lookup)
53093
+ (allow file-read-metadata)
53094
+ (allow network*)
53095
+ (allow file-read* (subpath \"/usr\"))
53096
+ (allow file-read* (subpath \"/bin\"))
53097
+ (allow file-read* (subpath \"/sbin\"))
53098
+ (allow file-read* (subpath \"/System\"))
53099
+ (allow file-read* (subpath \"/Library\"))
53100
+ (allow file-read* (subpath \"/etc\"))
53101
+ (allow file-read* (subpath \"/private/etc\"))"
53102
+ while IFS= read -r d; do
53103
+ POLICY+="
53104
+ (allow file-read* file-write* (subpath \"$d\"))"
53105
+ done <<< "$EXPOSED_JSON"
53106
+ exec sandbox-exec -p "$POLICY" "\${SHELL_CMD[@]}"
53107
+ ;;
53108
+ Linux)
53109
+ BWRAP_ARGS=(
53110
+ --unshare-user --unshare-ipc --unshare-pid --unshare-uts
53111
+ --die-with-parent
53112
+ --proc /proc --dev /dev --tmpfs /tmp
53113
+ --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /sbin /sbin
53114
+ --ro-bind /lib /lib --ro-bind /etc /etc
53115
+ )
53116
+ if [ -d /lib64 ]; then BWRAP_ARGS+=(--ro-bind /lib64 /lib64); fi
53117
+ while IFS= read -r d; do
53118
+ BWRAP_ARGS+=(--bind "$d" "$d")
53119
+ done <<< "$EXPOSED_JSON"
53120
+ exec bwrap "\${BWRAP_ARGS[@]}" "\${SHELL_CMD[@]}"
53121
+ ;;
53122
+ *)
53123
+ echo "clawd-ssh-jail: unsupported OS $(uname -s)" >&2
53124
+ exit 1
53125
+ ;;
53126
+ esac
53127
+ `;
53128
+ function ensureJailScript(dataDir) {
53129
+ const binDir = import_node_path36.default.join(dataDir, "bin");
53130
+ import_node_fs35.default.mkdirSync(binDir, { recursive: true, mode: 493 });
53131
+ const target = import_node_path36.default.join(binDir, "clawd-ssh-jail");
53132
+ import_node_fs35.default.writeFileSync(target, CLAWD_SSH_JAIL_SCRIPT, { mode: 493 });
53133
+ return target;
53134
+ }
53135
+
53136
+ // src/sshd/sshd-manager.ts
53137
+ var SshdManager = class {
53138
+ constructor(deps) {
53139
+ this.deps = deps;
53140
+ this.sshdDir = import_node_path37.default.join(deps.dataDir, "sshd");
53141
+ this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
53142
+ }
53143
+ deps;
53144
+ proc = null;
53145
+ sshdDir;
53146
+ stopping = false;
53147
+ exitHookInstalled = false;
53148
+ startupTimeoutMs;
53149
+ get port() {
53150
+ return this.deps.port;
53151
+ }
53152
+ async start() {
53153
+ const { logger } = this.deps;
53154
+ await (this.deps.killStaleImpl ?? killStaleSshd)({
53155
+ dataDir: this.deps.dataDir,
53156
+ ownPid: process.pid,
53157
+ logger
53158
+ });
53159
+ import_node_fs36.default.mkdirSync(this.sshdDir, { recursive: true, mode: 448 });
53160
+ import_node_fs36.default.mkdirSync(import_node_path37.default.join(this.sshdDir, "authorized_keys.d"), { recursive: true, mode: 448 });
53161
+ ensureJailScript(this.deps.dataDir);
53162
+ const hostKeyPath = import_node_path37.default.join(this.sshdDir, "host_key");
53163
+ if (!import_node_fs36.default.existsSync(hostKeyPath)) {
53164
+ await this.generateHostKey(hostKeyPath);
53165
+ }
53166
+ const akFile = import_node_path37.default.join(this.sshdDir, "authorized_keys.d", "clawd-contacts");
53167
+ if (!import_node_fs36.default.existsSync(akFile)) {
53168
+ import_node_fs36.default.writeFileSync(akFile, "", { mode: 384 });
53169
+ }
53170
+ const configPath = import_node_path37.default.join(this.sshdDir, "sshd_config");
53171
+ const config = buildSshdConfig({
53172
+ listenAddress: "127.0.0.1",
53173
+ port: this.deps.port,
53174
+ hostKeyPath,
53175
+ authorizedKeysFile: akFile,
53176
+ pidFilePath: import_node_path37.default.join(this.sshdDir, "sshd.pid")
53177
+ });
53178
+ import_node_fs36.default.writeFileSync(configPath, config, { mode: 384 });
53179
+ const sshdBin = this.deps.sshdBin ?? "/usr/sbin/sshd";
53180
+ const proc = (this.deps.spawnImpl ?? import_node_child_process11.spawn)(sshdBin, ["-D", "-e", "-f", configPath], {
53181
+ stdio: ["ignore", "pipe", "pipe"]
53182
+ });
53183
+ const logStream = import_node_fs36.default.createWriteStream(import_node_path37.default.join(this.sshdDir, "sshd.log"), {
53184
+ flags: "a",
53185
+ mode: 384
53186
+ });
53187
+ logStream.on("error", () => {
53188
+ });
53189
+ const tee = (c) => {
53190
+ logStream.write(String(c));
53191
+ };
53192
+ proc.stdout?.on("data", tee);
53193
+ proc.stderr?.on("data", tee);
53194
+ proc.once("exit", () => logStream.end());
53195
+ const ready = await waitForSshdReady(proc, this.startupTimeoutMs);
53196
+ if (!ready.ok) {
53197
+ try {
53198
+ proc.kill("SIGTERM");
53199
+ } catch {
53200
+ }
53201
+ const tail = ready.output.slice(-500);
53202
+ const msg = tail ? `${ready.error}
53203
+ ${tail}` : ready.error;
53204
+ throw new Error(msg);
53205
+ }
53206
+ if (typeof proc.pid === "number") writeSshdPid(this.deps.dataDir, proc.pid);
53207
+ this.proc = proc;
53208
+ this.installProcessExitHandlersIfNeeded();
53209
+ this.attachExitListener(proc);
53210
+ logger?.info("sshd: up", { port: this.deps.port, pid: proc.pid ?? null });
53211
+ return { port: this.deps.port };
53212
+ }
53213
+ async stop() {
53214
+ this.stopping = true;
53215
+ const proc = this.proc;
53216
+ this.proc = null;
53217
+ if (!proc) {
53218
+ clearSshdPid(this.deps.dataDir);
53219
+ return;
53220
+ }
53221
+ proc.kill("SIGTERM");
53222
+ await new Promise((resolve6) => {
53223
+ const t = setTimeout(() => {
53224
+ try {
53225
+ proc.kill("SIGKILL");
53226
+ } catch {
53227
+ }
53228
+ resolve6();
53229
+ }, 5e3);
53230
+ proc.once("exit", () => {
53231
+ clearTimeout(t);
53232
+ resolve6();
53233
+ });
53234
+ });
53235
+ clearSshdPid(this.deps.dataDir);
53236
+ }
53237
+ killSync() {
53238
+ const proc = this.proc;
53239
+ this.proc = null;
53240
+ clearSshdPid(this.deps.dataDir);
53241
+ if (!proc) return;
53242
+ try {
53243
+ proc.kill("SIGTERM");
53244
+ } catch {
53245
+ }
53246
+ }
53247
+ attachExitListener(proc) {
53248
+ proc.on("exit", (code) => {
53249
+ this.deps.logger?.warn("sshd exited", { code });
53250
+ if (this.stopping) return;
53251
+ this.proc = null;
53252
+ this.deps.onSshdExit?.({ code });
53253
+ });
53254
+ }
53255
+ installProcessExitHandlersIfNeeded() {
53256
+ if (this.exitHookInstalled) return;
53257
+ if (this.deps.installProcessExitHandlers !== true) return;
53258
+ this.exitHookInstalled = true;
53259
+ const sync = () => this.killSync();
53260
+ process.once("exit", sync);
53261
+ process.once("SIGHUP", sync);
53262
+ process.once("uncaughtException", sync);
53263
+ }
53264
+ async generateHostKey(hostKeyPath) {
53265
+ const keygenBin = this.deps.keygenBin ?? "/usr/bin/ssh-keygen";
53266
+ await new Promise((resolve6, reject) => {
53267
+ const p2 = (this.deps.spawnImpl ?? import_node_child_process11.spawn)(
53268
+ keygenBin,
53269
+ ["-t", "ed25519", "-f", hostKeyPath, "-N", "", "-q"],
53270
+ { stdio: "ignore" }
53271
+ );
53272
+ p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
53273
+ p2.on("error", reject);
53274
+ });
53275
+ try {
53276
+ import_node_fs36.default.chmodSync(hostKeyPath, 384);
53277
+ } catch {
53278
+ }
53279
+ }
53280
+ };
53281
+ async function waitForSshdReady(proc, timeoutMs) {
53282
+ return new Promise((resolve6) => {
53283
+ let settled = false;
53284
+ let buf = "";
53285
+ const finish = (r) => {
53286
+ if (settled) return;
53287
+ settled = true;
53288
+ cleanup();
53289
+ resolve6(r);
53290
+ };
53291
+ const onData = (chunk) => {
53292
+ buf += String(chunk);
53293
+ if (/Server listening on/i.test(buf)) finish({ ok: true });
53294
+ if (/fatal:/i.test(buf) || /error: Bind to port/i.test(buf)) {
53295
+ finish({ ok: false, error: "sshd startup failed", output: buf });
53296
+ }
53297
+ };
53298
+ const onExit = (code) => finish({ ok: false, error: `sshd exited before ready (code=${code})`, output: buf });
53299
+ const onErr = (err) => finish({ ok: false, error: `sshd spawn error: ${err.message}`, output: buf });
53300
+ const cleanup = () => {
53301
+ proc.stdout?.off("data", onData);
53302
+ proc.stderr?.off("data", onData);
53303
+ proc.off("exit", onExit);
53304
+ proc.off("error", onErr);
53305
+ clearTimeout(timer);
53306
+ };
53307
+ proc.stdout?.on("data", onData);
53308
+ proc.stderr?.on("data", onData);
53309
+ proc.on("exit", onExit);
53310
+ proc.on("error", onErr);
53311
+ const timer = setTimeout(
53312
+ () => finish({ ok: false, error: `sshd startup timeout after ${timeoutMs}ms`, output: buf }),
53313
+ timeoutMs
53314
+ );
53315
+ });
53316
+ }
53317
+
53318
+ // src/sshd/authorized-keys.ts
53319
+ var import_node_fs37 = __toESM(require("fs"), 1);
53320
+ var import_node_path38 = __toESM(require("path"), 1);
53321
+ var JAIL_BIN_PATH_ENV = "CLAWD_JAIL_BIN_PATH";
53322
+ var AUTHORIZED_KEYS_FILE = "clawd-contacts";
53323
+ function jailBinPath() {
53324
+ return process.env[JAIL_BIN_PATH_ENV] ?? import_node_path38.default.join(process.env.HOME ?? "", ".clawd", "bin", "clawd-ssh-jail");
53325
+ }
53326
+ function rebuildAuthorizedKeys(store, sshdDir) {
53327
+ const akDir = import_node_path38.default.join(sshdDir, "authorized_keys.d");
53328
+ const target = import_node_path38.default.join(akDir, AUTHORIZED_KEYS_FILE);
53329
+ import_node_fs37.default.mkdirSync(akDir, { recursive: true, mode: 448 });
53330
+ const lines = ["# managed by clawd; do not edit", ""];
53331
+ for (const c of store.list()) {
53332
+ if (!c.sshAllowed) continue;
53333
+ const safe = /^[A-Za-z0-9_.-]+$/.test(c.deviceId);
53334
+ if (!safe) continue;
53335
+ const pubkey = readIssuedPubkey(sshdDir, c.deviceId);
53336
+ if (!pubkey) continue;
53337
+ const bin = jailBinPath();
53338
+ lines.push(`command="${bin} ${c.deviceId}",restrict ${pubkey.trim()}`);
53339
+ lines.push(`# contact:${c.deviceId}`);
53340
+ }
53341
+ const body = lines.join("\n") + "\n";
53342
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
53343
+ import_node_fs37.default.writeFileSync(tmp, body, { mode: 384 });
53344
+ import_node_fs37.default.renameSync(tmp, target);
53345
+ }
53346
+ function readIssuedPubkey(sshdDir, deviceId) {
53347
+ const safeId = deviceId.replace(/[\/\\]/g, "_");
53348
+ const p2 = import_node_path38.default.join(sshdDir, "keys", `${safeId}.ed25519.pub`);
53349
+ try {
53350
+ return import_node_fs37.default.readFileSync(p2, "utf8");
53351
+ } catch {
53352
+ return null;
53353
+ }
53354
+ }
53355
+
53356
+ // src/sshd/contact-key-puller.ts
53357
+ var import_node_fs39 = __toESM(require("fs"), 1);
53358
+ var import_node_path40 = __toESM(require("path"), 1);
53359
+ init_peer_forward();
53360
+
53361
+ // src/sshd/contact-ssh-log.ts
53362
+ var import_node_fs38 = __toESM(require("fs"), 1);
53363
+ var import_node_path39 = __toESM(require("path"), 1);
53364
+ function createContactSshLog(dataDir) {
53365
+ const file = import_node_path39.default.join(dataDir, "contact-ssh.log");
53366
+ function append(level, tag, message, meta) {
53367
+ const time = (/* @__PURE__ */ new Date()).toISOString();
53368
+ let line = `[${time}] [${level}] [${tag}] ${message}`;
53369
+ if (meta && Object.keys(meta).length > 0) {
53370
+ try {
53371
+ line += " " + JSON.stringify(meta);
53372
+ } catch {
53373
+ line += " [meta-serialize-failed]";
53374
+ }
53375
+ }
53376
+ line += "\n";
53377
+ try {
53378
+ import_node_fs38.default.mkdirSync(import_node_path39.default.dirname(file), { recursive: true });
53379
+ import_node_fs38.default.appendFileSync(file, line, { mode: 384 });
53380
+ } catch {
53381
+ }
53382
+ }
53383
+ return {
53384
+ info: (tag, message, meta) => append("INFO", tag, message, meta),
53385
+ warn: (tag, message, meta) => append("WARN", tag, message, meta),
53386
+ error: (tag, message, meta) => append("ERROR", tag, message, meta)
53387
+ };
53388
+ }
53389
+ var nullContactSshLog = {
53390
+ info: () => {
53391
+ },
53392
+ warn: () => {
53393
+ },
53394
+ error: () => {
53395
+ }
53396
+ };
53397
+
53398
+ // src/sshd/contact-key-puller.ts
53399
+ var CONTACT_KEYS_DIR = "contact-ssh-keys";
53400
+ function safeContactKeyPath(dataDir, deviceId) {
53401
+ const safeId = deviceId.replace(/[\/\\]/g, "_");
53402
+ return import_node_path40.default.join(dataDir, CONTACT_KEYS_DIR, `${safeId}.ed25519`);
53403
+ }
53404
+ async function pullContactSshKeyOnce(deps) {
53405
+ const forward = deps.forwardImpl ?? ((c) => forwardContactSshKeyIssueToPeer({
53406
+ contact: { remoteUrl: c.remoteUrl, connectToken: c.connectToken },
53407
+ selfDeviceIdForRequest: c.deviceId
53408
+ }));
53409
+ const contacts = deps.store.list().filter((c) => c.connectToken.length > 0);
53410
+ const results = await Promise.all(
53411
+ contacts.map(async (c) => {
53412
+ try {
53413
+ const r = await forward(c);
53414
+ return { contact: c, result: r };
53415
+ } catch (err) {
53416
+ return {
53417
+ contact: c,
53418
+ result: {
53419
+ ok: false,
53420
+ code: "NETWORK",
53421
+ message: err instanceof Error ? err.message : String(err)
53422
+ }
53423
+ };
53424
+ }
53425
+ })
53426
+ );
53427
+ const errors = [];
53428
+ let pulled = 0;
53429
+ const sshLog = deps.sshLog ?? nullContactSshLog;
53430
+ for (const { contact, result } of results) {
53431
+ if (result.ok) {
53432
+ writeKeyFile(deps.dataDir, contact.deviceId, result.privateKeyPem);
53433
+ pulled++;
53434
+ deps.logger?.info("contact-key-puller: pulled", { deviceId: contact.deviceId });
53435
+ sshLog.info("key.pull.success", "B \u4FA7\u4ECE A \u62C9\u5230 privkey \u5E76\u843D\u76D8", {
53436
+ peerDeviceId: contact.deviceId,
53437
+ peerDisplayName: contact.displayName
53438
+ });
53439
+ } else if (result.code === "UNAUTHORIZED" || result.code === "FORBIDDEN") {
53440
+ const hadStale = removeKeyFile(deps.dataDir, contact.deviceId);
53441
+ sshLog.info("key.pull.rejected", "A \u4FA7\u672A\u6388\u6743\u6211 SSH\uFF08\u6B63\u5E38\u72B6\u6001\uFF1B\u8F6E\u8BE2\u7EE7\u7EED\uFF09", {
53442
+ peerDeviceId: contact.deviceId,
53443
+ peerDisplayName: contact.displayName,
53444
+ clearedStalePrivkey: hadStale
53445
+ });
53446
+ } else {
53447
+ errors.push({
53448
+ deviceId: contact.deviceId,
53449
+ code: result.code,
53450
+ message: result.message
53451
+ });
53452
+ deps.logger?.warn("contact-key-puller: pull failed", {
53453
+ deviceId: contact.deviceId,
53454
+ code: result.code,
53455
+ message: result.message
53456
+ });
53457
+ sshLog.warn("key.pull.error", "\u62C9 privkey \u65F6\u7F51\u7EDC/\u534F\u8BAE\u9519\u8BEF", {
53458
+ peerDeviceId: contact.deviceId,
53459
+ code: result.code,
53460
+ message: result.message
53461
+ });
53462
+ }
53463
+ }
53464
+ return { pulled, errors };
53465
+ }
53466
+ function writeKeyFile(dataDir, deviceId, pem) {
53467
+ const p2 = safeContactKeyPath(dataDir, deviceId);
53468
+ import_node_fs39.default.mkdirSync(import_node_path40.default.dirname(p2), { recursive: true, mode: 448 });
53469
+ import_node_fs39.default.writeFileSync(p2, pem, { mode: 384 });
53470
+ }
53471
+ function removeKeyFile(dataDir, deviceId) {
53472
+ try {
53473
+ import_node_fs39.default.unlinkSync(safeContactKeyPath(dataDir, deviceId));
53474
+ return true;
53475
+ } catch {
53476
+ return false;
53477
+ }
53478
+ }
53479
+ var ContactKeyPuller = class {
53480
+ constructor(deps) {
53481
+ this.deps = deps;
53482
+ }
53483
+ deps;
53484
+ timer = null;
53485
+ async start() {
53486
+ const interval = this.deps.intervalMs ?? 6e4;
53487
+ void this.tick();
53488
+ this.timer = setInterval(() => void this.tick(), interval);
53489
+ this.timer.unref();
53490
+ }
53491
+ stop() {
53492
+ if (this.timer) {
53493
+ clearInterval(this.timer);
53494
+ this.timer = null;
53495
+ }
53496
+ }
53497
+ async tick() {
53498
+ try {
53499
+ await pullContactSshKeyOnce(this.deps);
53500
+ } catch (err) {
53501
+ this.deps.logger?.warn("contact-key-puller: tick failed", {
53502
+ err: err instanceof Error ? err.message : String(err)
53503
+ });
53504
+ }
53505
+ }
53506
+ };
53507
+
53508
+ // src/sshd/ssh-tunnel-relay.ts
53509
+ var import_node_net2 = __toESM(require("net"), 1);
53510
+ async function handleSshTunnelUpgrade(req, socket, head, deps) {
53511
+ const sshLog = deps.sshLog ?? nullContactSshLog;
53512
+ const clientAddr = (req.socket && "remoteAddress" in req.socket ? req.socket.remoteAddress : null) ?? "unknown";
53513
+ const auth = req.headers.authorization ?? "";
53514
+ const m2 = /^Bearer\s+(.+)$/i.exec(auth);
53515
+ if (!m2) {
53516
+ sshLog.warn("tunnel.auth-failed", "/rpc/ssh-tunnel \u8BF7\u6C42\u7F3A Bearer token", {
53517
+ clientAddr
53518
+ });
53519
+ socket.write(
53520
+ "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n"
53521
+ );
53522
+ socket.destroy();
53523
+ return;
53524
+ }
53525
+ const token = m2[1].trim();
53526
+ let ok = false;
53527
+ try {
53528
+ ok = await deps.authorize(token);
53529
+ } catch (err) {
53530
+ deps.logger?.warn("ssh-tunnel: authorize threw", {
53531
+ err: err instanceof Error ? err.message : String(err)
53532
+ });
53533
+ }
53534
+ if (!ok) {
53535
+ sshLog.warn("tunnel.auth-failed", "/rpc/ssh-tunnel Bearer token \u9A8C\u8BC1\u5931\u8D25", {
53536
+ clientAddr
53537
+ });
53538
+ socket.write(
53539
+ "HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n"
53540
+ );
53541
+ socket.destroy();
53542
+ return;
53543
+ }
53544
+ sshLog.info("tunnel.auth-ok", "/rpc/ssh-tunnel \u8BA4\u8BC1\u901A\u8FC7\uFF0C\u5347\u7EA7 WS", {
53545
+ clientAddr
53546
+ });
53547
+ deps.wss.handleUpgrade(req, socket, head, (ws) => {
53548
+ pumpWsToSshd(ws, deps, clientAddr);
53549
+ });
53550
+ }
53551
+ function pumpWsToSshd(ws, deps, clientAddr) {
53552
+ const sshLog = deps.sshLog ?? nullContactSshLog;
53553
+ const tcp = import_node_net2.default.connect(deps.sshdPort, "127.0.0.1");
53554
+ let tcpConnected = false;
53555
+ let bytesFromWs = 0;
53556
+ let bytesToWs = 0;
53557
+ const startedAt = Date.now();
53558
+ const cleanup = (reason) => {
53559
+ try {
53560
+ ws.close(1e3, reason);
53561
+ } catch {
53562
+ }
53563
+ try {
53564
+ tcp.destroy();
53565
+ } catch {
53566
+ }
53567
+ sshLog.info("tunnel.disconnected", "SSH tunnel \u4F1A\u8BDD\u7ED3\u675F", {
53568
+ clientAddr,
53569
+ reason,
53570
+ bytesFromWs,
53571
+ bytesToWs,
53572
+ durationMs: Date.now() - startedAt
53573
+ });
53574
+ };
53575
+ tcp.once("connect", () => {
53576
+ tcpConnected = true;
53577
+ deps.logger?.info("ssh-tunnel: tcp connected to sshd", { port: deps.sshdPort });
53578
+ sshLog.info("tunnel.connected", "WS \u2194 \u672C\u673A sshd raw byte relay \u5DF2\u5C31\u7EEA", {
53579
+ clientAddr,
53580
+ sshdPort: deps.sshdPort
53581
+ });
53582
+ });
53583
+ tcp.on("data", (chunk) => {
53584
+ if (ws.readyState === ws.OPEN) {
53585
+ bytesToWs += chunk.length;
53586
+ ws.send(chunk, { binary: true });
53587
+ }
53588
+ });
53589
+ tcp.on("error", (err) => {
53590
+ deps.logger?.warn("ssh-tunnel: tcp error", { err: err.message, tcpConnected });
53591
+ sshLog.error(
53592
+ tcpConnected ? "tunnel.tcp-error" : "tunnel.tcp-connect-failed",
53593
+ tcpConnected ? "WS \u2194 sshd relay \u671F\u95F4 TCP \u4FA7\u51FA\u9519" : "\u65E0\u6CD5\u8FDE\u5230\u672C\u673A sshd\uFF08sshd \u672A\u8D77\uFF1F\u7AEF\u53E3\u9519\uFF1F\uFF09",
53594
+ { clientAddr, sshdPort: deps.sshdPort, err: err.message }
53595
+ );
53596
+ cleanup("tcp error");
53597
+ });
53598
+ tcp.on("end", () => cleanup("tcp end"));
53599
+ tcp.on("close", () => cleanup("tcp close"));
53600
+ ws.on("message", (data) => {
53601
+ let buf = null;
53602
+ if (Buffer.isBuffer(data)) buf = data;
53603
+ else if (Array.isArray(data)) buf = Buffer.concat(data);
53604
+ else if (data instanceof ArrayBuffer) buf = Buffer.from(data);
53605
+ if (buf) {
53606
+ bytesFromWs += buf.length;
53607
+ tcp.write(buf);
53608
+ }
53609
+ });
53610
+ ws.on("close", () => cleanup("ws close"));
53611
+ ws.on("error", (err) => {
53612
+ deps.logger?.warn("ssh-tunnel: ws error", { err: err.message });
53613
+ cleanup("ws error");
53614
+ });
53615
+ }
53616
+
52645
53617
  // src/tunnel/device-key.ts
52646
53618
  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);
53619
+ var import_node_path41 = __toESM(require("path"), 1);
53620
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
52649
53621
  var DERIVE_SALT = "clawd-tunnel-device-v1";
52650
53622
  function deriveStableDeviceKey(opts = {}) {
52651
53623
  const hostname = opts.hostname ?? import_node_os14.default.hostname();
52652
53624
  const uid = opts.uid ?? (typeof import_node_os14.default.userInfo === "function" ? import_node_os14.default.userInfo().uid : 0);
52653
53625
  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;
53626
+ const defaultDataDir = import_node_path41.default.resolve(import_node_path41.default.join(home, ".clawd"));
53627
+ const normalizedDataDir = opts.dataDir ? import_node_path41.default.resolve(opts.dataDir) : null;
52656
53628
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
52657
53629
  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);
53630
+ return import_node_crypto10.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
52659
53631
  }
52660
53632
 
52661
53633
  // 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);
53634
+ var import_node_fs40 = __toESM(require("fs"), 1);
53635
+ var import_node_path42 = __toESM(require("path"), 1);
53636
+ var import_node_crypto11 = __toESM(require("crypto"), 1);
52665
53637
  var AUTH_FILE_NAME = "auth.json";
52666
53638
  function authFilePath(dataDir) {
52667
- return import_node_path36.default.join(dataDir, AUTH_FILE_NAME);
53639
+ return import_node_path42.default.join(dataDir, AUTH_FILE_NAME);
52668
53640
  }
52669
53641
  function loadOrCreateAuthFile(opts) {
52670
53642
  const file = authFilePath(opts.dataDir);
@@ -52693,14 +53665,14 @@ function loadOrCreateAuthFile(opts) {
52693
53665
  return next;
52694
53666
  }
52695
53667
  function defaultGenerateToken() {
52696
- return import_node_crypto12.default.randomBytes(32).toString("base64url");
53668
+ return import_node_crypto11.default.randomBytes(32).toString("base64url");
52697
53669
  }
52698
53670
  function defaultGenerateOwnerPrincipalId() {
52699
- return `owner-${import_node_crypto12.default.randomUUID()}`;
53671
+ return `owner-${import_node_crypto11.default.randomUUID()}`;
52700
53672
  }
52701
53673
  function readAuthFile(file) {
52702
53674
  try {
52703
- const raw = import_node_fs34.default.readFileSync(file, "utf8");
53675
+ const raw = import_node_fs40.default.readFileSync(file, "utf8");
52704
53676
  const parsed = JSON.parse(raw);
52705
53677
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
52706
53678
  return null;
@@ -52720,25 +53692,25 @@ function readAuthFile(file) {
52720
53692
  }
52721
53693
  }
52722
53694
  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 });
53695
+ import_node_fs40.default.mkdirSync(import_node_path42.default.dirname(file), { recursive: true });
53696
+ import_node_fs40.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
52725
53697
  try {
52726
- import_node_fs34.default.chmodSync(file, 384);
53698
+ import_node_fs40.default.chmodSync(file, 384);
52727
53699
  } catch {
52728
53700
  }
52729
53701
  }
52730
53702
 
52731
53703
  // src/owner-profile.ts
52732
- var import_node_fs35 = __toESM(require("fs"), 1);
53704
+ var import_node_fs41 = __toESM(require("fs"), 1);
52733
53705
  var import_node_os15 = __toESM(require("os"), 1);
52734
- var import_node_path37 = __toESM(require("path"), 1);
53706
+ var import_node_path43 = __toESM(require("path"), 1);
52735
53707
  var PROFILE_FILENAME = "profile.json";
52736
53708
  function loadOwnerDisplayName(dataDir) {
52737
53709
  const fallback = import_node_os15.default.userInfo().username;
52738
- const profilePath = import_node_path37.default.join(dataDir, PROFILE_FILENAME);
53710
+ const profilePath = import_node_path43.default.join(dataDir, PROFILE_FILENAME);
52739
53711
  let raw;
52740
53712
  try {
52741
- raw = import_node_fs35.default.readFileSync(profilePath, "utf8");
53713
+ raw = import_node_fs41.default.readFileSync(profilePath, "utf8");
52742
53714
  } catch {
52743
53715
  return fallback;
52744
53716
  }
@@ -52761,18 +53733,18 @@ function loadOwnerDisplayName(dataDir) {
52761
53733
  }
52762
53734
 
52763
53735
  // 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);
53736
+ var import_node_fs42 = __toESM(require("fs"), 1);
53737
+ var import_node_path44 = __toESM(require("path"), 1);
52766
53738
  var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
52767
53739
  var OwnerIdentityStore = class {
52768
53740
  file;
52769
53741
  constructor(dataDir) {
52770
- this.file = import_node_path38.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
53742
+ this.file = import_node_path44.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
52771
53743
  }
52772
53744
  read() {
52773
53745
  let raw;
52774
53746
  try {
52775
- raw = import_node_fs36.default.readFileSync(this.file, "utf8");
53747
+ raw = import_node_fs42.default.readFileSync(this.file, "utf8");
52776
53748
  } catch {
52777
53749
  return null;
52778
53750
  }
@@ -52800,16 +53772,16 @@ var OwnerIdentityStore = class {
52800
53772
  };
52801
53773
  }
52802
53774
  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 });
53775
+ import_node_fs42.default.mkdirSync(import_node_path44.default.dirname(this.file), { recursive: true });
53776
+ import_node_fs42.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
52805
53777
  try {
52806
- import_node_fs36.default.chmodSync(this.file, 384);
53778
+ import_node_fs42.default.chmodSync(this.file, 384);
52807
53779
  } catch {
52808
53780
  }
52809
53781
  }
52810
53782
  clear() {
52811
53783
  try {
52812
- import_node_fs36.default.unlinkSync(this.file);
53784
+ import_node_fs42.default.unlinkSync(this.file);
52813
53785
  } catch (err) {
52814
53786
  const code = err?.code;
52815
53787
  if (code !== "ENOENT") throw err;
@@ -52818,7 +53790,7 @@ var OwnerIdentityStore = class {
52818
53790
  };
52819
53791
 
52820
53792
  // src/feishu-auth/login-flow.ts
52821
- var import_node_crypto13 = __toESM(require("crypto"), 1);
53793
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
52822
53794
  var STATE_TTL_MS = 5 * 60 * 1e3;
52823
53795
  var LoginFlow = class {
52824
53796
  constructor(deps) {
@@ -52827,7 +53799,7 @@ var LoginFlow = class {
52827
53799
  deps;
52828
53800
  pendingStates = /* @__PURE__ */ new Map();
52829
53801
  start() {
52830
- const state = import_node_crypto13.default.randomBytes(16).toString("base64url");
53802
+ const state = import_node_crypto12.default.randomBytes(16).toString("base64url");
52831
53803
  const now = (this.deps.now ?? Date.now)();
52832
53804
  this.pendingStates.set(state, now);
52833
53805
  this.gcExpired(now);
@@ -52930,9 +53902,9 @@ var CentralClientError = class extends Error {
52930
53902
  code;
52931
53903
  cause;
52932
53904
  };
52933
- async function centralRequest(opts, path68, init) {
53905
+ async function centralRequest(opts, path76, init) {
52934
53906
  const f = opts.fetchImpl ?? globalThis.fetch;
52935
- const url = `${opts.api.replace(/\/+$/, "")}${path68}`;
53907
+ const url = `${opts.api.replace(/\/+$/, "")}${path76}`;
52936
53908
  const ctrl = new AbortController();
52937
53909
  const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
52938
53910
  let res;
@@ -53074,8 +54046,8 @@ function verifyConnectToken(args) {
53074
54046
  }
53075
54047
 
53076
54048
  // src/feishu-auth/server-key.ts
53077
- var fs46 = __toESM(require("fs"), 1);
53078
- var path47 = __toESM(require("path"), 1);
54049
+ var fs52 = __toESM(require("fs"), 1);
54050
+ var path53 = __toESM(require("path"), 1);
53079
54051
  var FILE_NAME2 = "server-signing-key.json";
53080
54052
  var ServerKeyStore = class {
53081
54053
  constructor(dataDir) {
@@ -53083,12 +54055,12 @@ var ServerKeyStore = class {
53083
54055
  }
53084
54056
  dataDir;
53085
54057
  filePath() {
53086
- return path47.join(this.dataDir, FILE_NAME2);
54058
+ return path53.join(this.dataDir, FILE_NAME2);
53087
54059
  }
53088
54060
  /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
53089
54061
  read() {
53090
54062
  try {
53091
- const raw = fs46.readFileSync(this.filePath(), "utf8");
54063
+ const raw = fs52.readFileSync(this.filePath(), "utf8");
53092
54064
  const parsed = JSON.parse(raw);
53093
54065
  if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
53094
54066
  return parsed.publicKeyPem;
@@ -53103,12 +54075,12 @@ var ServerKeyStore = class {
53103
54075
  publicKeyPem,
53104
54076
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
53105
54077
  };
53106
- fs46.mkdirSync(this.dataDir, { recursive: true });
53107
- fs46.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
54078
+ fs52.mkdirSync(this.dataDir, { recursive: true });
54079
+ fs52.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
53108
54080
  }
53109
54081
  clear() {
53110
54082
  try {
53111
- fs46.unlinkSync(this.filePath());
54083
+ fs52.unlinkSync(this.filePath());
53112
54084
  } catch {
53113
54085
  }
53114
54086
  }
@@ -53121,12 +54093,12 @@ init_protocol();
53121
54093
  init_protocol();
53122
54094
 
53123
54095
  // src/session/fork.ts
53124
- var import_node_fs37 = __toESM(require("fs"), 1);
54096
+ var import_node_fs43 = __toESM(require("fs"), 1);
53125
54097
  var import_node_os16 = __toESM(require("os"), 1);
53126
- var import_node_path39 = __toESM(require("path"), 1);
54098
+ var import_node_path45 = __toESM(require("path"), 1);
53127
54099
  init_claude_history();
53128
54100
  function readJsonlEntries(file) {
53129
- const raw = import_node_fs37.default.readFileSync(file, "utf8");
54101
+ const raw = import_node_fs43.default.readFileSync(file, "utf8");
53130
54102
  const out = [];
53131
54103
  for (const line of raw.split("\n")) {
53132
54104
  const t = line.trim();
@@ -53139,10 +54111,10 @@ function readJsonlEntries(file) {
53139
54111
  return out;
53140
54112
  }
53141
54113
  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)) {
54114
+ const baseDir = input.baseDir ?? import_node_path45.default.join(import_node_os16.default.homedir(), ".claude");
54115
+ const projectDir = import_node_path45.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
54116
+ const sourceFile = import_node_path45.default.join(projectDir, `${input.toolSessionId}.jsonl`);
54117
+ if (!import_node_fs43.default.existsSync(sourceFile)) {
53146
54118
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
53147
54119
  }
53148
54120
  const entries = readJsonlEntries(sourceFile);
@@ -53172,9 +54144,9 @@ function forkSession(input) {
53172
54144
  }
53173
54145
  forkedLines.push(JSON.stringify(forked));
53174
54146
  }
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 });
54147
+ const forkedFilePath = import_node_path45.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
54148
+ import_node_fs43.default.mkdirSync(projectDir, { recursive: true });
54149
+ import_node_fs43.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
53178
54150
  return { forkedToolSessionId, forkedFilePath };
53179
54151
  }
53180
54152
 
@@ -53526,7 +54498,7 @@ function buildPermissionHandlers(deps) {
53526
54498
  }
53527
54499
 
53528
54500
  // src/handlers/history.ts
53529
- var path50 = __toESM(require("path"), 1);
54501
+ var path56 = __toESM(require("path"), 1);
53530
54502
  init_protocol();
53531
54503
 
53532
54504
  // src/session/recent-dirs.ts
@@ -53544,7 +54516,7 @@ function listRecentDirs(store, limit = 50) {
53544
54516
  }
53545
54517
 
53546
54518
  // src/permission/persona-paths.ts
53547
- var path49 = __toESM(require("path"), 1);
54519
+ var path55 = __toESM(require("path"), 1);
53548
54520
  function getAllowedPersonaIds(grants, action) {
53549
54521
  const ids = /* @__PURE__ */ new Set();
53550
54522
  for (const g2 of grants) {
@@ -53557,42 +54529,42 @@ function getAllowedPersonaIds(grants, action) {
53557
54529
  return ids;
53558
54530
  }
53559
54531
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read", userWorkDir) {
53560
- const target = path49.resolve(absPath);
54532
+ const target = path55.resolve(absPath);
53561
54533
  if (userWorkDir) {
53562
- const u = path49.resolve(userWorkDir);
53563
- const usep = u.endsWith(path49.sep) ? "" : path49.sep;
54534
+ const u = path55.resolve(userWorkDir);
54535
+ const usep = u.endsWith(path55.sep) ? "" : path55.sep;
53564
54536
  if (target === u || target.startsWith(u + usep)) return true;
53565
54537
  }
53566
- const root = path49.resolve(personaRoot);
53567
- const sep3 = root.endsWith(path49.sep) ? "" : path49.sep;
54538
+ const root = path55.resolve(personaRoot);
54539
+ const sep3 = root.endsWith(path55.sep) ? "" : path55.sep;
53568
54540
  if (!target.startsWith(root + sep3)) return false;
53569
- const rel = path49.relative(root, target);
54541
+ const rel = path55.relative(root, target);
53570
54542
  if (!rel || rel.startsWith("..")) return false;
53571
- const personaId = rel.split(path49.sep)[0];
54543
+ const personaId = rel.split(path55.sep)[0];
53572
54544
  if (!personaId) return false;
53573
54545
  const allowed = getAllowedPersonaIds(grants, action);
53574
54546
  if (allowed === "*") return true;
53575
54547
  return allowed.has(personaId);
53576
54548
  }
53577
54549
  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;
54550
+ const root = path55.resolve(personaRoot);
54551
+ const target = path55.resolve(absPath);
54552
+ const sep3 = root.endsWith(path55.sep) ? "" : path55.sep;
53581
54553
  if (!target.startsWith(root + sep3)) return null;
53582
- const rel = path49.relative(root, target);
54554
+ const rel = path55.relative(root, target);
53583
54555
  if (!rel || rel.startsWith("..")) return null;
53584
- const id = rel.split(path49.sep)[0];
54556
+ const id = rel.split(path55.sep)[0];
53585
54557
  return id || null;
53586
54558
  }
53587
54559
  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;
54560
+ const d = path55.resolve(dir);
54561
+ const t = path55.resolve(absPath);
54562
+ const sep3 = d.endsWith(path55.sep) ? "" : path55.sep;
53591
54563
  return t === d || t.startsWith(d + sep3);
53592
54564
  }
53593
54565
  function isPathInGuestBoundary(personaRoot, personaId, userWorkDir, absPath) {
53594
54566
  if (userWorkDir && isPathWithin(userWorkDir, absPath)) return true;
53595
- return personaIdFromPath(path49.resolve(absPath), personaRoot) === personaId;
54567
+ return personaIdFromPath(path55.resolve(absPath), personaRoot) === personaId;
53596
54568
  }
53597
54569
 
53598
54570
  // src/handlers/history.ts
@@ -53618,7 +54590,7 @@ function buildHistoryHandlers(deps) {
53618
54590
  if (!pid) return false;
53619
54591
  return isGuestPathAllowed(
53620
54592
  ctx.grants,
53621
- path50.join(personaRoot, pid),
54593
+ path56.join(personaRoot, pid),
53622
54594
  personaRoot,
53623
54595
  "read",
53624
54596
  userWorkDir
@@ -53630,7 +54602,7 @@ function buildHistoryHandlers(deps) {
53630
54602
  };
53631
54603
  const list = async (frame, _client, ctx) => {
53632
54604
  const args = HistoryListArgs.parse(frame);
53633
- assertGuestPath(ctx, path50.resolve(args.projectPath), personaRoot, "history:list");
54605
+ assertGuestPath(ctx, path56.resolve(args.projectPath), personaRoot, "history:list");
53634
54606
  const sessions = await history.listSessions(args);
53635
54607
  return { response: { type: "history:list", sessions } };
53636
54608
  };
@@ -53662,13 +54634,13 @@ function buildHistoryHandlers(deps) {
53662
54634
  };
53663
54635
  const subagents = async (frame, _client, ctx) => {
53664
54636
  const args = HistorySubagentsArgs.parse(frame);
53665
- assertGuestPath(ctx, path50.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
54637
+ assertGuestPath(ctx, path56.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
53666
54638
  const subs = await history.listSubagents(args);
53667
54639
  return { response: { type: "history:subagents", subagents: subs } };
53668
54640
  };
53669
54641
  const subagentRead = async (frame, _client, ctx) => {
53670
54642
  const args = HistorySubagentReadArgs.parse(frame);
53671
- assertGuestPath(ctx, path50.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
54643
+ assertGuestPath(ctx, path56.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
53672
54644
  const res = await history.readSubagent(args);
53673
54645
  return { response: { type: "history:subagent-read", ...res } };
53674
54646
  };
@@ -53677,7 +54649,7 @@ function buildHistoryHandlers(deps) {
53677
54649
  if (ctx?.principal.kind === "guest" && personaRoot) {
53678
54650
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
53679
54651
  const filtered = dirs.filter(
53680
- (d) => isGuestPathAllowed(ctx.grants, path50.resolve(d.cwd), personaRoot, "read", userWorkDir)
54652
+ (d) => isGuestPathAllowed(ctx.grants, path56.resolve(d.cwd), personaRoot, "read", userWorkDir)
53681
54653
  );
53682
54654
  return { response: { type: "history:recentDirs", dirs: filtered } };
53683
54655
  }
@@ -53694,7 +54666,7 @@ function buildHistoryHandlers(deps) {
53694
54666
  }
53695
54667
 
53696
54668
  // src/handlers/workspace.ts
53697
- var path51 = __toESM(require("path"), 1);
54669
+ var path57 = __toESM(require("path"), 1);
53698
54670
  var os16 = __toESM(require("os"), 1);
53699
54671
  init_protocol();
53700
54672
  init_protocol();
@@ -53736,22 +54708,22 @@ function buildWorkspaceHandlers(deps) {
53736
54708
  const args = WorkspaceListArgs.parse(frame);
53737
54709
  const isGuest = ctx?.principal.kind === "guest";
53738
54710
  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;
54711
+ const resolvedCwd = path57.resolve(args.cwd ?? fallbackCwd);
54712
+ const target = args.path ? path57.resolve(resolvedCwd, args.path) : resolvedCwd;
53741
54713
  assertGuestPath2(ctx, target, personaRoot, "workspace:list", usersRoot);
53742
54714
  const res = workspace.list({ ...args, cwd: resolvedCwd });
53743
54715
  return { response: { type: "workspace:list", ...res } };
53744
54716
  };
53745
54717
  const read = async (frame, _client, ctx) => {
53746
54718
  const args = WorkspaceReadArgs.parse(frame);
53747
- const target = path51.isAbsolute(args.path) ? path51.resolve(args.path) : path51.resolve(args.cwd, args.path);
54719
+ const target = path57.isAbsolute(args.path) ? path57.resolve(args.path) : path57.resolve(args.cwd, args.path);
53748
54720
  assertGuestPath2(ctx, target, personaRoot, "workspace:read", usersRoot);
53749
54721
  const res = workspace.read(args);
53750
54722
  return { response: { type: "workspace:read", ...res } };
53751
54723
  };
53752
54724
  const skillsList = async (frame, _client, ctx) => {
53753
54725
  const args = SkillsListArgs.parse(frame);
53754
- const cwdAbs = path51.resolve(args.cwd);
54726
+ const cwdAbs = path57.resolve(args.cwd);
53755
54727
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list", usersRoot);
53756
54728
  const list2 = await getSkillsForTool(args.tool ?? "claude", cwdAbs);
53757
54729
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -53763,7 +54735,7 @@ function buildWorkspaceHandlers(deps) {
53763
54735
  };
53764
54736
  const agentsList = async (frame, _client, ctx) => {
53765
54737
  const args = AgentsListArgs.parse(frame);
53766
- const cwdAbs = path51.resolve(args.cwd);
54738
+ const cwdAbs = path57.resolve(args.cwd);
53767
54739
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list", usersRoot);
53768
54740
  if (args.tool === "codex") {
53769
54741
  return { response: { type: "agents:list", agents: [] } };
@@ -53785,20 +54757,20 @@ function buildWorkspaceHandlers(deps) {
53785
54757
  }
53786
54758
 
53787
54759
  // src/handlers/git.ts
53788
- var path53 = __toESM(require("path"), 1);
54760
+ var path59 = __toESM(require("path"), 1);
53789
54761
  init_protocol();
53790
54762
  init_protocol();
53791
54763
 
53792
54764
  // 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);
54765
+ var import_node_child_process12 = require("child_process");
54766
+ var import_node_fs44 = __toESM(require("fs"), 1);
54767
+ var import_node_path46 = __toESM(require("path"), 1);
53796
54768
  var import_node_util = require("util");
53797
- var pexec = (0, import_node_util.promisify)(import_node_child_process10.execFile);
54769
+ var pexec = (0, import_node_util.promisify)(import_node_child_process12.execFile);
53798
54770
  function normalizePath(p2) {
53799
- const resolved = import_node_path40.default.resolve(p2);
54771
+ const resolved = import_node_path46.default.resolve(p2);
53800
54772
  try {
53801
- return import_node_fs38.default.realpathSync(resolved);
54773
+ return import_node_fs44.default.realpathSync(resolved);
53802
54774
  } catch {
53803
54775
  return resolved;
53804
54776
  }
@@ -53872,7 +54844,7 @@ async function listGitBranches(cwd) {
53872
54844
  function assertGuestCwd(ctx, cwd, personaRoot, method, usersRoot) {
53873
54845
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
53874
54846
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
53875
- if (!isGuestPathAllowed(ctx.grants, path53.resolve(cwd), personaRoot, "read", userWorkDir)) {
54847
+ if (!isGuestPathAllowed(ctx.grants, path59.resolve(cwd), personaRoot, "read", userWorkDir)) {
53876
54848
  throw new ClawdError(
53877
54849
  ERROR_CODES.UNAUTHORIZED,
53878
54850
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -53922,6 +54894,7 @@ function buildCapabilitiesHandlers(deps) {
53922
54894
 
53923
54895
  // src/handlers/capability.ts
53924
54896
  init_zod();
54897
+ init_src();
53925
54898
  init_protocol();
53926
54899
  var DeleteArgsSchema = external_exports.object({
53927
54900
  capabilityId: external_exports.string().min(1)
@@ -53960,6 +54933,7 @@ function buildCapabilityHandlers(deps) {
53960
54933
  }
53961
54934
 
53962
54935
  // src/handlers/inbox.ts
54936
+ init_src();
53963
54937
  init_protocol();
53964
54938
  function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
53965
54939
  if (ctx.principal.kind === "owner") {
@@ -54138,6 +55112,7 @@ function buildInboxHandlers(deps) {
54138
55112
  }
54139
55113
 
54140
55114
  // src/handlers/contact.ts
55115
+ init_src();
54141
55116
  init_protocol();
54142
55117
  function ensureOwner(ctx) {
54143
55118
  if (!ctx || ctx.principal.kind !== "owner") {
@@ -54205,7 +55180,157 @@ function buildContactHandlers(deps) {
54205
55180
  };
54206
55181
  }
54207
55182
 
55183
+ // src/handlers/contact-ssh.ts
55184
+ init_src();
55185
+ init_protocol();
55186
+
55187
+ // src/sshd/key-issue.ts
55188
+ var import_node_fs45 = __toESM(require("fs"), 1);
55189
+ var import_node_path47 = __toESM(require("path"), 1);
55190
+ var import_node_child_process13 = require("child_process");
55191
+ function safeDeviceId(deviceId) {
55192
+ return deviceId.replace(/[\/\\]/g, "_");
55193
+ }
55194
+ async function issueContactSshKey(deviceId, sshdDir, opts = {}) {
55195
+ const safeId = safeDeviceId(deviceId);
55196
+ const keysDir = import_node_path47.default.join(sshdDir, "keys");
55197
+ import_node_fs45.default.mkdirSync(keysDir, { recursive: true, mode: 448 });
55198
+ const privPath = import_node_path47.default.join(keysDir, `${safeId}.ed25519`);
55199
+ const pubPath = `${privPath}.pub`;
55200
+ if (import_node_fs45.default.existsSync(privPath) && import_node_fs45.default.existsSync(pubPath)) {
55201
+ return {
55202
+ privateKeyPem: import_node_fs45.default.readFileSync(privPath, "utf8"),
55203
+ publicKeyLine: import_node_fs45.default.readFileSync(pubPath, "utf8").trim()
55204
+ };
55205
+ }
55206
+ const bin = opts.keygenBin ?? "/usr/bin/ssh-keygen";
55207
+ await new Promise((resolve6, reject) => {
55208
+ const p2 = (opts.spawnImpl ?? import_node_child_process13.spawn)(
55209
+ bin,
55210
+ ["-t", "ed25519", "-f", privPath, "-N", "", "-q", "-C", `clawd-contact-${safeId}`],
55211
+ { stdio: "ignore" }
55212
+ );
55213
+ p2.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`ssh-keygen exit ${code}`)));
55214
+ p2.on("error", reject);
55215
+ });
55216
+ try {
55217
+ import_node_fs45.default.chmodSync(privPath, 384);
55218
+ } catch {
55219
+ }
55220
+ try {
55221
+ import_node_fs45.default.chmodSync(pubPath, 420);
55222
+ } catch {
55223
+ }
55224
+ return {
55225
+ privateKeyPem: import_node_fs45.default.readFileSync(privPath, "utf8"),
55226
+ publicKeyLine: import_node_fs45.default.readFileSync(pubPath, "utf8").trim()
55227
+ };
55228
+ }
55229
+
55230
+ // src/handlers/contact-ssh.ts
55231
+ function ensureOwner2(ctx) {
55232
+ if (!ctx || ctx.principal.kind !== "owner") {
55233
+ throw new ClawdError(
55234
+ ERROR_CODES.UNAUTHORIZED,
55235
+ "UNAUTHORIZED: contact:setSshAccess requires owner ctx"
55236
+ );
55237
+ }
55238
+ }
55239
+ function ensureGuest(ctx) {
55240
+ if (!ctx || ctx.principal.kind !== "guest") {
55241
+ throw new ClawdError(
55242
+ ERROR_CODES.UNAUTHORIZED,
55243
+ "UNAUTHORIZED: contact:sshKey:issue requires guest ctx"
55244
+ );
55245
+ }
55246
+ return ctx.principal.id;
55247
+ }
55248
+ function buildContactSshHandlers(deps) {
55249
+ const sshLog = deps.sshLog ?? nullContactSshLog;
55250
+ const setSshAccess = async (frame, _client, ctx) => {
55251
+ ensureOwner2(ctx);
55252
+ const { type: _t, requestId: _r, ...rest } = frame;
55253
+ const args = ContactSetSshAccessArgsSchema.parse(rest);
55254
+ const hit = deps.store.setSshAccess(args.deviceId, {
55255
+ sshAllowed: args.sshAllowed,
55256
+ exposedDirs: args.exposedDirs
55257
+ });
55258
+ if (!hit) {
55259
+ sshLog.warn("authz.setSshAccess", "owner \u5C1D\u8BD5\u6539 SSH \u6388\u6743\u4F46 contact \u4E0D\u5728 store", {
55260
+ peerDeviceId: args.deviceId
55261
+ });
55262
+ throw new ClawdError(
55263
+ ERROR_CODES.CONTACT_NOT_FOUND,
55264
+ `CONTACT_NOT_FOUND: contact ${args.deviceId} not in store`
55265
+ );
55266
+ }
55267
+ rebuildAuthorizedKeys(deps.store, deps.sshdDir);
55268
+ sshLog.info("authz.setSshAccess", "owner \u6539\u4E86 SSH \u6388\u6743 \u2192 authorized_keys \u5DF2\u91CD\u5EFA", {
55269
+ peerDeviceId: args.deviceId,
55270
+ sshAllowed: args.sshAllowed,
55271
+ exposedDirs: args.exposedDirs
55272
+ });
55273
+ deps.broadcast({
55274
+ type: "contact:ssh-access-updated",
55275
+ deviceId: args.deviceId,
55276
+ sshAllowed: args.sshAllowed,
55277
+ exposedDirs: args.exposedDirs
55278
+ });
55279
+ return {
55280
+ response: {
55281
+ type: "contact:setSshAccess:ok",
55282
+ deviceId: args.deviceId,
55283
+ sshAllowed: args.sshAllowed,
55284
+ exposedDirs: args.exposedDirs
55285
+ }
55286
+ };
55287
+ };
55288
+ const sshKeyIssue = async (frame, _client, ctx) => {
55289
+ const callerDeviceId = ensureGuest(ctx);
55290
+ const { type: _t, requestId: _r, ...rest } = frame;
55291
+ ContactSshKeyIssueArgsSchema.parse(rest);
55292
+ const contact = deps.store.get(callerDeviceId);
55293
+ if (!contact || !contact.sshAllowed) {
55294
+ sshLog.info(
55295
+ "key.issue.rejected",
55296
+ contact ? "guest \u8BF7\u6C42 SSH key \u4F46 owner \u672A\u6388\u6743" : "guest \u8BF7\u6C42 SSH key \u4F46\u4E0D\u5728\u6211\u7684 contactStore",
55297
+ {
55298
+ peerDeviceId: callerDeviceId,
55299
+ peerDisplayName: contact?.displayName
55300
+ }
55301
+ );
55302
+ throw new ClawdError(
55303
+ ERROR_CODES.UNAUTHORIZED,
55304
+ `UNAUTHORIZED: contact ${callerDeviceId} not authorized for SSH`
55305
+ );
55306
+ }
55307
+ const { privateKeyPem, publicKeyLine } = await issueContactSshKey(callerDeviceId, deps.sshdDir);
55308
+ rebuildAuthorizedKeys(deps.store, deps.sshdDir);
55309
+ sshLog.info(
55310
+ "key.issue.success",
55311
+ "A \u4FA7\u53D1 privkey \u7ED9 guest\uFF08\u5E42\u7B49\uFF09\uFF0Cauthorized_keys \u5DF2\u91CD\u5EFA",
55312
+ {
55313
+ peerDeviceId: callerDeviceId,
55314
+ peerDisplayName: contact.displayName,
55315
+ publicKeyFingerprint: publicKeyLine.slice(0, 32) + "..."
55316
+ }
55317
+ );
55318
+ return {
55319
+ response: {
55320
+ type: "contact:sshKey:issue:ok",
55321
+ privateKeyPem,
55322
+ publicKeyLine
55323
+ }
55324
+ };
55325
+ };
55326
+ return {
55327
+ "contact:setSshAccess": setSshAccess,
55328
+ "contact:sshKey:issue": sshKeyIssue
55329
+ };
55330
+ }
55331
+
54208
55332
  // src/handlers/whoami.ts
55333
+ init_src();
54209
55334
  init_protocol();
54210
55335
  function buildWhoamiHandler(deps) {
54211
55336
  return async (_frame, _client, ctx) => {
@@ -54299,8 +55424,9 @@ function buildFeishuAuthHandlers(deps) {
54299
55424
  }
54300
55425
 
54301
55426
  // src/handlers/device.ts
55427
+ init_src();
54302
55428
  init_protocol();
54303
- function ensureOwner2(ctx) {
55429
+ function ensureOwner3(ctx) {
54304
55430
  if (!ctx || ctx.principal.kind !== "owner") {
54305
55431
  throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
54306
55432
  }
@@ -54308,14 +55434,14 @@ function ensureOwner2(ctx) {
54308
55434
  function buildDeviceHandlers(deps) {
54309
55435
  const now = deps.now ?? Date.now;
54310
55436
  const list = async (_frame, _client, ctx) => {
54311
- ensureOwner2(ctx);
55437
+ ensureOwner3(ctx);
54312
55438
  const devices = await deps.listDevices();
54313
55439
  return {
54314
55440
  response: { type: "device:list:ok", devices }
54315
55441
  };
54316
55442
  };
54317
55443
  const connect = async (frame, _client, ctx) => {
54318
- ensureOwner2(ctx);
55444
+ ensureOwner3(ctx);
54319
55445
  const { type: _t, requestId: _r, ...rest } = frame;
54320
55446
  const args = DeviceConnectArgsSchema.parse(rest);
54321
55447
  const exchanged = await deps.exchange(args.deviceId);
@@ -54358,7 +55484,9 @@ function buildDeviceHandlers(deps) {
54358
55484
  connectToken: exchanged.token,
54359
55485
  grants: wh.grants,
54360
55486
  addedAt: now(),
54361
- pinnedAt: null
55487
+ pinnedAt: null,
55488
+ sshAllowed: false,
55489
+ exposedDirs: []
54362
55490
  };
54363
55491
  deps.store.upsert(contact);
54364
55492
  deps.broadcast({ type: "contact:added", contact });
@@ -54514,7 +55642,7 @@ function buildPersonaHandlers(deps) {
54514
55642
  }
54515
55643
 
54516
55644
  // src/handlers/attachment.ts
54517
- var import_node_path41 = __toESM(require("path"), 1);
55645
+ var import_node_path48 = __toESM(require("path"), 1);
54518
55646
  init_protocol();
54519
55647
  init_protocol();
54520
55648
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -54594,12 +55722,12 @@ function buildAttachmentHandlers(deps) {
54594
55722
  `session ${args.sessionId} scope unresolved`
54595
55723
  );
54596
55724
  }
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);
55725
+ const cwdAbs = import_node_path48.default.resolve(sessionFile.cwd);
55726
+ const candidateAbs = import_node_path48.default.isAbsolute(args.relPath) ? import_node_path48.default.resolve(args.relPath) : import_node_path48.default.resolve(cwdAbs, args.relPath);
54599
55727
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.signUrl", "group-acl");
54600
55728
  const entries = deps.groupFileStore.list(scope, args.sessionId);
54601
55729
  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);
55730
+ const storedAbs = import_node_path48.default.isAbsolute(e.relPath) ? import_node_path48.default.resolve(e.relPath) : import_node_path48.default.resolve(cwdAbs, e.relPath);
54603
55731
  return storedAbs === candidateAbs && !e.stale;
54604
55732
  });
54605
55733
  if (!entry) {
@@ -54624,7 +55752,7 @@ function buildAttachmentHandlers(deps) {
54624
55752
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
54625
55753
  const f = deps.sessionStore.read(sessionId);
54626
55754
  if (!f) return;
54627
- assertGuestAttachmentPath(ctx, import_node_path41.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
55755
+ assertGuestAttachmentPath(ctx, import_node_path48.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
54628
55756
  }
54629
55757
  const groupAdd = async (frame, _client, ctx) => {
54630
55758
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -54639,8 +55767,8 @@ function buildAttachmentHandlers(deps) {
54639
55767
  if (!scope) {
54640
55768
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
54641
55769
  }
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);
55770
+ const cwdAbs = import_node_path48.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
55771
+ const candidateAbs = import_node_path48.default.isAbsolute(args.relPath) ? import_node_path48.default.resolve(args.relPath) : import_node_path48.default.resolve(cwdAbs, args.relPath);
54644
55772
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.groupAdd", "cwd-subtree");
54645
55773
  const from = ctx?.principal.kind === "owner" ? "owner" : "agent";
54646
55774
  const size = 0;
@@ -54699,19 +55827,20 @@ function buildAttachmentHandlers(deps) {
54699
55827
 
54700
55828
  // src/handlers/extension.ts
54701
55829
  var import_promises8 = __toESM(require("fs/promises"), 1);
54702
- var import_node_path46 = __toESM(require("path"), 1);
55830
+ var import_node_path53 = __toESM(require("path"), 1);
54703
55831
  init_protocol();
55832
+ init_src();
54704
55833
 
54705
55834
  // src/extension/bundle-zip.ts
54706
55835
  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);
55836
+ var import_node_path49 = __toESM(require("path"), 1);
55837
+ var import_node_crypto13 = __toESM(require("crypto"), 1);
54709
55838
  var import_jszip2 = __toESM(require_lib3(), 1);
54710
55839
  async function bundleExtensionDir(dir) {
54711
55840
  const entries = await listFilesSorted(dir);
54712
55841
  const zip = new import_jszip2.default();
54713
55842
  for (const rel of entries) {
54714
- const abs = import_node_path42.default.join(dir, rel);
55843
+ const abs = import_node_path49.default.join(dir, rel);
54715
55844
  const content = await import_promises5.default.readFile(abs);
54716
55845
  zip.file(rel, content, { date: FIXED_DATE });
54717
55846
  }
@@ -54720,7 +55849,7 @@ async function bundleExtensionDir(dir) {
54720
55849
  compression: "DEFLATE",
54721
55850
  compressionOptions: { level: 6 }
54722
55851
  });
54723
- const sha256 = import_node_crypto14.default.createHash("sha256").update(buffer).digest("hex");
55852
+ const sha256 = import_node_crypto13.default.createHash("sha256").update(buffer).digest("hex");
54724
55853
  return { buffer, sha256 };
54725
55854
  }
54726
55855
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -54732,7 +55861,7 @@ async function listFilesSorted(rootDir) {
54732
55861
  return out;
54733
55862
  }
54734
55863
  async function walk(absRoot, relPrefix, out) {
54735
- const dirAbs = import_node_path42.default.join(absRoot, relPrefix);
55864
+ const dirAbs = import_node_path49.default.join(absRoot, relPrefix);
54736
55865
  const entries = await import_promises5.default.readdir(dirAbs, { withFileTypes: true });
54737
55866
  for (const e of entries) {
54738
55867
  if (IGNORE_BASENAMES.has(e.name)) continue;
@@ -54746,6 +55875,8 @@ async function walk(absRoot, relPrefix, out) {
54746
55875
  }
54747
55876
 
54748
55877
  // src/extension/publish-check.ts
55878
+ init_src();
55879
+ init_src();
54749
55880
  function computePublishCheck(args) {
54750
55881
  const { localHash, localVersion, head } = args;
54751
55882
  if (head === null) {
@@ -54786,25 +55917,27 @@ function computePublishCheck(args) {
54786
55917
 
54787
55918
  // src/extension/install-flow.ts
54788
55919
  var import_promises6 = __toESM(require("fs/promises"), 1);
54789
- var import_node_path44 = __toESM(require("path"), 1);
55920
+ var import_node_path51 = __toESM(require("path"), 1);
54790
55921
  var import_node_os19 = __toESM(require("os"), 1);
54791
- var import_node_crypto15 = __toESM(require("crypto"), 1);
55922
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
54792
55923
  var import_jszip3 = __toESM(require_lib3(), 1);
55924
+ init_src();
54793
55925
 
54794
55926
  // src/extension/paths.ts
54795
55927
  var import_node_os18 = __toESM(require("os"), 1);
54796
- var import_node_path43 = __toESM(require("path"), 1);
55928
+ var import_node_path50 = __toESM(require("path"), 1);
55929
+ init_src();
54797
55930
  function clawdHomeRoot(override) {
54798
- return override ?? process.env.CLAWD_HOME ?? import_node_path43.default.join(import_node_os18.default.homedir(), ".clawd");
55931
+ return override ?? process.env.CLAWD_HOME ?? import_node_path50.default.join(import_node_os18.default.homedir(), ".clawd");
54799
55932
  }
54800
55933
  function extensionsRoot(override) {
54801
- return import_node_path43.default.join(clawdHomeRoot(override), "extensions");
55934
+ return import_node_path50.default.join(clawdHomeRoot(override), "extensions");
54802
55935
  }
54803
55936
  function publishedChannelsFile(override) {
54804
- return import_node_path43.default.join(clawdHomeRoot(override), "extensions-published.json");
55937
+ return import_node_path50.default.join(clawdHomeRoot(override), "extensions-published.json");
54805
55938
  }
54806
55939
  function bundleCacheRoot(override) {
54807
- return import_node_path43.default.join(clawdHomeRoot(override), "extension-bundles");
55940
+ return import_node_path50.default.join(clawdHomeRoot(override), "extension-bundles");
54808
55941
  }
54809
55942
 
54810
55943
  // src/extension/install-flow.ts
@@ -54817,7 +55950,7 @@ var InstallError = class extends Error {
54817
55950
  };
54818
55951
  async function installFromChannel(args, deps) {
54819
55952
  const { channelRef, snapshotHash, bundleZip } = args;
54820
- const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
55953
+ const computed = import_node_crypto14.default.createHash("sha256").update(bundleZip).digest("hex");
54821
55954
  if (computed !== snapshotHash) {
54822
55955
  throw new InstallError(
54823
55956
  "HASH_MISMATCH",
@@ -54831,7 +55964,7 @@ async function installFromChannel(args, deps) {
54831
55964
  throw new InstallError("ZIP_INVALID", `failed to load zip: ${e.message}`);
54832
55965
  }
54833
55966
  for (const name of Object.keys(zip.files)) {
54834
- if (name.includes("..") || name.startsWith("/") || import_node_path44.default.isAbsolute(name)) {
55967
+ if (name.includes("..") || name.startsWith("/") || import_node_path51.default.isAbsolute(name)) {
54835
55968
  throw new InstallError("ZIP_INVALID", `unsafe zip entry: ${name}`);
54836
55969
  }
54837
55970
  }
@@ -54863,7 +55996,7 @@ async function installFromChannel(args, deps) {
54863
55996
  );
54864
55997
  }
54865
55998
  const localExtId = namespacedExtId(ownerSlug, channelRef.ownerPrincipalId);
54866
- const destDir = import_node_path44.default.join(deps.extensionsRoot, localExtId);
55999
+ const destDir = import_node_path51.default.join(deps.extensionsRoot, localExtId);
54867
56000
  let destExists = false;
54868
56001
  try {
54869
56002
  await import_promises6.default.access(destDir);
@@ -54877,16 +56010,16 @@ async function installFromChannel(args, deps) {
54877
56010
  );
54878
56011
  }
54879
56012
  const stage = await import_promises6.default.mkdtemp(
54880
- import_node_path44.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
56013
+ import_node_path51.default.join(import_node_os19.default.tmpdir(), `clawd-ext-install-${localExtId}-`)
54881
56014
  );
54882
56015
  try {
54883
56016
  for (const [name, entry] of Object.entries(zip.files)) {
54884
- const dest = import_node_path44.default.join(stage, name);
56017
+ const dest = import_node_path51.default.join(stage, name);
54885
56018
  if (entry.dir) {
54886
56019
  await import_promises6.default.mkdir(dest, { recursive: true });
54887
56020
  continue;
54888
56021
  }
54889
- await import_promises6.default.mkdir(import_node_path44.default.dirname(dest), { recursive: true });
56022
+ await import_promises6.default.mkdir(import_node_path51.default.dirname(dest), { recursive: true });
54890
56023
  if (name === "manifest.json") {
54891
56024
  const rewritten = { ...parsed.data, id: localExtId };
54892
56025
  await import_promises6.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -54907,10 +56040,11 @@ async function installFromChannel(args, deps) {
54907
56040
 
54908
56041
  // src/extension/update-flow.ts
54909
56042
  var import_promises7 = __toESM(require("fs/promises"), 1);
54910
- var import_node_path45 = __toESM(require("path"), 1);
56043
+ var import_node_path52 = __toESM(require("path"), 1);
54911
56044
  var import_node_os20 = __toESM(require("os"), 1);
54912
- var import_node_crypto16 = __toESM(require("crypto"), 1);
56045
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
54913
56046
  var import_jszip4 = __toESM(require_lib3(), 1);
56047
+ init_src();
54914
56048
  var UpdateError = class extends Error {
54915
56049
  constructor(code, message) {
54916
56050
  super(message);
@@ -54924,11 +56058,11 @@ async function updateFromChannel(args, deps) {
54924
56058
  channelRef.extId,
54925
56059
  channelRef.ownerPrincipalId
54926
56060
  );
54927
- const liveDir = import_node_path45.default.join(deps.extensionsRoot, localExtId);
56061
+ const liveDir = import_node_path52.default.join(deps.extensionsRoot, localExtId);
54928
56062
  const prevDir = `${liveDir}.prev`;
54929
56063
  let existingVersion;
54930
56064
  try {
54931
- const raw = await import_promises7.default.readFile(import_node_path45.default.join(liveDir, "manifest.json"), "utf8");
56065
+ const raw = await import_promises7.default.readFile(import_node_path52.default.join(liveDir, "manifest.json"), "utf8");
54932
56066
  const parsed2 = ExtensionManifestSchema.safeParse(JSON.parse(raw));
54933
56067
  if (!parsed2.success) {
54934
56068
  throw new UpdateError(
@@ -54947,7 +56081,7 @@ async function updateFromChannel(args, deps) {
54947
56081
  if (e instanceof UpdateError) throw e;
54948
56082
  throw e;
54949
56083
  }
54950
- const computed = import_node_crypto16.default.createHash("sha256").update(bundleZip).digest("hex");
56084
+ const computed = import_node_crypto15.default.createHash("sha256").update(bundleZip).digest("hex");
54951
56085
  if (computed !== snapshotHash) {
54952
56086
  throw new UpdateError(
54953
56087
  "HASH_MISMATCH",
@@ -54961,7 +56095,7 @@ async function updateFromChannel(args, deps) {
54961
56095
  throw new UpdateError("ZIP_INVALID", `failed to load zip: ${e.message}`);
54962
56096
  }
54963
56097
  for (const name of Object.keys(zip.files)) {
54964
- if (name.includes("..") || name.startsWith("/") || import_node_path45.default.isAbsolute(name)) {
56098
+ if (name.includes("..") || name.startsWith("/") || import_node_path52.default.isAbsolute(name)) {
54965
56099
  throw new UpdateError("ZIP_INVALID", `unsafe zip entry: ${name}`);
54966
56100
  }
54967
56101
  }
@@ -54996,16 +56130,16 @@ async function updateFromChannel(args, deps) {
54996
56130
  await import_promises7.default.rm(prevDir, { recursive: true, force: true });
54997
56131
  await import_promises7.default.rename(liveDir, prevDir);
54998
56132
  const stage = await import_promises7.default.mkdtemp(
54999
- import_node_path45.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
56133
+ import_node_path52.default.join(import_node_os20.default.tmpdir(), `clawd-ext-update-${localExtId}-`)
55000
56134
  );
55001
56135
  try {
55002
56136
  for (const [name, entry] of Object.entries(zip.files)) {
55003
- const dest = import_node_path45.default.join(stage, name);
56137
+ const dest = import_node_path52.default.join(stage, name);
55004
56138
  if (entry.dir) {
55005
56139
  await import_promises7.default.mkdir(dest, { recursive: true });
55006
56140
  continue;
55007
56141
  }
55008
- await import_promises7.default.mkdir(import_node_path45.default.dirname(dest), { recursive: true });
56142
+ await import_promises7.default.mkdir(import_node_path52.default.dirname(dest), { recursive: true });
55009
56143
  if (name === "manifest.json") {
55010
56144
  const rewritten = { ...parsed.data, id: localExtId };
55011
56145
  await import_promises7.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -55056,6 +56190,7 @@ async function rollback(deps, localExtId, liveDir, prevDir) {
55056
56190
  }
55057
56191
 
55058
56192
  // src/handlers/extension.ts
56193
+ init_src();
55059
56194
  function pickChannelRef(frame) {
55060
56195
  const ref = frame.channelRef;
55061
56196
  const parsed = ChannelRefSchema.safeParse(ref);
@@ -55098,7 +56233,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55098
56233
  );
55099
56234
  }
55100
56235
  }
55101
- const manifestPath = import_node_path46.default.join(root, extId, "manifest.json");
56236
+ const manifestPath = import_node_path53.default.join(root, extId, "manifest.json");
55102
56237
  const manifest = await readManifest(root, extId);
55103
56238
  const next = { ...manifest, version: newVersion };
55104
56239
  const tmp = `${manifestPath}.tmp`;
@@ -55106,7 +56241,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55106
56241
  await import_promises8.default.rename(tmp, manifestPath);
55107
56242
  }
55108
56243
  async function readManifest(root, extId) {
55109
- const file = import_node_path46.default.join(root, extId, "manifest.json");
56244
+ const file = import_node_path53.default.join(root, extId, "manifest.json");
55110
56245
  let raw;
55111
56246
  try {
55112
56247
  raw = await import_promises8.default.readFile(file, "utf8");
@@ -55197,7 +56332,7 @@ function buildExtensionHandlers(deps) {
55197
56332
  };
55198
56333
  async function buildSnapshotMeta(extId) {
55199
56334
  const manifest = await readManifest(deps.root, extId);
55200
- const { sha256, buffer } = await bundleExtensionDir(import_node_path46.default.join(deps.root, extId));
56335
+ const { sha256, buffer } = await bundleExtensionDir(import_node_path53.default.join(deps.root, extId));
55201
56336
  return { manifest, contentHash: sha256, buffer };
55202
56337
  }
55203
56338
  const publish = async (frame, _client, ctx) => {
@@ -55378,9 +56513,9 @@ function buildExtensionHandlers(deps) {
55378
56513
  }
55379
56514
 
55380
56515
  // 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");
56516
+ var import_node_fs46 = require("fs");
56517
+ var import_node_child_process14 = require("child_process");
56518
+ var import_node_path54 = require("path");
55384
56519
  init_protocol();
55385
56520
  var PROJECTS_DIR = "projects";
55386
56521
  var META_FILE = ".clawd-project.json";
@@ -55394,19 +56529,19 @@ var ProjectStore = class {
55394
56529
  root;
55395
56530
  /** projects/<name>/.clawd-project.json 路径 */
55396
56531
  metaPath(name) {
55397
- return (0, import_node_path47.join)(this.projectsRoot(), name, META_FILE);
56532
+ return (0, import_node_path54.join)(this.projectsRoot(), name, META_FILE);
55398
56533
  }
55399
56534
  /** projects/<name>/ 目录路径(cwd 用) */
55400
56535
  projectDir(name) {
55401
- return (0, import_node_path47.join)(this.projectsRoot(), name);
56536
+ return (0, import_node_path54.join)(this.projectsRoot(), name);
55402
56537
  }
55403
56538
  projectsRoot() {
55404
- return (0, import_node_path47.join)(this.root, PROJECTS_DIR);
56539
+ return (0, import_node_path54.join)(this.root, PROJECTS_DIR);
55405
56540
  }
55406
56541
  async list() {
55407
56542
  let entries;
55408
56543
  try {
55409
- entries = await import_node_fs39.promises.readdir(this.projectsRoot());
56544
+ entries = await import_node_fs46.promises.readdir(this.projectsRoot());
55410
56545
  } catch (err) {
55411
56546
  if (err.code === "ENOENT") return [];
55412
56547
  throw err;
@@ -55414,7 +56549,7 @@ var ProjectStore = class {
55414
56549
  const out = [];
55415
56550
  for (const name of entries) {
55416
56551
  try {
55417
- const raw = await import_node_fs39.promises.readFile(this.metaPath(name), "utf8");
56552
+ const raw = await import_node_fs46.promises.readFile(this.metaPath(name), "utf8");
55418
56553
  const json = JSON.parse(raw);
55419
56554
  let migrated = false;
55420
56555
  if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
@@ -55425,7 +56560,7 @@ var ProjectStore = class {
55425
56560
  if (parsed.success) {
55426
56561
  out.push(parsed.data);
55427
56562
  if (migrated) {
55428
- void import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
56563
+ void import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(parsed.data, null, 2) + "\n", "utf8").catch(() => {
55429
56564
  });
55430
56565
  }
55431
56566
  }
@@ -55469,8 +56604,8 @@ var ProjectStore = class {
55469
56604
  throw new Error(`invalid name "${name}": ${validated.error.message}`);
55470
56605
  }
55471
56606
  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");
56607
+ await import_node_fs46.promises.mkdir(dir, { recursive: true });
56608
+ await import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(meta, null, 2) + "\n", "utf8");
55474
56609
  return meta;
55475
56610
  }
55476
56611
  /**
@@ -55484,7 +56619,7 @@ var ProjectStore = class {
55484
56619
  async scaffold(name, templateSrcDir, scaffoldScriptPath) {
55485
56620
  const destDir = this.projectDir(name);
55486
56621
  return await new Promise((resolve6, reject) => {
55487
- const child = (0, import_node_child_process11.spawn)("bash", [scaffoldScriptPath, name, templateSrcDir, destDir], {
56622
+ const child = (0, import_node_child_process14.spawn)("bash", [scaffoldScriptPath, name, templateSrcDir, destDir], {
55488
56623
  env: { ...process.env, PATH: process.env.PATH ?? "" },
55489
56624
  stdio: ["ignore", "pipe", "pipe"]
55490
56625
  });
@@ -55513,7 +56648,7 @@ var ProjectStore = class {
55513
56648
  }
55514
56649
  async delete(name) {
55515
56650
  const dir = this.projectDir(name);
55516
- await import_node_fs39.promises.rm(dir, { recursive: true, force: true });
56651
+ await import_node_fs46.promises.rm(dir, { recursive: true, force: true });
55517
56652
  }
55518
56653
  async updatePort(name, newPort) {
55519
56654
  if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
@@ -55529,7 +56664,7 @@ var ProjectStore = class {
55529
56664
  throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
55530
56665
  }
55531
56666
  const updated = { ...target, port: newPort };
55532
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
56667
+ await import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(updated, null, 2) + "\n", "utf8");
55533
56668
  return updated;
55534
56669
  }
55535
56670
  /**
@@ -55546,7 +56681,7 @@ var ProjectStore = class {
55546
56681
  if (!validated.success) {
55547
56682
  throw new Error(`invalid prodUrl "${url}": ${validated.error.message}`);
55548
56683
  }
55549
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56684
+ await import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55550
56685
  return validated.data;
55551
56686
  }
55552
56687
  /**
@@ -55567,7 +56702,7 @@ var ProjectStore = class {
55567
56702
  if (!validated.success) {
55568
56703
  throw new Error(`invalid publishJob: ${validated.error.message}`);
55569
56704
  }
55570
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56705
+ await import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55571
56706
  return validated.data;
55572
56707
  }
55573
56708
  /** 清掉 .clawd-project.json.publishJob 字段。其他字段保持原样。 */
@@ -55582,13 +56717,13 @@ var ProjectStore = class {
55582
56717
  if (!validated.success) {
55583
56718
  throw new Error(`failed to clear publishJob: ${validated.error.message}`);
55584
56719
  }
55585
- await import_node_fs39.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
56720
+ await import_node_fs46.promises.writeFile(this.metaPath(name), JSON.stringify(validated.data, null, 2) + "\n", "utf8");
55586
56721
  return validated.data;
55587
56722
  }
55588
56723
  };
55589
56724
 
55590
56725
  // src/app-builder/kill-port.ts
55591
- var import_node_child_process12 = require("child_process");
56726
+ var import_node_child_process15 = require("child_process");
55592
56727
  async function killPortOccupants(port, ownedPids, logger) {
55593
56728
  let pids;
55594
56729
  try {
@@ -55630,7 +56765,7 @@ async function killPortOccupants(port, ownedPids, logger) {
55630
56765
  }
55631
56766
  function listPidsOnPort(port) {
55632
56767
  return new Promise((resolve6, reject) => {
55633
- (0, import_node_child_process12.execFile)(
56768
+ (0, import_node_child_process15.execFile)(
55634
56769
  "lsof",
55635
56770
  ["-ti", `:${port}`],
55636
56771
  { timeout: 3e3 },
@@ -55652,7 +56787,7 @@ function listPidsOnPort(port) {
55652
56787
  }
55653
56788
 
55654
56789
  // src/app-builder/publish-registry.ts
55655
- var import_node_crypto17 = require("crypto");
56790
+ var import_node_crypto16 = require("crypto");
55656
56791
  var PublishJobRegistry = class {
55657
56792
  jobs = /* @__PURE__ */ new Map();
55658
56793
  has(name) {
@@ -55669,7 +56804,7 @@ var PublishJobRegistry = class {
55669
56804
  if (this.jobs.has(args.name)) {
55670
56805
  throw new Error(`already publishing: ${args.name}`);
55671
56806
  }
55672
- const jobId = args.jobId ?? `job-${(0, import_node_crypto17.randomUUID)()}`;
56807
+ const jobId = args.jobId ?? `job-${(0, import_node_crypto16.randomUUID)()}`;
55673
56808
  this.jobs.set(args.name, {
55674
56809
  jobId,
55675
56810
  name: args.name,
@@ -55702,9 +56837,9 @@ var PublishJobRegistry = class {
55702
56837
  };
55703
56838
 
55704
56839
  // 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");
56840
+ var import_node_child_process16 = require("child_process");
56841
+ var import_node_fs47 = require("fs");
56842
+ var import_node_path55 = require("path");
55708
56843
 
55709
56844
  // src/app-builder/publish-stage-parser.ts
55710
56845
  var STAGE_RE = /^\s*::stage::(build|deploy|verify)\s*$/;
@@ -55731,19 +56866,19 @@ function tailStderrLines(buf, n) {
55731
56866
  // src/app-builder/publish-job-runner.ts
55732
56867
  async function startPublishJob(deps, args) {
55733
56868
  const { registry: registry2, projectDir } = deps;
55734
- const spawn12 = deps.spawnImpl ?? import_node_child_process13.spawn;
56869
+ const spawn15 = deps.spawnImpl ?? import_node_child_process16.spawn;
55735
56870
  if (registry2.has(args.name)) {
55736
56871
  return { jobId: registry2.get(args.name).jobId, status: "already-publishing" };
55737
56872
  }
55738
56873
  const projDir = projectDir(args.name);
55739
- const logPath = (0, import_node_path48.join)(projDir, ".publish.log");
56874
+ const logPath = (0, import_node_path55.join)(projDir, ".publish.log");
55740
56875
  let logStream = null;
55741
56876
  try {
55742
- logStream = (0, import_node_fs40.createWriteStream)(logPath, { flags: "w" });
56877
+ logStream = (0, import_node_fs47.createWriteStream)(logPath, { flags: "w" });
55743
56878
  } catch {
55744
56879
  logStream = null;
55745
56880
  }
55746
- const child = spawn12("bash", [args.scriptPath, projDir, args.personaRoot ?? ""], {
56881
+ const child = spawn15("bash", [args.scriptPath, projDir, args.personaRoot ?? ""], {
55747
56882
  cwd: projDir,
55748
56883
  env: process.env,
55749
56884
  stdio: ["ignore", "pipe", "pipe"]
@@ -55996,8 +57131,8 @@ async function recoverInterruptedJobs(deps) {
55996
57131
 
55997
57132
  // src/handlers/app-builder.ts
55998
57133
  init_protocol();
55999
- var import_node_path49 = require("path");
56000
- var import_node_fs41 = require("fs");
57134
+ var import_node_path56 = require("path");
57135
+ var import_node_fs48 = require("fs");
56001
57136
  var APP_BUILDER_PERSONAS = ["persona-app-builder", "persona-dataclaw-builder"];
56002
57137
  var DEV_SERVER_READY_TIMEOUT_MS = 3e4;
56003
57138
  async function recoverInterruptedPublishJobs(store, logger) {
@@ -56078,7 +57213,7 @@ function buildAppBuilderHandlers(deps) {
56078
57213
  async function listAllUsersProjects() {
56079
57214
  if (!deps.usersRoot || !deps.getStore) return [];
56080
57215
  const getStore = deps.getStore;
56081
- const userIds = await import_node_fs41.promises.readdir(deps.usersRoot).catch(() => []);
57216
+ const userIds = await import_node_fs48.promises.readdir(deps.usersRoot).catch(() => []);
56082
57217
  const perUser = await Promise.all(
56083
57218
  userIds.map((uid) => getStore(uid).list().catch(() => []))
56084
57219
  );
@@ -56154,8 +57289,8 @@ function buildAppBuilderHandlers(deps) {
56154
57289
  const project = await userStore.create(f.name, reservedPorts);
56155
57290
  try {
56156
57291
  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");
57292
+ const templateSrcDir = (0, import_node_path56.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
57293
+ const scaffoldScript = (0, import_node_path56.join)(deps.deployKitRoot, "scripts", "new-extension.sh");
56159
57294
  const scaffoldResult = await userStore.scaffold(project.name, templateSrcDir, scaffoldScript);
56160
57295
  deps.logger?.info("app-builder.scaffold.done", {
56161
57296
  name: project.name,
@@ -56376,7 +57511,7 @@ function buildAppBuilderHandlers(deps) {
56376
57511
  await userStore.clearPublishJob(args.name);
56377
57512
  }
56378
57513
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(boundSession.ownerPersonaId ?? "") : deps.personaRoot;
56379
- const scriptPath = (0, import_node_path49.join)(deps.deployKitRoot, "scripts", "publish.sh");
57514
+ const scriptPath = (0, import_node_path56.join)(deps.deployKitRoot, "scripts", "publish.sh");
56380
57515
  deps.logger?.info("app-builder.publish.start", {
56381
57516
  name: args.name,
56382
57517
  sessionId: boundSession.sessionId,
@@ -56520,7 +57655,7 @@ function buildShiftHandlers(deps) {
56520
57655
 
56521
57656
  // src/handlers/visitor.ts
56522
57657
  init_protocol();
56523
- function ensureOwner3(ctx) {
57658
+ function ensureOwner4(ctx) {
56524
57659
  if (!ctx || ctx.principal.kind !== "owner") {
56525
57660
  throw new ClawdError(
56526
57661
  ERROR_CODES.UNAUTHORIZED,
@@ -56530,7 +57665,7 @@ function ensureOwner3(ctx) {
56530
57665
  }
56531
57666
  function buildVisitorHandlers(deps) {
56532
57667
  const list = async (_frame, _client, ctx) => {
56533
- ensureOwner3(ctx);
57668
+ ensureOwner4(ctx);
56534
57669
  return {
56535
57670
  response: {
56536
57671
  type: "visitor:list",
@@ -56545,7 +57680,8 @@ function buildVisitorHandlers(deps) {
56545
57680
 
56546
57681
  // src/extension/registry.ts
56547
57682
  var import_promises9 = __toESM(require("fs/promises"), 1);
56548
- var import_node_path50 = __toESM(require("path"), 1);
57683
+ var import_node_path57 = __toESM(require("path"), 1);
57684
+ init_src();
56549
57685
  async function loadAll(root) {
56550
57686
  let entries;
56551
57687
  try {
@@ -56558,13 +57694,13 @@ async function loadAll(root) {
56558
57694
  for (const ent of entries) {
56559
57695
  if (!ent.isDirectory()) continue;
56560
57696
  if (ent.name.startsWith(".")) continue;
56561
- records.push(await loadOne(import_node_path50.default.join(root, ent.name), ent.name));
57697
+ records.push(await loadOne(import_node_path57.default.join(root, ent.name), ent.name));
56562
57698
  }
56563
57699
  records.sort((a, b2) => a.extId < b2.extId ? -1 : a.extId > b2.extId ? 1 : 0);
56564
57700
  return records;
56565
57701
  }
56566
57702
  async function loadOne(dir, dirName) {
56567
- const manifestPath = import_node_path50.default.join(dir, "manifest.json");
57703
+ const manifestPath = import_node_path57.default.join(dir, "manifest.json");
56568
57704
  let raw;
56569
57705
  try {
56570
57706
  raw = await import_promises9.default.readFile(manifestPath, "utf8");
@@ -56609,7 +57745,7 @@ async function loadOne(dir, dirName) {
56609
57745
 
56610
57746
  // src/extension/uninstall.ts
56611
57747
  var import_promises10 = __toESM(require("fs/promises"), 1);
56612
- var import_node_path51 = __toESM(require("path"), 1);
57748
+ var import_node_path58 = __toESM(require("path"), 1);
56613
57749
  var UninstallError = class extends Error {
56614
57750
  constructor(code, message) {
56615
57751
  super(message);
@@ -56618,7 +57754,7 @@ var UninstallError = class extends Error {
56618
57754
  code;
56619
57755
  };
56620
57756
  async function uninstall(deps) {
56621
- const dir = import_node_path51.default.join(deps.root, deps.extId);
57757
+ const dir = import_node_path58.default.join(deps.root, deps.extId);
56622
57758
  try {
56623
57759
  await import_promises10.default.access(dir);
56624
57760
  } catch {
@@ -56629,7 +57765,8 @@ async function uninstall(deps) {
56629
57765
  }
56630
57766
 
56631
57767
  // src/handlers/index.ts
56632
- var import_node_crypto18 = require("crypto");
57768
+ var import_node_crypto17 = require("crypto");
57769
+ init_peer_forward();
56633
57770
  function buildMethodHandlers(deps) {
56634
57771
  return {
56635
57772
  ...buildSessionHandlers({
@@ -56662,7 +57799,7 @@ function buildMethodHandlers(deps) {
56662
57799
  const c = deps.contactStore.get(deviceId);
56663
57800
  return c ? { deviceId: c.deviceId, remoteUrl: c.remoteUrl, connectToken: c.connectToken } : null;
56664
57801
  },
56665
- genId: () => (0, import_node_crypto18.randomUUID)(),
57802
+ genId: () => (0, import_node_crypto17.randomUUID)(),
56666
57803
  now: () => Date.now(),
56667
57804
  forwardInboxPostToPeer,
56668
57805
  logger: deps.logger
@@ -56673,6 +57810,12 @@ function buildMethodHandlers(deps) {
56673
57810
  broadcast: deps.broadcastToOwners,
56674
57811
  now: () => Date.now()
56675
57812
  }),
57813
+ ...buildContactSshHandlers({
57814
+ store: deps.contactStore,
57815
+ broadcast: deps.broadcastToOwners,
57816
+ sshdDir: deps.sshdDir,
57817
+ sshLog: deps.contactSshLog
57818
+ }),
56676
57819
  whoami: buildWhoamiHandler({
56677
57820
  ownerDisplayName: deps.ownerDisplayName,
56678
57821
  ownerPrincipalId: deps.ownerPrincipalId,
@@ -56719,7 +57862,7 @@ function buildMethodHandlers(deps) {
56719
57862
  }
56720
57863
 
56721
57864
  // src/app-builder/dev-server-supervisor.ts
56722
- var import_node_child_process14 = require("child_process");
57865
+ var import_node_child_process17 = require("child_process");
56723
57866
  var import_node_events2 = require("events");
56724
57867
  var DEFAULT_READY_PATTERN = /Local:\s+https?:\/\/|Nest application successfully started|server listening on/i;
56725
57868
  var DevServerSupervisor = class extends import_node_events2.EventEmitter {
@@ -56756,7 +57899,7 @@ var DevServerSupervisor = class extends import_node_events2.EventEmitter {
56756
57899
  tunnelHost: args.tunnelHost,
56757
57900
  devCommand: cmd
56758
57901
  });
56759
- const child = (0, import_node_child_process14.spawn)("sh", ["-c", cmd], {
57902
+ const child = (0, import_node_child_process17.spawn)("sh", ["-c", cmd], {
56760
57903
  cwd: args.cwd,
56761
57904
  env,
56762
57905
  stdio: "pipe",
@@ -57008,6 +58151,12 @@ var METHOD_GRANT_MAP = {
57008
58151
  "contact:list": ADMIN_ANY,
57009
58152
  "contact:pin": ADMIN_ANY,
57010
58153
  "contact:remove": ADMIN_ANY,
58154
+ // contact:setSshAccess (owner UI 配 SSH 授权):ADMIN_ANY
58155
+ // contact:sshKey:issue (guest daemon 拉自己的 privkey):public — handler 内校
58156
+ // ctx.principal.kind==='guest' + store.get(callerId).sshAllowed
58157
+ // (对齐 inbox:postMessage 的"能连上=有 auth,业务在 handler 校"模式)
58158
+ "contact:setSshAccess": ADMIN_ANY,
58159
+ "contact:sshKey:issue": { kind: "public" },
57011
58160
  // ---- visitor:* (访客名单,owner-only) ----
57012
58161
  // owner 看完整访客名单(含没开会话的);guest 不可调(handler 内再 assertOwner 兜底)。
57013
58162
  "visitor:list": ADMIN_ANY,
@@ -57188,12 +58337,13 @@ async function dispatchRpc(method, frame, client, ctx, deps) {
57188
58337
  }
57189
58338
 
57190
58339
  // src/extension/runtime.ts
57191
- var import_node_child_process15 = require("child_process");
57192
- var import_node_path52 = __toESM(require("path"), 1);
58340
+ var import_node_child_process18 = require("child_process");
58341
+ var import_node_path59 = __toESM(require("path"), 1);
57193
58342
  var import_promises11 = require("timers/promises");
58343
+ init_src();
57194
58344
 
57195
58345
  // src/extension/port-allocator.ts
57196
- var import_node_net2 = __toESM(require("net"), 1);
58346
+ var import_node_net3 = __toESM(require("net"), 1);
57197
58347
  var PortExhaustedError = class extends Error {
57198
58348
  constructor(min, max) {
57199
58349
  super(`no free port in [${min},${max}]`);
@@ -57206,7 +58356,7 @@ var PortExhaustedError = class extends Error {
57206
58356
  };
57207
58357
  function probe(port) {
57208
58358
  return new Promise((resolve6) => {
57209
- const srv = import_node_net2.default.createServer();
58359
+ const srv = import_node_net3.default.createServer();
57210
58360
  srv.once("error", () => resolve6(false));
57211
58361
  srv.once("listening", () => {
57212
58362
  srv.close(() => resolve6(true));
@@ -57290,13 +58440,13 @@ var Runtime = class {
57290
58440
  /\$CLAWOS_EXT_PORT/g,
57291
58441
  String(port)
57292
58442
  );
57293
- const dir = import_node_path52.default.join(this.root, extId);
58443
+ const dir = import_node_path59.default.join(this.root, extId);
57294
58444
  const env = {
57295
58445
  ...process.env,
57296
58446
  CLAWOS_EXT_PORT: String(port),
57297
58447
  CLAWOS_EXT_ID: extId
57298
58448
  };
57299
- const child = (0, import_node_child_process15.spawn)("sh", ["-c", cmd], {
58449
+ const child = (0, import_node_child_process18.spawn)("sh", ["-c", cmd], {
57300
58450
  cwd: dir,
57301
58451
  env,
57302
58452
  stdio: ["ignore", "pipe", "pipe"],
@@ -57402,7 +58552,8 @@ ${handle.stderrTail}`
57402
58552
 
57403
58553
  // src/extension/published-channels.ts
57404
58554
  var import_promises12 = __toESM(require("fs/promises"), 1);
57405
- var import_node_path53 = __toESM(require("path"), 1);
58555
+ var import_node_path60 = __toESM(require("path"), 1);
58556
+ init_src();
57406
58557
  init_zod();
57407
58558
  var PublishedChannelsError = class extends Error {
57408
58559
  constructor(code, message) {
@@ -57501,7 +58652,7 @@ var PublishedChannelStore = class {
57501
58652
  )
57502
58653
  };
57503
58654
  const tmp = `${this.filePath}.tmp`;
57504
- await import_promises12.default.mkdir(import_node_path53.default.dirname(this.filePath), { recursive: true });
58655
+ await import_promises12.default.mkdir(import_node_path60.default.dirname(this.filePath), { recursive: true });
57505
58656
  await import_promises12.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
57506
58657
  await import_promises12.default.rename(tmp, this.filePath);
57507
58658
  }
@@ -57509,7 +58660,7 @@ var PublishedChannelStore = class {
57509
58660
 
57510
58661
  // src/extension/bundle-cache.ts
57511
58662
  var import_promises13 = __toESM(require("fs/promises"), 1);
57512
- var import_node_path54 = __toESM(require("path"), 1);
58663
+ var import_node_path61 = __toESM(require("path"), 1);
57513
58664
  var BundleCache = class {
57514
58665
  constructor(rootDir) {
57515
58666
  this.rootDir = rootDir;
@@ -57518,14 +58669,14 @@ var BundleCache = class {
57518
58669
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
57519
58670
  async write(snapshotHash, buffer) {
57520
58671
  await import_promises13.default.mkdir(this.rootDir, { recursive: true });
57521
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58672
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
57522
58673
  const tmp = `${file}.tmp`;
57523
58674
  await import_promises13.default.writeFile(tmp, buffer, { mode: 384 });
57524
58675
  await import_promises13.default.rename(tmp, file);
57525
58676
  }
57526
58677
  /** Returns the bundle bytes, or null when the file doesn't exist. */
57527
58678
  async read(snapshotHash) {
57528
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58679
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
57529
58680
  try {
57530
58681
  return await import_promises13.default.readFile(file);
57531
58682
  } catch (e) {
@@ -57535,7 +58686,7 @@ var BundleCache = class {
57535
58686
  }
57536
58687
  /** Idempotent — missing file is not an error. */
57537
58688
  async delete(snapshotHash) {
57538
- const file = import_node_path54.default.join(this.rootDir, `${snapshotHash}.zip`);
58689
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
57539
58690
  await import_promises13.default.rm(file, { force: true });
57540
58691
  }
57541
58692
  };
@@ -57560,17 +58711,10 @@ async function startDaemon(config) {
57560
58711
  });
57561
58712
  const logger = createLogger({
57562
58713
  level: config.logLevel,
57563
- file: import_node_path55.default.join(config.dataDir, "clawd.log"),
58714
+ file: import_node_path62.default.join(config.dataDir, "clawd.log"),
57564
58715
  logClient
57565
58716
  });
57566
58717
  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
58718
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
57575
58719
  const pre = stateMgr.preflight();
57576
58720
  if (pre.status === "active") {
@@ -57707,8 +58851,8 @@ async function startDaemon(config) {
57707
58851
  const agents = new AgentsScanner();
57708
58852
  const history = new ClaudeHistoryReader();
57709
58853
  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");
58854
+ const personaStore = new PersonaStore(import_node_path62.default.join(config.dataDir, "personas"));
58855
+ const usersRoot = import_node_path62.default.join(config.dataDir, "users");
57712
58856
  const defaultsRoot = findDefaultsRoot(logger);
57713
58857
  if (defaultsRoot) {
57714
58858
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -57728,17 +58872,17 @@ async function startDaemon(config) {
57728
58872
  migrateCodexSandbox({ store: personaStore, logger });
57729
58873
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
57730
58874
  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));
58875
+ const here = typeof __dirname === "string" ? __dirname : import_node_path62.default.dirname((0, import_node_url4.fileURLToPath)(import_meta6.url));
57732
58876
  const dispatchServerCandidates = [
57733
- import_node_path55.default.join(here, "dispatch", "mcp-server.cjs"),
58877
+ import_node_path62.default.join(here, "dispatch", "mcp-server.cjs"),
57734
58878
  // 生产 dist/index → dist/dispatch/mcp-server.cjs
57735
- import_node_path55.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
58879
+ import_node_path62.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
57736
58880
  // dev tsx src/index → ../dist/dispatch/mcp-server.cjs
57737
58881
  ];
57738
- const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58882
+ const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
57739
58883
  let dispatchMcpConfigPath2;
57740
58884
  if (dispatchServerScriptPath) {
57741
- const dispatchLogPath = import_node_path55.default.join(config.dataDir, "dispatch-mcp-server.log");
58885
+ const dispatchLogPath = import_node_path62.default.join(config.dataDir, "dispatch-mcp-server.log");
57742
58886
  dispatchMcpConfigPath2 = writeDispatchMcpConfig({
57743
58887
  dataDir: config.dataDir,
57744
58888
  serverScriptPath: dispatchServerScriptPath,
@@ -57755,15 +58899,15 @@ async function startDaemon(config) {
57755
58899
  });
57756
58900
  }
57757
58901
  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")
58902
+ import_node_path62.default.join(here, "ticket", "mcp-server.cjs"),
58903
+ import_node_path62.default.join(here, "..", "dist", "ticket", "mcp-server.cjs")
57760
58904
  ];
57761
- const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58905
+ const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
57762
58906
  const ticketOwnerUnionId = feishuIdentity?.identity.unionId ?? "";
57763
58907
  const ticketOwnerName = feishuIdentity?.identity.displayName ?? "";
57764
58908
  let ticketMcpConfigPath2;
57765
58909
  if (ticketServerScriptPath && ticketOwnerUnionId) {
57766
- const ticketLogPath = import_node_path55.default.join(config.dataDir, "ticket-mcp-server.log");
58910
+ const ticketLogPath = import_node_path62.default.join(config.dataDir, "ticket-mcp-server.log");
57767
58911
  ticketMcpConfigPath2 = writeTicketMcpConfig({
57768
58912
  dataDir: config.dataDir,
57769
58913
  serverScriptPath: ticketServerScriptPath,
@@ -57784,13 +58928,13 @@ async function startDaemon(config) {
57784
58928
  });
57785
58929
  }
57786
58930
  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")
58931
+ import_node_path62.default.join(here, "shift", "mcp-server.cjs"),
58932
+ import_node_path62.default.join(here, "..", "dist", "shift", "mcp-server.cjs")
57789
58933
  ];
57790
- const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58934
+ const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
57791
58935
  let shiftMcpConfigPath2;
57792
58936
  if (shiftServerScriptPath) {
57793
- const shiftLogPath = import_node_path55.default.join(config.dataDir, "shift-mcp-server.log");
58937
+ const shiftLogPath = import_node_path62.default.join(config.dataDir, "shift-mcp-server.log");
57794
58938
  shiftMcpConfigPath2 = await writeShiftMcpConfig({
57795
58939
  dataDir: config.dataDir,
57796
58940
  serverScriptPath: shiftServerScriptPath,
@@ -57808,13 +58952,13 @@ async function startDaemon(config) {
57808
58952
  );
57809
58953
  }
57810
58954
  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")
58955
+ import_node_path62.default.join(here, "inbox", "mcp-server.cjs"),
58956
+ import_node_path62.default.join(here, "..", "dist", "inbox", "mcp-server.cjs")
57813
58957
  ];
57814
- const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs42.default.existsSync(p2));
58958
+ const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
57815
58959
  let inboxMcpConfigPath2;
57816
58960
  if (inboxServerScriptPath) {
57817
- const inboxLogPath = import_node_path55.default.join(config.dataDir, "inbox-mcp-server.log");
58961
+ const inboxLogPath = import_node_path62.default.join(config.dataDir, "inbox-mcp-server.log");
57818
58962
  inboxMcpConfigPath2 = await writeInboxMcpConfig({
57819
58963
  dataDir: config.dataDir,
57820
58964
  serverScriptPath: inboxServerScriptPath,
@@ -57832,7 +58976,7 @@ async function startDaemon(config) {
57832
58976
  );
57833
58977
  }
57834
58978
  const shiftStore = createShiftStore({
57835
- filePath: import_node_path55.default.join(config.dataDir, "shift.json"),
58979
+ filePath: import_node_path62.default.join(config.dataDir, "shift.json"),
57836
58980
  ownerIdProvider: () => ownerPrincipalId,
57837
58981
  now: () => Date.now()
57838
58982
  });
@@ -57847,14 +58991,10 @@ async function startDaemon(config) {
57847
58991
  // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
57848
58992
  storeFactory: sessionStoreFactory,
57849
58993
  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
58994
  getAdapter,
57855
58995
  historyReader: history,
57856
58996
  dataDir: config.dataDir,
57857
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
58997
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
57858
58998
  usersRoot,
57859
58999
  personaStore,
57860
59000
  ownerDisplayName,
@@ -57897,10 +59037,10 @@ async function startDaemon(config) {
57897
59037
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
57898
59038
  attachmentGroup: {
57899
59039
  onFileEdit: (input) => {
57900
- const absPath = import_node_path55.default.isAbsolute(input.relPath) ? input.relPath : import_node_path55.default.join(input.cwd, input.relPath);
59040
+ const absPath = import_node_path62.default.isAbsolute(input.relPath) ? input.relPath : import_node_path62.default.join(input.cwd, input.relPath);
57901
59041
  let size = 0;
57902
59042
  try {
57903
- size = import_node_fs42.default.statSync(absPath).size;
59043
+ size = import_node_fs49.default.statSync(absPath).size;
57904
59044
  } catch (err) {
57905
59045
  logger.warn("attachment.onFileEdit stat failed", {
57906
59046
  sessionId: input.sessionId,
@@ -57968,10 +59108,10 @@ async function startDaemon(config) {
57968
59108
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
57969
59109
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
57970
59110
  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
59111
+ // 屏幕静止补权威 turn_end(修 turn_duration 尾随 text 覆盖 lastEventKind)
59112
+ onTurnIdle: (tsid) => manager.dispatchTurnIdle(tsid),
59113
+ // 复合条件闸:observer 还需静止多久才满 idleMs(屏幕静止后精确等这段剩余再补 turn_end
59114
+ getObserverWaitMs: (tsid, idleMs) => manager.observerIdleWaitMs(tsid, idleMs)
57975
59115
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
57976
59116
  registerAdapter("claude", claudeAdapter);
57977
59117
  registerAdapter("codex", new CodexAdapter({ logger, historyReader: new CodexHistoryReader() }));
@@ -58055,6 +59195,7 @@ async function startDaemon(config) {
58055
59195
  const effectivePreviewPorts = Array.from(
58056
59196
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
58057
59197
  );
59198
+ const sshLog = createContactSshLog(config.dataDir);
58058
59199
  const makeHandlers = () => buildMethodHandlers({
58059
59200
  manager,
58060
59201
  workspace,
@@ -58098,11 +59239,11 @@ async function startDaemon(config) {
58098
59239
  // 'persona/<pid>/owner',default 走 'default'。
58099
59240
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58100
59241
  // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
58101
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
59242
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
58102
59243
  usersRoot
58103
59244
  },
58104
59245
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
58105
- personaRoot: import_node_path55.default.join(config.dataDir, "personas"),
59246
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
58106
59247
  // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
58107
59248
  usersRoot,
58108
59249
  // capability:list / delete handler 依赖
@@ -58122,6 +59263,10 @@ async function startDaemon(config) {
58122
59263
  inboxStore,
58123
59264
  // 联系人列表 store(device:connect / 自动反向落同一 store)
58124
59265
  contactStore,
59266
+ // <dataDir>/sshd 绝对路径 —— contact-ssh handlers 用它拼 authorized_keys / keys/ 子路径
59267
+ // Task 10 会加 SshdManager 起 sshd;handlers wire 提前挂 sshdDir 让 typecheck 过
59268
+ sshdDir: import_node_path62.default.join(config.dataDir, "sshd"),
59269
+ contactSshLog: sshLog,
58125
59270
  // inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
58126
59271
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58127
59272
  // contact:removed broadcast;复用 capability:tokenIssued 同款通路
@@ -58211,11 +59356,11 @@ async function startDaemon(config) {
58211
59356
  // 发布上线脚手架化 (spec 2026-06-03 §5.2):
58212
59357
  // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
58213
59358
  // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
58214
- appBuilderPersonaRoot: import_node_path55.default.join(config.dataDir, "personas", "persona-app-builder"),
59359
+ appBuilderPersonaRoot: import_node_path62.default.join(config.dataDir, "personas", "persona-app-builder"),
58215
59360
  // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
58216
- deployKitRoot: import_node_path55.default.join(config.dataDir, "deploy-kit"),
59361
+ deployKitRoot: import_node_path62.default.join(config.dataDir, "deploy-kit"),
58217
59362
  // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
58218
- resolvePersonaRoot: (personaId) => import_node_path55.default.join(config.dataDir, "personas", personaId),
59363
+ resolvePersonaRoot: (personaId) => import_node_path62.default.join(config.dataDir, "personas", personaId),
58219
59364
  // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
58220
59365
  // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
58221
59366
  // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
@@ -58258,7 +59403,7 @@ async function startDaemon(config) {
58258
59403
  }
58259
59404
  let sourceJsonlPath = "(no transcript yet \u2014 operate from the task description alone)";
58260
59405
  if (sourceFile && sourceFile.toolSessionId) {
58261
- sourceJsonlPath = import_node_path55.default.join(
59406
+ sourceJsonlPath = import_node_path62.default.join(
58262
59407
  import_node_os21.default.homedir(),
58263
59408
  ".claude",
58264
59409
  "projects",
@@ -58447,6 +59592,36 @@ async function startDaemon(config) {
58447
59592
  httpRequestHandler: httpRouter,
58448
59593
  // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
58449
59594
  previewPorts: effectivePreviewPorts,
59595
+ // Contact SSH tunnel (PR: contact-ssh-tunnel): /rpc/ssh-tunnel WS 前置拦截 →
59596
+ // Bearer auth (owner token / connect token / capability token 三选一) → net.connect 到本机 sshd →
59597
+ // raw byte relay. 用于 B 侧 `clawd ssh-relay` CLI 拨号(SSH ProxyCommand)
59598
+ sshTunnelUpgradeHandler: async (req, socket, head, wss2) => {
59599
+ await handleSshTunnelUpgrade(req, socket, head, {
59600
+ wss: wss2,
59601
+ sshdPort: config.sshdPort,
59602
+ logger,
59603
+ sshLog,
59604
+ authorize: async (token) => {
59605
+ if (!token) return false;
59606
+ if (resolvedAuthToken && token === resolvedAuthToken) return true;
59607
+ const cap = capabilityRegistry.verifyToken(token);
59608
+ if (cap.ok) return true;
59609
+ const vis = verifyVisitorToken(authFile.visitorTokenSecret, token, Math.floor(Date.now() / 1e3));
59610
+ if (vis.ok) return true;
59611
+ const publicKeyPem = serverKeyStore.read();
59612
+ if (publicKeyPem) {
59613
+ const r = verifyConnectToken({
59614
+ token,
59615
+ publicKeyPem,
59616
+ expectedDeviceId: authFile.deviceId
59617
+ });
59618
+ if (r.ok) return true;
59619
+ }
59620
+ return false;
59621
+ }
59622
+ });
59623
+ return true;
59624
+ },
58450
59625
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
58451
59626
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
58452
59627
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -58558,8 +59733,8 @@ async function startDaemon(config) {
58558
59733
  const lines = [
58559
59734
  `Tunnel: ${r.url}`,
58560
59735
  ...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")}`
59736
+ `Frpc config: ${import_node_path62.default.join(config.dataDir, "frpc.toml")}`,
59737
+ `Frpc log: ${import_node_path62.default.join(config.dataDir, "frpc.log")}`
58563
59738
  ];
58564
59739
  const width = Math.max(...lines.map((l) => l.length));
58565
59740
  const bar = "\u2550".repeat(width + 4);
@@ -58572,8 +59747,8 @@ ${bar}
58572
59747
 
58573
59748
  `);
58574
59749
  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 });
59750
+ const connectPath = import_node_path62.default.join(config.dataDir, "connect.txt");
59751
+ import_node_fs49.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
58577
59752
  } catch {
58578
59753
  }
58579
59754
  } catch (err) {
@@ -58598,6 +59773,30 @@ ${bar}
58598
59773
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
58599
59774
  }
58600
59775
  }
59776
+ const sshdMgr = new SshdManager({
59777
+ dataDir: config.dataDir,
59778
+ port: config.sshdPort,
59779
+ logger,
59780
+ installProcessExitHandlers: true,
59781
+ onSshdExit: (info) => logger.warn("sshd exited unexpectedly", info)
59782
+ });
59783
+ try {
59784
+ await sshdMgr.start();
59785
+ rebuildAuthorizedKeys(contactStore, import_node_path62.default.join(config.dataDir, "sshd"));
59786
+ logger.info("sshd: contact-ssh sandbox ready", { port: config.sshdPort });
59787
+ } catch (err) {
59788
+ logger.warn("sshd start failed; contact SSH grant will not work until fixed", {
59789
+ err: err.message
59790
+ });
59791
+ }
59792
+ const contactKeyPuller = new ContactKeyPuller({
59793
+ store: contactStore,
59794
+ dataDir: config.dataDir,
59795
+ logger,
59796
+ intervalMs: 6e4,
59797
+ sshLog
59798
+ });
59799
+ void contactKeyPuller.start();
58601
59800
  void reportDevice();
58602
59801
  void fetchServerKey();
58603
59802
  const tickAttachmentGc = () => {
@@ -58632,6 +59831,12 @@ ${bar}
58632
59831
  if (tunnelMgr) {
58633
59832
  await tunnelMgr.stop();
58634
59833
  }
59834
+ contactKeyPuller.stop();
59835
+ await sshdMgr.stop().catch((err) => {
59836
+ logger.warn("shutdown.sshd-stop-failed", {
59837
+ error: err instanceof Error ? err.message : String(err)
59838
+ });
59839
+ });
58635
59840
  await wss.stop();
58636
59841
  stateMgr.delete();
58637
59842
  if (logClient) await logClient.dispose();
@@ -58645,9 +59850,9 @@ ${bar}
58645
59850
  };
58646
59851
  }
58647
59852
  function migrateDropPersonsDir(dataDir) {
58648
- const dir = import_node_path55.default.join(dataDir, "persons");
59853
+ const dir = import_node_path62.default.join(dataDir, "persons");
58649
59854
  try {
58650
- import_node_fs42.default.rmSync(dir, { recursive: true, force: true });
59855
+ import_node_fs49.default.rmSync(dir, { recursive: true, force: true });
58651
59856
  } catch {
58652
59857
  }
58653
59858
  }
@@ -58660,6 +59865,11 @@ async function main() {
58660
59865
  const code = await runCase2(argv.slice(1));
58661
59866
  process.exit(code);
58662
59867
  }
59868
+ if (argv[0] === "ssh-relay") {
59869
+ const { sshRelay: sshRelay2 } = await Promise.resolve().then(() => (init_sshd_cli_relay(), sshd_cli_relay_exports));
59870
+ const code = await sshRelay2(argv.slice(1));
59871
+ process.exit(code);
59872
+ }
58663
59873
  const parsed = parseArgs(argv);
58664
59874
  if (parsed.help) {
58665
59875
  process.stdout.write(HELP_TEXT);