@clawos-dev/clawd 0.2.200 → 0.2.201-beta.403.73f7019

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
@@ -742,8 +742,8 @@ var init_parseUtil = __esm({
742
742
  init_errors2();
743
743
  init_en();
744
744
  makeIssue = (params) => {
745
- const { data, path: path73, errorMaps, issueData } = params;
746
- const fullPath = [...path73, ...issueData.path || []];
745
+ const { data, path: path76, errorMaps, issueData } = params;
746
+ const fullPath = [...path76, ...issueData.path || []];
747
747
  const fullIssue = {
748
748
  ...issueData,
749
749
  path: fullPath
@@ -1054,11 +1054,11 @@ var init_types = __esm({
1054
1054
  init_parseUtil();
1055
1055
  init_util();
1056
1056
  ParseInputLazyPath = class {
1057
- constructor(parent, value, path73, key) {
1057
+ constructor(parent, value, path76, key) {
1058
1058
  this._cachedPath = [];
1059
1059
  this.parent = parent;
1060
1060
  this.data = value;
1061
- this._path = path73;
1061
+ this._path = path76;
1062
1062
  this._key = key;
1063
1063
  }
1064
1064
  get path() {
@@ -6449,8 +6449,8 @@ var require_req = __commonJS({
6449
6449
  if (req.originalUrl) {
6450
6450
  _req.url = req.originalUrl;
6451
6451
  } else {
6452
- const path73 = req.path;
6453
- _req.url = typeof path73 === "string" ? path73 : 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;
6454
6454
  }
6455
6455
  if (req.query) {
6456
6456
  _req.query = req.query;
@@ -6615,14 +6615,14 @@ var require_redact = __commonJS({
6615
6615
  }
6616
6616
  return obj;
6617
6617
  }
6618
- function parsePath(path73) {
6618
+ function parsePath(path76) {
6619
6619
  const parts = [];
6620
6620
  let current = "";
6621
6621
  let inBrackets = false;
6622
6622
  let inQuotes = false;
6623
6623
  let quoteChar = "";
6624
- for (let i = 0; i < path73.length; i++) {
6625
- const char = path73[i];
6624
+ for (let i = 0; i < path76.length; i++) {
6625
+ const char = path76[i];
6626
6626
  if (!inBrackets && char === ".") {
6627
6627
  if (current) {
6628
6628
  parts.push(current);
@@ -6753,10 +6753,10 @@ var require_redact = __commonJS({
6753
6753
  return current;
6754
6754
  }
6755
6755
  function redactPaths(obj, paths, censor, remove = false) {
6756
- for (const path73 of paths) {
6757
- const parts = parsePath(path73);
6756
+ for (const path76 of paths) {
6757
+ const parts = parsePath(path76);
6758
6758
  if (parts.includes("*")) {
6759
- redactWildcardPath(obj, parts, censor, path73, remove);
6759
+ redactWildcardPath(obj, parts, censor, path76, remove);
6760
6760
  } else {
6761
6761
  if (remove) {
6762
6762
  removeKey(obj, parts);
@@ -6841,8 +6841,8 @@ var require_redact = __commonJS({
6841
6841
  }
6842
6842
  } else {
6843
6843
  if (afterWildcard.includes("*")) {
6844
- const wrappedCensor = typeof censor === "function" ? (value, path73) => {
6845
- const fullPath = [...pathArray.slice(0, pathLength), ...path73];
6844
+ const wrappedCensor = typeof censor === "function" ? (value, path76) => {
6845
+ const fullPath = [...pathArray.slice(0, pathLength), ...path76];
6846
6846
  return censor(value, fullPath);
6847
6847
  } : censor;
6848
6848
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6877,8 +6877,8 @@ var require_redact = __commonJS({
6877
6877
  return null;
6878
6878
  }
6879
6879
  const pathStructure = /* @__PURE__ */ new Map();
6880
- for (const path73 of pathsToClone) {
6881
- const parts = parsePath(path73);
6880
+ for (const path76 of pathsToClone) {
6881
+ const parts = parsePath(path76);
6882
6882
  let current = pathStructure;
6883
6883
  for (let i = 0; i < parts.length; i++) {
6884
6884
  const part = parts[i];
@@ -6930,24 +6930,24 @@ var require_redact = __commonJS({
6930
6930
  }
6931
6931
  return cloneSelectively(obj, pathStructure);
6932
6932
  }
6933
- function validatePath(path73) {
6934
- if (typeof path73 !== "string") {
6933
+ function validatePath(path76) {
6934
+ if (typeof path76 !== "string") {
6935
6935
  throw new Error("Paths must be (non-empty) strings");
6936
6936
  }
6937
- if (path73 === "") {
6937
+ if (path76 === "") {
6938
6938
  throw new Error("Invalid redaction path ()");
6939
6939
  }
6940
- if (path73.includes("..")) {
6941
- throw new Error(`Invalid redaction path (${path73})`);
6940
+ if (path76.includes("..")) {
6941
+ throw new Error(`Invalid redaction path (${path76})`);
6942
6942
  }
6943
- if (path73.includes(",")) {
6944
- throw new Error(`Invalid redaction path (${path73})`);
6943
+ if (path76.includes(",")) {
6944
+ throw new Error(`Invalid redaction path (${path76})`);
6945
6945
  }
6946
6946
  let bracketCount = 0;
6947
6947
  let inQuotes = false;
6948
6948
  let quoteChar = "";
6949
- for (let i = 0; i < path73.length; i++) {
6950
- const char = path73[i];
6949
+ for (let i = 0; i < path76.length; i++) {
6950
+ const char = path76[i];
6951
6951
  if ((char === '"' || char === "'") && bracketCount > 0) {
6952
6952
  if (!inQuotes) {
6953
6953
  inQuotes = true;
@@ -6961,20 +6961,20 @@ var require_redact = __commonJS({
6961
6961
  } else if (char === "]" && !inQuotes) {
6962
6962
  bracketCount--;
6963
6963
  if (bracketCount < 0) {
6964
- throw new Error(`Invalid redaction path (${path73})`);
6964
+ throw new Error(`Invalid redaction path (${path76})`);
6965
6965
  }
6966
6966
  }
6967
6967
  }
6968
6968
  if (bracketCount !== 0) {
6969
- throw new Error(`Invalid redaction path (${path73})`);
6969
+ throw new Error(`Invalid redaction path (${path76})`);
6970
6970
  }
6971
6971
  }
6972
6972
  function validatePaths(paths) {
6973
6973
  if (!Array.isArray(paths)) {
6974
6974
  throw new TypeError("paths must be an array");
6975
6975
  }
6976
- for (const path73 of paths) {
6977
- validatePath(path73);
6976
+ for (const path76 of paths) {
6977
+ validatePath(path76);
6978
6978
  }
6979
6979
  }
6980
6980
  function slowRedact(options = {}) {
@@ -7142,8 +7142,8 @@ var require_redaction = __commonJS({
7142
7142
  if (shape[k2] === null) {
7143
7143
  o[k2] = (value) => topCensor(value, [k2]);
7144
7144
  } else {
7145
- const wrappedCensor = typeof censor === "function" ? (value, path73) => {
7146
- return censor(value, [k2, ...path73]);
7145
+ const wrappedCensor = typeof censor === "function" ? (value, path76) => {
7146
+ return censor(value, [k2, ...path76]);
7147
7147
  } : censor;
7148
7148
  o[k2] = Redact({
7149
7149
  paths: shape[k2],
@@ -7361,10 +7361,10 @@ var require_atomic_sleep = __commonJS({
7361
7361
  var require_sonic_boom = __commonJS({
7362
7362
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7363
7363
  "use strict";
7364
- var fs66 = require("fs");
7364
+ var fs69 = require("fs");
7365
7365
  var EventEmitter3 = require("events");
7366
7366
  var inherits = require("util").inherits;
7367
- var path73 = require("path");
7367
+ var path76 = require("path");
7368
7368
  var sleep2 = require_atomic_sleep();
7369
7369
  var assert = require("assert");
7370
7370
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7418,20 +7418,20 @@ var require_sonic_boom = __commonJS({
7418
7418
  const mode = sonic.mode;
7419
7419
  if (sonic.sync) {
7420
7420
  try {
7421
- if (sonic.mkdir) fs66.mkdirSync(path73.dirname(file), { recursive: true });
7422
- const fd = fs66.openSync(file, flags, mode);
7421
+ if (sonic.mkdir) fs69.mkdirSync(path76.dirname(file), { recursive: true });
7422
+ const fd = fs69.openSync(file, flags, mode);
7423
7423
  fileOpened(null, fd);
7424
7424
  } catch (err) {
7425
7425
  fileOpened(err);
7426
7426
  throw err;
7427
7427
  }
7428
7428
  } else if (sonic.mkdir) {
7429
- fs66.mkdir(path73.dirname(file), { recursive: true }, (err) => {
7429
+ fs69.mkdir(path76.dirname(file), { recursive: true }, (err) => {
7430
7430
  if (err) return fileOpened(err);
7431
- fs66.open(file, flags, mode, fileOpened);
7431
+ fs69.open(file, flags, mode, fileOpened);
7432
7432
  });
7433
7433
  } else {
7434
- fs66.open(file, flags, mode, fileOpened);
7434
+ fs69.open(file, flags, mode, fileOpened);
7435
7435
  }
7436
7436
  }
7437
7437
  function SonicBoom(opts) {
@@ -7472,8 +7472,8 @@ var require_sonic_boom = __commonJS({
7472
7472
  this.flush = flushBuffer;
7473
7473
  this.flushSync = flushBufferSync;
7474
7474
  this._actualWrite = actualWriteBuffer;
7475
- fsWriteSync = () => fs66.writeSync(this.fd, this._writingBuf);
7476
- fsWrite = () => fs66.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);
7477
7477
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7478
7478
  this._writingBuf = "";
7479
7479
  this.write = write;
@@ -7482,15 +7482,15 @@ var require_sonic_boom = __commonJS({
7482
7482
  this._actualWrite = actualWrite;
7483
7483
  fsWriteSync = () => {
7484
7484
  if (Buffer.isBuffer(this._writingBuf)) {
7485
- return fs66.writeSync(this.fd, this._writingBuf);
7485
+ return fs69.writeSync(this.fd, this._writingBuf);
7486
7486
  }
7487
- return fs66.writeSync(this.fd, this._writingBuf, "utf8");
7487
+ return fs69.writeSync(this.fd, this._writingBuf, "utf8");
7488
7488
  };
7489
7489
  fsWrite = () => {
7490
7490
  if (Buffer.isBuffer(this._writingBuf)) {
7491
- return fs66.write(this.fd, this._writingBuf, this.release);
7491
+ return fs69.write(this.fd, this._writingBuf, this.release);
7492
7492
  }
7493
- return fs66.write(this.fd, this._writingBuf, "utf8", this.release);
7493
+ return fs69.write(this.fd, this._writingBuf, "utf8", this.release);
7494
7494
  };
7495
7495
  } else {
7496
7496
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7547,7 +7547,7 @@ var require_sonic_boom = __commonJS({
7547
7547
  }
7548
7548
  }
7549
7549
  if (this._fsync) {
7550
- fs66.fsyncSync(this.fd);
7550
+ fs69.fsyncSync(this.fd);
7551
7551
  }
7552
7552
  const len = this._len;
7553
7553
  if (this._reopening) {
@@ -7661,7 +7661,7 @@ var require_sonic_boom = __commonJS({
7661
7661
  const onDrain = () => {
7662
7662
  if (!this._fsync) {
7663
7663
  try {
7664
- fs66.fsync(this.fd, (err) => {
7664
+ fs69.fsync(this.fd, (err) => {
7665
7665
  this._flushPending = false;
7666
7666
  cb(err);
7667
7667
  });
@@ -7763,7 +7763,7 @@ var require_sonic_boom = __commonJS({
7763
7763
  const fd = this.fd;
7764
7764
  this.once("ready", () => {
7765
7765
  if (fd !== this.fd) {
7766
- fs66.close(fd, (err) => {
7766
+ fs69.close(fd, (err) => {
7767
7767
  if (err) {
7768
7768
  return this.emit("error", err);
7769
7769
  }
@@ -7812,7 +7812,7 @@ var require_sonic_boom = __commonJS({
7812
7812
  buf = this._bufs[0];
7813
7813
  }
7814
7814
  try {
7815
- const n = Buffer.isBuffer(buf) ? fs66.writeSync(this.fd, buf) : fs66.writeSync(this.fd, buf, "utf8");
7815
+ const n = Buffer.isBuffer(buf) ? fs69.writeSync(this.fd, buf) : fs69.writeSync(this.fd, buf, "utf8");
7816
7816
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7817
7817
  buf = releasedBufObj.writingBuf;
7818
7818
  this._len = releasedBufObj.len;
@@ -7828,7 +7828,7 @@ var require_sonic_boom = __commonJS({
7828
7828
  }
7829
7829
  }
7830
7830
  try {
7831
- fs66.fsyncSync(this.fd);
7831
+ fs69.fsyncSync(this.fd);
7832
7832
  } catch {
7833
7833
  }
7834
7834
  }
@@ -7849,7 +7849,7 @@ var require_sonic_boom = __commonJS({
7849
7849
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7850
7850
  }
7851
7851
  try {
7852
- const n = fs66.writeSync(this.fd, buf);
7852
+ const n = fs69.writeSync(this.fd, buf);
7853
7853
  buf = buf.subarray(n);
7854
7854
  this._len = Math.max(this._len - n, 0);
7855
7855
  if (buf.length <= 0) {
@@ -7877,13 +7877,13 @@ var require_sonic_boom = __commonJS({
7877
7877
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7878
7878
  if (this.sync) {
7879
7879
  try {
7880
- const written = Buffer.isBuffer(this._writingBuf) ? fs66.writeSync(this.fd, this._writingBuf) : fs66.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");
7881
7881
  release(null, written);
7882
7882
  } catch (err) {
7883
7883
  release(err);
7884
7884
  }
7885
7885
  } else {
7886
- fs66.write(this.fd, this._writingBuf, release);
7886
+ fs69.write(this.fd, this._writingBuf, release);
7887
7887
  }
7888
7888
  }
7889
7889
  function actualWriteBuffer() {
@@ -7892,7 +7892,7 @@ var require_sonic_boom = __commonJS({
7892
7892
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7893
7893
  if (this.sync) {
7894
7894
  try {
7895
- const written = fs66.writeSync(this.fd, this._writingBuf);
7895
+ const written = fs69.writeSync(this.fd, this._writingBuf);
7896
7896
  release(null, written);
7897
7897
  } catch (err) {
7898
7898
  release(err);
@@ -7901,7 +7901,7 @@ var require_sonic_boom = __commonJS({
7901
7901
  if (kCopyBuffer) {
7902
7902
  this._writingBuf = Buffer.from(this._writingBuf);
7903
7903
  }
7904
- fs66.write(this.fd, this._writingBuf, release);
7904
+ fs69.write(this.fd, this._writingBuf, release);
7905
7905
  }
7906
7906
  }
7907
7907
  function actualClose(sonic) {
@@ -7917,12 +7917,12 @@ var require_sonic_boom = __commonJS({
7917
7917
  sonic._lens = [];
7918
7918
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7919
7919
  try {
7920
- fs66.fsync(sonic.fd, closeWrapped);
7920
+ fs69.fsync(sonic.fd, closeWrapped);
7921
7921
  } catch {
7922
7922
  }
7923
7923
  function closeWrapped() {
7924
7924
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7925
- fs66.close(sonic.fd, done);
7925
+ fs69.close(sonic.fd, done);
7926
7926
  } else {
7927
7927
  done();
7928
7928
  }
@@ -10286,7 +10286,7 @@ var require_multistream = __commonJS({
10286
10286
  var require_pino = __commonJS({
10287
10287
  "../node_modules/.pnpm/pino@9.14.0/node_modules/pino/pino.js"(exports2, module2) {
10288
10288
  "use strict";
10289
- var os23 = require("os");
10289
+ var os24 = require("os");
10290
10290
  var stdSerializers = require_pino_std_serializers();
10291
10291
  var caller = require_caller();
10292
10292
  var redaction = require_redaction();
@@ -10333,7 +10333,7 @@ var require_pino = __commonJS({
10333
10333
  } = symbols;
10334
10334
  var { epochTime, nullTime } = time;
10335
10335
  var { pid } = process;
10336
- var hostname = os23.hostname();
10336
+ var hostname = os24.hostname();
10337
10337
  var defaultErrorSerializer = stdSerializers.err;
10338
10338
  var defaultOptions = {
10339
10339
  level: "info",
@@ -11057,11 +11057,11 @@ var init_lib = __esm({
11057
11057
  }
11058
11058
  }
11059
11059
  },
11060
- addToPath: function addToPath(path73, added, removed, oldPosInc, options) {
11061
- var last = path73.lastComponent;
11060
+ addToPath: function addToPath(path76, added, removed, oldPosInc, options) {
11061
+ var last = path76.lastComponent;
11062
11062
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
11063
11063
  return {
11064
- oldPos: path73.oldPos + oldPosInc,
11064
+ oldPos: path76.oldPos + oldPosInc,
11065
11065
  lastComponent: {
11066
11066
  count: last.count + 1,
11067
11067
  added,
@@ -11071,7 +11071,7 @@ var init_lib = __esm({
11071
11071
  };
11072
11072
  } else {
11073
11073
  return {
11074
- oldPos: path73.oldPos + oldPosInc,
11074
+ oldPos: path76.oldPos + oldPosInc,
11075
11075
  lastComponent: {
11076
11076
  count: 1,
11077
11077
  added,
@@ -11537,10 +11537,10 @@ function attachmentToHistoryMessage(o, ts) {
11537
11537
  const memories = raw.map((m2) => {
11538
11538
  if (!m2 || typeof m2 !== "object") return null;
11539
11539
  const rec3 = m2;
11540
- const path73 = typeof rec3.path === "string" ? rec3.path : null;
11540
+ const path76 = typeof rec3.path === "string" ? rec3.path : null;
11541
11541
  const content = typeof rec3.content === "string" ? rec3.content : null;
11542
- if (!path73 || content == null) return null;
11543
- const entry = { path: path73, content };
11542
+ if (!path76 || content == null) return null;
11543
+ const entry = { path: path76, content };
11544
11544
  if (typeof rec3.mtimeMs === "number") entry.mtimeMs = rec3.mtimeMs;
11545
11545
  return entry;
11546
11546
  }).filter((m2) => m2 !== null);
@@ -12004,6 +12004,14 @@ var init_claude_history = __esm({
12004
12004
  }
12005
12005
  });
12006
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
+
12007
12015
  // src/tools/claude.ts
12008
12016
  function probeViaWhich() {
12009
12017
  try {
@@ -12352,10 +12360,10 @@ function parseAttachment(obj) {
12352
12360
  const memories = raw.map((m2) => {
12353
12361
  if (!m2 || typeof m2 !== "object") return null;
12354
12362
  const rec3 = m2;
12355
- const path73 = typeof rec3.path === "string" ? rec3.path : null;
12363
+ const path76 = typeof rec3.path === "string" ? rec3.path : null;
12356
12364
  const content = typeof rec3.content === "string" ? rec3.content : null;
12357
- if (!path73 || content == null) return null;
12358
- const out = { path: path73, content };
12365
+ if (!path76 || content == null) return null;
12366
+ const out = { path: path76, content };
12359
12367
  if (typeof rec3.mtimeMs === "number") out.mtimeMs = rec3.mtimeMs;
12360
12368
  return out;
12361
12369
  }).filter((m2) => m2 !== null);
@@ -26827,6 +26835,121 @@ var require_dist = __commonJS({
26827
26835
  }
26828
26836
  });
26829
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
+
26830
26953
  // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js
26831
26954
  var require_constants2 = __commonJS({
26832
26955
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/constants.js"(exports2, module2) {
@@ -29028,9 +29151,9 @@ var require_websocket = __commonJS({
29028
29151
  var EventEmitter3 = require("events");
29029
29152
  var https = require("https");
29030
29153
  var http3 = require("http");
29031
- var net3 = require("net");
29154
+ var net4 = require("net");
29032
29155
  var tls = require("tls");
29033
- var { randomBytes, createHash: createHash3 } = require("crypto");
29156
+ var { randomBytes, createHash: createHash2 } = require("crypto");
29034
29157
  var { Duplex, Readable: Readable3 } = require("stream");
29035
29158
  var { URL: URL2 } = require("url");
29036
29159
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -29690,7 +29813,7 @@ var require_websocket = __commonJS({
29690
29813
  abortHandshake(websocket, socket, "Invalid Upgrade header");
29691
29814
  return;
29692
29815
  }
29693
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
29816
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
29694
29817
  if (res.headers["sec-websocket-accept"] !== digest) {
29695
29818
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
29696
29819
  return;
@@ -29762,12 +29885,12 @@ var require_websocket = __commonJS({
29762
29885
  }
29763
29886
  function netConnect(options) {
29764
29887
  options.path = options.socketPath;
29765
- return net3.connect(options);
29888
+ return net4.connect(options);
29766
29889
  }
29767
29890
  function tlsConnect(options) {
29768
29891
  options.path = void 0;
29769
29892
  if (!options.servername && options.servername !== "") {
29770
- options.servername = net3.isIP(options.host) ? "" : options.host;
29893
+ options.servername = net4.isIP(options.host) ? "" : options.host;
29771
29894
  }
29772
29895
  return tls.connect(options);
29773
29896
  }
@@ -30057,7 +30180,7 @@ var require_websocket_server = __commonJS({
30057
30180
  var EventEmitter3 = require("events");
30058
30181
  var http3 = require("http");
30059
30182
  var { Duplex } = require("stream");
30060
- var { createHash: createHash3 } = require("crypto");
30183
+ var { createHash: createHash2 } = require("crypto");
30061
30184
  var extension2 = require_extension();
30062
30185
  var PerMessageDeflate2 = require_permessage_deflate();
30063
30186
  var subprotocol2 = require_subprotocol();
@@ -30358,7 +30481,7 @@ var require_websocket_server = __commonJS({
30358
30481
  );
30359
30482
  }
30360
30483
  if (this._state > RUNNING) return abortHandshake(socket, 503);
30361
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
30484
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
30362
30485
  const headers = [
30363
30486
  "HTTP/1.1 101 Switching Protocols",
30364
30487
  "Upgrade: websocket",
@@ -30443,6 +30566,23 @@ var require_websocket_server = __commonJS({
30443
30566
  }
30444
30567
  });
30445
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
+
30446
30586
  // ../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js
30447
30587
  var require_process_nextick_args = __commonJS({
30448
30588
  "../node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js"(exports2, module2) {
@@ -33320,8 +33460,8 @@ var require_utils = __commonJS({
33320
33460
  var result = transform[inputType][outputType](input);
33321
33461
  return result;
33322
33462
  };
33323
- exports2.resolve = function(path73) {
33324
- var parts = path73.split("/");
33463
+ exports2.resolve = function(path76) {
33464
+ var parts = path76.split("/");
33325
33465
  var result = [];
33326
33466
  for (var index = 0; index < parts.length; index++) {
33327
33467
  var part = parts[index];
@@ -39174,18 +39314,18 @@ var require_object = __commonJS({
39174
39314
  var object = new ZipObject(name, zipObjectContent, o);
39175
39315
  this.files[name] = object;
39176
39316
  };
39177
- var parentFolder = function(path73) {
39178
- if (path73.slice(-1) === "/") {
39179
- path73 = path73.substring(0, path73.length - 1);
39317
+ var parentFolder = function(path76) {
39318
+ if (path76.slice(-1) === "/") {
39319
+ path76 = path76.substring(0, path76.length - 1);
39180
39320
  }
39181
- var lastSlash = path73.lastIndexOf("/");
39182
- return lastSlash > 0 ? path73.substring(0, lastSlash) : "";
39321
+ var lastSlash = path76.lastIndexOf("/");
39322
+ return lastSlash > 0 ? path76.substring(0, lastSlash) : "";
39183
39323
  };
39184
- var forceTrailingSlash = function(path73) {
39185
- if (path73.slice(-1) !== "/") {
39186
- path73 += "/";
39324
+ var forceTrailingSlash = function(path76) {
39325
+ if (path76.slice(-1) !== "/") {
39326
+ path76 += "/";
39187
39327
  }
39188
- return path73;
39328
+ return path76;
39189
39329
  };
39190
39330
  var folderAdd = function(name, createFolders) {
39191
39331
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -40187,7 +40327,7 @@ var require_lib3 = __commonJS({
40187
40327
  // src/run-case/recorder.ts
40188
40328
  function startRunCaseRecorder(opts) {
40189
40329
  const now = opts.now ?? Date.now;
40190
- const dir = import_node_path61.default.dirname(opts.recordPath);
40330
+ const dir = import_node_path63.default.dirname(opts.recordPath);
40191
40331
  let stream = null;
40192
40332
  let closing = false;
40193
40333
  let closedSettled = false;
@@ -40201,8 +40341,8 @@ function startRunCaseRecorder(opts) {
40201
40341
  });
40202
40342
  const ensureStream = () => {
40203
40343
  if (stream) return stream;
40204
- import_node_fs48.default.mkdirSync(dir, { recursive: true });
40205
- stream = import_node_fs48.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" });
40206
40346
  stream.on("close", () => closedResolve());
40207
40347
  return stream;
40208
40348
  };
@@ -40227,12 +40367,12 @@ function startRunCaseRecorder(opts) {
40227
40367
  };
40228
40368
  return { tap, close, closed };
40229
40369
  }
40230
- var import_node_fs48, import_node_path61;
40370
+ var import_node_fs50, import_node_path63;
40231
40371
  var init_recorder = __esm({
40232
40372
  "src/run-case/recorder.ts"() {
40233
40373
  "use strict";
40234
- import_node_fs48 = __toESM(require("fs"), 1);
40235
- import_node_path61 = __toESM(require("path"), 1);
40374
+ import_node_fs50 = __toESM(require("fs"), 1);
40375
+ import_node_path63 = __toESM(require("path"), 1);
40236
40376
  }
40237
40377
  });
40238
40378
 
@@ -40275,7 +40415,7 @@ var init_wire = __esm({
40275
40415
  // src/run-case/controller.ts
40276
40416
  async function runController(opts) {
40277
40417
  const now = opts.now ?? Date.now;
40278
- const cwd = opts.cwd ?? (0, import_node_fs49.mkdtempSync)(import_node_path62.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-"));
40279
40419
  const ownsCwd = opts.cwd === void 0;
40280
40420
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
40281
40421
  const spawnCtx = { cwd };
@@ -40436,19 +40576,19 @@ async function runController(opts) {
40436
40576
  if (sigintHandler) process.off("SIGINT", sigintHandler);
40437
40577
  if (ownsCwd) {
40438
40578
  try {
40439
- (0, import_node_fs49.rmSync)(cwd, { recursive: true, force: true });
40579
+ (0, import_node_fs51.rmSync)(cwd, { recursive: true, force: true });
40440
40580
  } catch {
40441
40581
  }
40442
40582
  }
40443
40583
  return exitCode ?? 0;
40444
40584
  }
40445
- var import_node_fs49, import_node_os22, import_node_path62;
40585
+ var import_node_fs51, import_node_os22, import_node_path64;
40446
40586
  var init_controller = __esm({
40447
40587
  "src/run-case/controller.ts"() {
40448
40588
  "use strict";
40449
- import_node_fs49 = require("fs");
40589
+ import_node_fs51 = require("fs");
40450
40590
  import_node_os22 = __toESM(require("os"), 1);
40451
- import_node_path62 = __toESM(require("path"), 1);
40591
+ import_node_path64 = __toESM(require("path"), 1);
40452
40592
  init_claude();
40453
40593
  init_stdout_splitter();
40454
40594
  init_permission_stdio();
@@ -40544,6 +40684,163 @@ stdout \u4E8B\u4EF6\uFF08\u884C JSON\uFF09\uFF1A
40544
40684
  }
40545
40685
  });
40546
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
+
40547
40844
  // src/config.ts
40548
40845
  var import_node_fs = __toESM(require("fs"), 1);
40549
40846
  var import_node_os = __toESM(require("os"), 1);
@@ -40712,6 +41009,10 @@ Subcommands:
40712
41009
  \u72EC\u7ACB CC \u63A7\u5236\u5668\u8FDB\u7A0B\uFF1A\u5916\u90E8 AI \u901A\u8FC7 stdin/stdout \u884C JSON \u534F\u8BAE
40713
41010
  \u9A71\u52A8 CC \u591A\u8F6E\u5BF9\u8BDD\u4E0E\u6743\u9650\u51B3\u7B56\uFF0CIPC \u5168\u7A0B\u5F55\u5230 --record JSONL\u3002
40714
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'
40715
41016
 
40716
41017
  Env (advanced):
40717
41018
  CLAWD_FRPC_BIN \u81EA\u5E26 frpc \u4E8C\u8FDB\u5236\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u6309\u9700\u4E0B\u8F7D\u5230 ~/.clawd/bin/frpc\uFF09
@@ -40722,8 +41023,8 @@ Env (advanced):
40722
41023
  `;
40723
41024
 
40724
41025
  // src/index.ts
40725
- var import_node_path60 = __toESM(require("path"), 1);
40726
- var import_node_fs47 = __toESM(require("fs"), 1);
41026
+ var import_node_path62 = __toESM(require("path"), 1);
41027
+ var import_node_fs49 = __toESM(require("fs"), 1);
40727
41028
  var import_node_os21 = __toESM(require("os"), 1);
40728
41029
 
40729
41030
  // ../node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/stringify.js
@@ -40843,18 +41144,6 @@ function createLogger(opts = {}) {
40843
41144
  );
40844
41145
  return wrap(base);
40845
41146
  }
40846
- function createFileOnlyLogger(opts) {
40847
- const level = opts.level ?? "debug";
40848
- try {
40849
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(opts.file), { recursive: true });
40850
- } catch {
40851
- }
40852
- const base = (0, import_pino.default)(
40853
- { level, base: {} },
40854
- import_pino.default.destination({ dest: opts.file, mkdir: true, sync: true })
40855
- );
40856
- return wrap(base);
40857
- }
40858
41147
  function pinoLevelToString(n) {
40859
41148
  if (typeof n !== "number") return null;
40860
41149
  if (n >= 50) return "error";
@@ -41434,6 +41723,7 @@ function composeGuestSandbox(base, userWorkDir, spawnCwd) {
41434
41723
  fsv.denyWrite = unionArr((fsv.denyWrite ?? []).filter((p2) => p2 !== "~/"), [spawnCwd]);
41435
41724
  fsv.allowRead = unionArr(fsv.allowRead, [userWorkDir]);
41436
41725
  fsv.allowWrite = unionArr(fsv.allowWrite, [userWorkDir]);
41726
+ fsv.allowRead = unionArr(fsv.allowRead, ["~/.clawd/contact-ssh-keys"]);
41437
41727
  return s;
41438
41728
  }
41439
41729
 
@@ -41574,6 +41864,31 @@ deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731
41574
41864
  - \u5931\u8D25\uFF1A\`{ isFailure: true, reason: "..." }\`
41575
41865
  - \u62A5\u544A\u5B8C\u540E\u4F60\u7684 session \u5C31\u7ED3\u675F\u4E86\uFF0C\u4E0D\u8981\u7EE7\u7EED\u8F93\u51FA`;
41576
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
+
41577
41892
  // src/session/reducer.ts
41578
41893
  function cloneState(s) {
41579
41894
  return {
@@ -41710,6 +42025,9 @@ function buildSpawnContext(state, deps) {
41710
42025
  if (daemonUrl) {
41711
42026
  ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + ATTACHMENT_SHARING_HINT;
41712
42027
  }
42028
+ if (meta?.personaMode === "guest") {
42029
+ ctx.extraSystemPrompt = (ctx.extraSystemPrompt ? ctx.extraSystemPrompt + "\n\n" : "") + CONTACT_SSH_SYSTEM_PROMPT_HINT;
42030
+ }
41713
42031
  if (meta?.extraSettings) {
41714
42032
  ctx.extraSettings = meta.extraSettings;
41715
42033
  }
@@ -43020,18 +43338,9 @@ var SessionManager = class {
43020
43338
  // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
43021
43339
  // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
43022
43340
  realUuidBySynth = /* @__PURE__ */ new Map();
43023
- // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s 挂进 pending,等 `notifyScreenIdle`
43024
- // (屏幕 armed=true 触发点)flush 成真正的 turn_end reducer。
43025
- //
43026
- // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
43027
- //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
43028
- // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
43029
- //
43030
- // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
43031
- // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
43032
- //
43033
- // 清理点:newSession / stop / session-delete / stopAll。
43034
- pendingTurnDurationSignals = /* @__PURE__ */ new Set();
43341
+ // observeScreenIdle 复合条件闸用:sessionIdobserver 上次喂出业务事件的时刻(deps.now())。
43342
+ // observerIdleWaitMs 据此判 observer 是否也静止 idleMs(屏幕静止 AND observer 静止才补 turn_end)。
43343
+ lastObserverEventAt = /* @__PURE__ */ new Map();
43035
43344
  // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
43036
43345
  // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
43037
43346
  // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
@@ -43371,14 +43680,6 @@ var SessionManager = class {
43371
43680
  routeFromRunner(frame, target) {
43372
43681
  const compressed = compressFrameForWire(frame);
43373
43682
  if (!compressed) return;
43374
- if (compressed.type === "session:status") {
43375
- const s = compressed;
43376
- this.deps.screenIdleProbeLogger?.info("session:status wire emit", {
43377
- sessionId: s.sessionId,
43378
- status: s.status,
43379
- target
43380
- });
43381
- }
43382
43683
  if (compressed.type === "session:event" || compressed.type === "session:status") {
43383
43684
  const sid = compressed.sessionId;
43384
43685
  if (sid) {
@@ -43652,7 +43953,7 @@ var SessionManager = class {
43652
43953
  this.runners.delete(args.sessionId);
43653
43954
  this.realUuidBySynth.delete(args.sessionId);
43654
43955
  this.lastUiSizeBySessionId.delete(args.sessionId);
43655
- this.clearPendingTurnEnd(args.sessionId);
43956
+ this.lastObserverEventAt.delete(args.sessionId);
43656
43957
  return { response: { sessionId: args.sessionId }, broadcast };
43657
43958
  }
43658
43959
  this.deleteOwned(args.sessionId);
@@ -43682,7 +43983,6 @@ var SessionManager = class {
43682
43983
  async stop(args) {
43683
43984
  const runner = this.runners.get(args.sessionId);
43684
43985
  if (!runner) return { response: { ok: true }, broadcast: [] };
43685
- this.clearPendingTurnEnd(args.sessionId);
43686
43986
  const { broadcast } = this.withCollector(() => {
43687
43987
  runner.input({ kind: "command", command: { kind: "stop" } });
43688
43988
  });
@@ -43816,7 +44116,6 @@ var SessionManager = class {
43816
44116
  newSession(args) {
43817
44117
  const existingFile = this.getFile(args.sessionId);
43818
44118
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? v4_default() : void 0;
43819
- this.clearPendingTurnEnd(args.sessionId);
43820
44119
  const runner = this.runners.get(args.sessionId);
43821
44120
  if (runner) {
43822
44121
  const { value, broadcast } = this.withCollector(() => {
@@ -43917,7 +44216,6 @@ var SessionManager = class {
43917
44216
  for (const r of this.runners.values()) {
43918
44217
  r.input({ kind: "command", command: { kind: "stop" } });
43919
44218
  }
43920
- this.pendingTurnDurationSignals.clear();
43921
44219
  }
43922
44220
  // 给 observer 用:拿已存在的 runner
43923
44221
  getActive(sessionId) {
@@ -44134,7 +44432,7 @@ var SessionManager = class {
44134
44432
  this.runners.delete(args.sessionId);
44135
44433
  this.realUuidBySynth.delete(args.sessionId);
44136
44434
  this.lastUiSizeBySessionId.delete(args.sessionId);
44137
- this.clearPendingTurnEnd(args.sessionId);
44435
+ this.lastObserverEventAt.delete(args.sessionId);
44138
44436
  return { response: { sessionId: args.sessionId }, broadcast };
44139
44437
  }
44140
44438
  this.storeFor(args.scope).delete(args.sessionId);
@@ -44380,93 +44678,23 @@ var SessionManager = class {
44380
44678
  return;
44381
44679
  }
44382
44680
  }
44681
+ this.lastObserverEventAt.set(sessionId, (this.deps.now ?? Date.now)());
44383
44682
  let feedEvents = outEvents;
44384
44683
  if (outEvents.some((e) => e.kind === "turn_end")) {
44385
- const runnerState = runner.getState();
44386
- const toolSessionId = runnerState.file.toolSessionId;
44387
- const adapter = this.deps.getAdapter(runnerState.file.tool ?? "claude");
44388
- const gateOpen = !adapter.canAcceptTurnEnd || !toolSessionId ? true : adapter.canAcceptTurnEnd(toolSessionId);
44389
- this.deps.screenIdleProbeLogger?.info("turn_duration signal received", {
44390
- sessionId,
44391
- toolSessionId,
44392
- batchKinds: outEvents.map((e) => e.kind),
44393
- gateOpen,
44394
- hasCanAcceptGate: !!adapter.canAcceptTurnEnd
44395
- });
44396
- if (gateOpen) {
44397
- this.deps.screenIdleProbeLogger?.info(
44398
- "turn_duration \u2192 turn_end confirmed (gate open) \u2192 fed to reducer",
44399
- { sessionId, toolSessionId }
44400
- );
44401
- } else {
44684
+ const ev = this.peekTurnEvidence(runner);
44685
+ if (!ev.turnHasContent) {
44402
44686
  feedEvents = outEvents.filter((e) => e.kind !== "turn_end");
44403
- if (this.pendingTurnDurationSignals.has(sessionId)) {
44404
- this.deps.screenIdleProbeLogger?.info(
44405
- "turn_duration dedup: pending already set (repeated observer signal)",
44406
- { sessionId, toolSessionId }
44407
- );
44408
- } else {
44409
- this.pendingTurnDurationSignals.add(sessionId);
44410
- this.deps.screenIdleProbeLogger?.info(
44411
- "turn_duration pending: gate closed \u2192 waiting for screen-idle signal",
44412
- { sessionId, toolSessionId }
44413
- );
44414
- }
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
+ });
44415
44693
  }
44416
44694
  }
44417
44695
  if (feedEvents.length === 0) return;
44418
44696
  runner.feedObserverEvents(feedEvents);
44419
44697
  }
44420
- /**
44421
- * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
44422
- *
44423
- * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
44424
- * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
44425
- * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
44426
- *
44427
- * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
44428
- * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
44429
- * turn_end" 语义不同。
44430
- *
44431
- * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
44432
- */
44433
- notifyScreenIdle(toolSessionId) {
44434
- if (this.deps.mode !== "tui") return;
44435
- const sid = this.sessionIdByToolSid(toolSessionId);
44436
- if (!sid) {
44437
- this.deps.screenIdleProbeLogger?.warn("notifyScreenIdle: no session for toolSessionId", {
44438
- toolSessionId
44439
- });
44440
- return;
44441
- }
44442
- if (!this.pendingTurnDurationSignals.has(sid)) {
44443
- this.deps.screenIdleProbeLogger?.info(
44444
- "notifyScreenIdle: no pending turn_duration \u2192 noop",
44445
- { sessionId: sid, toolSessionId }
44446
- );
44447
- return;
44448
- }
44449
- const runner = this.runners.get(sid);
44450
- if (!runner) {
44451
- this.pendingTurnDurationSignals.delete(sid);
44452
- this.deps.screenIdleProbeLogger?.warn(
44453
- "notifyScreenIdle: pending but no runner \u2192 cleared without inject",
44454
- { sessionId: sid, toolSessionId }
44455
- );
44456
- return;
44457
- }
44458
- this.pendingTurnDurationSignals.delete(sid);
44459
- this.deps.screenIdleProbeLogger?.info(
44460
- "notifyScreenIdle: pending turn_duration + screen idle confirmed \u2192 inject turn_end",
44461
- { sessionId: sid, toolSessionId }
44462
- );
44463
- runner.input({ kind: "inject-events", events: [{ kind: "turn_end" }] });
44464
- }
44465
- clearPendingTurnEnd(sessionId) {
44466
- if (this.pendingTurnDurationSignals.delete(sessionId)) {
44467
- this.deps.screenIdleProbeLogger?.info("pending turn_duration cleared", { sessionId });
44468
- }
44469
- }
44470
44698
  // AskUserQuestion 表单回写(plan: clawd-ask-user-question):UI 答完所有 question 后调用。
44471
44699
  // - session 不存在 / 无 runner → noop 幂等返回 ok(first-decider-wins)
44472
44700
  // - reducer noop(toolUseId 不存在或已答过)也保持幂等返回,handler 不抛
@@ -44725,6 +44953,70 @@ var SessionManager = class {
44725
44953
  if (!runner) return;
44726
44954
  runner.input({ kind: "ready-detected" });
44727
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
+ }
44728
45020
  /** toolSessionId → sessionId 反查(遍历 runners);session 数典型 < 10,O(n) 可接受 */
44729
45021
  sessionIdByToolSid(toolSessionId) {
44730
45022
  for (const [sid, runner] of this.runners) {
@@ -45873,11 +46165,7 @@ function tryLoadShareUi(logger) {
45873
46165
 
45874
46166
  // src/visitor/visitor-token.ts
45875
46167
  var import_node_crypto4 = __toESM(require("crypto"), 1);
45876
-
45877
- // ../protocol/src/index.ts
45878
- init_runtime();
45879
-
45880
- // src/visitor/visitor-token.ts
46168
+ init_src();
45881
46169
  function hmac(secret, body) {
45882
46170
  return import_node_crypto4.default.createHmac("sha256", secret).update(body).digest("base64url");
45883
46171
  }
@@ -46234,8 +46522,8 @@ function turnStartInput(text) {
46234
46522
  const items = [];
46235
46523
  let leftover = text;
46236
46524
  for (const m2 of text.matchAll(SKILL_RE)) {
46237
- const [marker, name, path73] = m2;
46238
- items.push({ type: "skill", name, path: path73 });
46525
+ const [marker, name, path76] = m2;
46526
+ items.push({ type: "skill", name, path: path76 });
46239
46527
  leftover = leftover.replace(marker, "");
46240
46528
  }
46241
46529
  for (const m2 of text.matchAll(ATTACHMENT_RE2)) {
@@ -46467,7 +46755,6 @@ var CodexAdapter = class {
46467
46755
  };
46468
46756
 
46469
46757
  // src/tools/claude-tui.ts
46470
- var import_node_crypto5 = require("crypto");
46471
46758
  var import_node_fs16 = __toESM(require("fs"), 1);
46472
46759
  var import_node_os7 = __toESM(require("os"), 1);
46473
46760
  var import_node_path14 = __toESM(require("path"), 1);
@@ -47278,56 +47565,22 @@ function observeScreenIdle(surface, opts) {
47278
47565
  timer = null;
47279
47566
  if (disposed) return;
47280
47567
  if (opts.getPopupVisible()) {
47281
- opts.probeLogger?.info("screen-idle fire suppressed: popup visible", {
47282
- label: opts.probeLabel
47283
- });
47284
47568
  timer = setTimeout(fire, opts.idleMs);
47285
47569
  return;
47286
47570
  }
47287
47571
  const obsWait = opts.getObserverWaitMs?.() ?? 0;
47288
47572
  if (obsWait > 0) {
47289
- opts.probeLogger?.info("screen-idle fire suppressed: observer not idle", {
47290
- label: opts.probeLabel,
47291
- obsWait
47292
- });
47293
47573
  timer = setTimeout(fire, Math.max(obsWait, REWAIT_MIN_MS));
47294
47574
  return;
47295
47575
  }
47296
- if (armed) {
47297
- opts.probeLogger?.debug("screen-idle fire noop: already armed", {
47298
- label: opts.probeLabel
47299
- });
47300
- return;
47301
- }
47576
+ if (armed) return;
47302
47577
  armed = true;
47303
- opts.probeLogger?.info("screen-idle fire triggered \u2192 armed=true, calling onIdle", {
47304
- label: opts.probeLabel
47305
- });
47306
47578
  opts.onIdle();
47307
47579
  };
47308
47580
  const unsub = surface.onTick((lines) => {
47309
47581
  if (disposed) return;
47310
47582
  const snap = snapOf(lines);
47311
47583
  if (snap === lastSnap) return;
47312
- if (opts.probeLogger) {
47313
- const prev = lastSnap;
47314
- const meta = {
47315
- label: opts.probeLabel,
47316
- prevHash: prev === null ? null : shortHash(prev),
47317
- nextHash: shortHash(snap),
47318
- prevLen: prev?.length ?? 0,
47319
- nextLen: snap.length
47320
- };
47321
- if (prev !== null) {
47322
- const diff2 = firstLineDiff(prev, snap);
47323
- if (diff2) {
47324
- meta.diffRow = diff2.row;
47325
- meta.prevRow = diff2.prev;
47326
- meta.nextRow = diff2.next;
47327
- }
47328
- }
47329
- opts.probeLogger.info("screen-idle tick snap changed", meta);
47330
- }
47331
47584
  lastSnap = snap;
47332
47585
  armed = false;
47333
47586
  clear();
@@ -47338,38 +47591,9 @@ function observeScreenIdle(surface, opts) {
47338
47591
  disposed = true;
47339
47592
  unsub();
47340
47593
  clear();
47341
- },
47342
- isIdle() {
47343
- const popupVisible = opts.getPopupVisible();
47344
- const idle = armed && !popupVisible;
47345
- if (opts.probeLogger) {
47346
- opts.probeLogger.info("screen-idle isIdle check", {
47347
- label: opts.probeLabel,
47348
- idle,
47349
- armed,
47350
- popupVisible
47351
- });
47352
- }
47353
- return idle;
47354
47594
  }
47355
47595
  };
47356
47596
  }
47357
- function shortHash(s) {
47358
- return (0, import_node_crypto5.createHash)("sha1").update(s).digest("hex").slice(0, 8);
47359
- }
47360
- function firstLineDiff(prev, next) {
47361
- const p2 = prev.split("\n");
47362
- const n = next.split("\n");
47363
- const rows = Math.max(p2.length, n.length);
47364
- for (let i = 0; i < rows; i++) {
47365
- const pl = p2[i] ?? "";
47366
- const nl = n[i] ?? "";
47367
- if (pl !== nl) {
47368
- return { row: i, prev: pl.slice(0, 60), next: nl.slice(0, 60) };
47369
- }
47370
- }
47371
- return null;
47372
- }
47373
47597
  var BYPASS_SETTLE_MS = 300;
47374
47598
  var SCREEN_IDLE_MS = 5e3;
47375
47599
  function createBootGate(pty, logger) {
@@ -47444,42 +47668,11 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47444
47668
  // 用于 spawn / PtyChildProcess 链路打日志
47445
47669
  tuiLogger;
47446
47670
  tuiOpts;
47447
- /**
47448
- * per-toolSessionId 的 tui 观察者句柄,仅用于 turn_end gate 查询(`canAcceptTurnEnd`)。
47449
- * onIdle / onPopupTransition 等回调仍走原有闭包(不复用这份 map),本 map 只承担
47450
- * "manager 需要跨模块查屏幕/弹框状态"这单一职责。
47451
- */
47452
- tuiStates = /* @__PURE__ */ new Map();
47453
47671
  constructor(opts = {}) {
47454
47672
  super(opts);
47455
47673
  this.tuiLogger = opts.logger;
47456
47674
  this.tuiOpts = opts;
47457
47675
  }
47458
- /**
47459
- * TUI adapter 的 turn_end 权威判定:屏幕已 idle 且非弹框态才放行。
47460
- *
47461
- * `feedObserverEvents` 收到 observer 回灌 `turn_end` 时调用。屏幕仍在变(如后台 agent 在跑)
47462
- * 时 drop 掉 turn_end,避免 `system/turn_duration` JSONL 帧误触发 running-idle 状态转换。
47463
- *
47464
- * 未跟踪的 toolSessionId(spawn 前 / spawn 失败 / 已 dispose)视为 pass —— gate 只 drop
47465
- * "有证据判定为伪信号"的场景,不做 unknown → block。
47466
- */
47467
- canAcceptTurnEnd(toolSessionId) {
47468
- const state = this.tuiStates.get(toolSessionId);
47469
- if (!state) {
47470
- this.tuiOpts.screenIdleProbeLogger?.info(
47471
- "canAcceptTurnEnd: no tuiState \u2192 pass (\u672A\u8DDF\u8E2A)",
47472
- { toolSessionId }
47473
- );
47474
- return true;
47475
- }
47476
- const result = state.screenIdle.isIdle();
47477
- this.tuiOpts.screenIdleProbeLogger?.info("canAcceptTurnEnd", {
47478
- toolSessionId,
47479
- result
47480
- });
47481
- return result;
47482
- }
47483
47676
  spawn(ctx) {
47484
47677
  const args = buildTuiSpawnArgs(ctx, jsonlExistsForCtx(ctx));
47485
47678
  const cmd = process.env.CLAUDE_BIN ?? "claude";
@@ -47537,26 +47730,18 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47537
47730
  const screenIdleObserver = observeScreenIdle(surface, {
47538
47731
  idleMs: SCREEN_IDLE_MS,
47539
47732
  onIdle: () => {
47540
- if (!ctx.toolSessionId || !this.tuiOpts.onScreenIdle) return;
47541
- this.tuiLogger?.debug("screen-idle \u2192 notifyScreenIdle", { toolSessionId: ctx.toolSessionId });
47542
- 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);
47543
47736
  },
47544
47737
  getPopupVisible: () => popupObserver.visibleKind !== null,
47545
- // 取证 probe(可选,装配处传独立 file-only logger,跟主 daemon.log 解耦)
47546
- ...this.tuiOpts.screenIdleProbeLogger ? {
47547
- probeLogger: this.tuiOpts.screenIdleProbeLogger,
47548
- probeLabel: ctx.toolSessionId ?? "<no-tsid>"
47549
- } : {}
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
47550
47741
  });
47551
47742
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceRegister) {
47552
47743
  this.tuiOpts.onSurfaceRegister(ctx.toolSessionId, surface);
47553
47744
  }
47554
- if (ctx.toolSessionId) {
47555
- this.tuiStates.set(ctx.toolSessionId, {
47556
- screenIdle: screenIdleObserver,
47557
- popup: popupObserver
47558
- });
47559
- }
47560
47745
  let chunkSeq = 0;
47561
47746
  if (ctx.toolSessionId && this.tuiOpts.onPtyReplayRegister) {
47562
47747
  this.tuiOpts.onPtyReplayRegister(ctx.toolSessionId, async () => {
@@ -47602,9 +47787,6 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
47602
47787
  readyObserver.dispose();
47603
47788
  popupObserver.dispose();
47604
47789
  screenIdleObserver.dispose();
47605
- if (ctx.toolSessionId) {
47606
- this.tuiStates.delete(ctx.toolSessionId);
47607
- }
47608
47790
  if (ctx.toolSessionId && this.tuiOpts.onSurfaceUnregister) {
47609
47791
  this.tuiOpts.onSurfaceUnregister(ctx.toolSessionId);
47610
47792
  }
@@ -47887,7 +48069,7 @@ async function writeInboxMcpConfig(args) {
47887
48069
  // src/shift/store.ts
47888
48070
  var import_promises = __toESM(require("fs/promises"), 1);
47889
48071
  var import_node_path19 = __toESM(require("path"), 1);
47890
- var import_node_crypto6 = require("crypto");
48072
+ var import_node_crypto5 = require("crypto");
47891
48073
 
47892
48074
  // src/shift/constants.ts
47893
48075
  var MAX_RUNS_PER_SHIFT = 30;
@@ -47983,7 +48165,7 @@ function createShiftStore(deps) {
47983
48165
  const nextRunAtMs = computeNextRunAtMs(input.schedule, now) ?? void 0;
47984
48166
  const shift = {
47985
48167
  ...input,
47986
- id: (0, import_node_crypto6.randomUUID)(),
48168
+ id: (0, import_node_crypto5.randomUUID)(),
47987
48169
  createdAtMs: now,
47988
48170
  updatedAtMs: now,
47989
48171
  state: { nextRunAtMs },
@@ -48500,78 +48682,8 @@ function buildPersonaDispatchHandlers(deps) {
48500
48682
  };
48501
48683
  }
48502
48684
 
48503
- // src/dispatch/peer-forward.ts
48504
- function wsUrlToHttp(url) {
48505
- if (url.startsWith("wss://")) return "https://" + url.slice("wss://".length);
48506
- if (url.startsWith("ws://")) return "http://" + url.slice("ws://".length);
48507
- return url;
48508
- }
48509
- async function forwardDispatchToPeer(args) {
48510
- const f = args.fetchImpl ?? fetch;
48511
- const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
48512
- const url = `${base}/rpc/personaDispatch:run`;
48513
- let res;
48514
- try {
48515
- res = await f(url, {
48516
- method: "POST",
48517
- headers: {
48518
- "content-type": "application/json",
48519
- authorization: `Bearer ${args.contact.connectToken}`
48520
- },
48521
- // 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
48522
- body: JSON.stringify({ targetPersona: args.targetPersona, prompt: args.prompt })
48523
- });
48524
- } catch (err) {
48525
- const msg = err instanceof Error ? err.message : String(err);
48526
- return { kind: "failure", reason: `forward to peer failed: ${msg}` };
48527
- }
48528
- let json;
48529
- try {
48530
- json = await res.json();
48531
- } catch {
48532
- return {
48533
- kind: "failure",
48534
- reason: `peer returned non-JSON response (HTTP ${res.status})`
48535
- };
48536
- }
48537
- if (json.ok === false) {
48538
- return { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` };
48539
- }
48540
- return json.result.outcome;
48541
- }
48542
- async function forwardInboxPostToPeer(args) {
48543
- const f = args.fetchImpl ?? fetch;
48544
- const base = wsUrlToHttp(args.contact.remoteUrl).replace(/\/+$/, "");
48545
- const url = `${base}/rpc/inbox:postMessage`;
48546
- let res;
48547
- try {
48548
- res = await f(url, {
48549
- method: "POST",
48550
- headers: {
48551
- "content-type": "application/json",
48552
- authorization: `Bearer ${args.contact.connectToken}`
48553
- },
48554
- body: JSON.stringify({
48555
- id: args.id,
48556
- text: args.text,
48557
- createdAt: args.createdAt,
48558
- ...args.origin ? { origin: args.origin } : {}
48559
- })
48560
- });
48561
- } catch (err) {
48562
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
48563
- }
48564
- let json = null;
48565
- try {
48566
- json = await res.json();
48567
- } catch {
48568
- return { ok: false, error: `peer non-JSON (HTTP ${res.status})` };
48569
- }
48570
- if (res.status < 200 || res.status >= 300 || json?.ok === false) {
48571
- return { ok: false, error: json?.error ?? `HTTP ${res.status}` };
48572
- }
48573
- return { ok: true };
48574
- }
48685
+ // src/index.ts
48686
+ init_peer_forward();
48575
48687
 
48576
48688
  // src/tools/codex-history.ts
48577
48689
  var import_node_child_process5 = require("child_process");
@@ -48759,13 +48871,13 @@ function mapSkillsListResponse(res) {
48759
48871
  const r = s ?? {};
48760
48872
  const name = str3(r.name);
48761
48873
  if (!name) continue;
48762
- const path73 = str3(r.path);
48874
+ const path76 = str3(r.path);
48763
48875
  const description = str3(r.description);
48764
48876
  const isPlugin = name.includes(":");
48765
48877
  out.push({
48766
48878
  name,
48767
48879
  source: isPlugin ? "plugin" : "project",
48768
- ...path73 ? { path: path73 } : {},
48880
+ ...path76 ? { path: path76 } : {},
48769
48881
  ...description ? { description } : {},
48770
48882
  ...isPlugin ? { plugin: name.split(":")[0] } : {}
48771
48883
  });
@@ -49414,18 +49526,8 @@ function listRegistered() {
49414
49526
  return [...registry.keys()];
49415
49527
  }
49416
49528
 
49417
- // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
49418
- var import_stream = __toESM(require_stream(), 1);
49419
- var import_extension = __toESM(require_extension(), 1);
49420
- var import_permessage_deflate = __toESM(require_permessage_deflate(), 1);
49421
- var import_receiver = __toESM(require_receiver(), 1);
49422
- var import_sender = __toESM(require_sender(), 1);
49423
- var import_subprotocol = __toESM(require_subprotocol(), 1);
49424
- var import_websocket = __toESM(require_websocket(), 1);
49425
- var import_websocket_server = __toESM(require_websocket_server(), 1);
49426
- var wrapper_default = import_websocket.default;
49427
-
49428
49529
  // src/transport/local-ws-server.ts
49530
+ init_wrapper();
49429
49531
  var import_node_http2 = __toESM(require("http"), 1);
49430
49532
 
49431
49533
  // src/transport/preview-proxy.ts
@@ -49501,6 +49603,18 @@ var LocalWsServer = class {
49501
49603
  const httpServer = import_node_http2.default.createServer((req, res) => this.handleHttpRequest(req, res));
49502
49604
  const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
49503
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
+ }
49504
49618
  if (req.url?.startsWith("/preview/")) {
49505
49619
  const pathname = (() => {
49506
49620
  try {
@@ -50073,6 +50187,7 @@ function constantTimeEqual2(a, b2) {
50073
50187
  }
50074
50188
 
50075
50189
  // src/transport/connection-context.ts
50190
+ init_src();
50076
50191
  function ownerContext(ownerPrincipalId, displayName) {
50077
50192
  return {
50078
50193
  principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
@@ -50126,6 +50241,7 @@ async function authenticate(token, deps) {
50126
50241
  // src/permission/capability-store.ts
50127
50242
  var fs28 = __toESM(require("fs"), 1);
50128
50243
  var path28 = __toESM(require("path"), 1);
50244
+ init_src();
50129
50245
  var CAPABILITIES_FILE_NAME = "capabilities.json";
50130
50246
  var FILE_VERSION = 1;
50131
50247
  var CapabilityStore = class {
@@ -50303,6 +50419,7 @@ function cleanupGuestSessionsForCapability(cap, factory) {
50303
50419
  // src/inbox/inbox-store.ts
50304
50420
  var fs30 = __toESM(require("fs"), 1);
50305
50421
  var path29 = __toESM(require("path"), 1);
50422
+ init_src();
50306
50423
  var INBOX_SUBDIR = "inbox";
50307
50424
  var InboxStore = class {
50308
50425
  constructor(dataDir) {
@@ -50493,6 +50610,7 @@ var InboxManager = class {
50493
50610
  // src/state/contact-store.ts
50494
50611
  var fs31 = __toESM(require("fs"), 1);
50495
50612
  var path30 = __toESM(require("path"), 1);
50613
+ init_src();
50496
50614
  var FILE_NAME = "contacts.json";
50497
50615
  var ContactStore = class {
50498
50616
  constructor(dataDir) {
@@ -50597,6 +50715,7 @@ var ContactStore = class {
50597
50715
  };
50598
50716
 
50599
50717
  // src/contact/connect-remote.ts
50718
+ init_wrapper();
50600
50719
  var crypto6 = __toESM(require("crypto"), 1);
50601
50720
  var HANDSHAKE_TIMEOUT_MS = 5e3;
50602
50721
  var RPC_TIMEOUT_MS = 5e3;
@@ -50742,6 +50861,9 @@ async function autoReverseContact(args) {
50742
50861
  args.broadcast({ type: "contact:added", contact: upgraded });
50743
50862
  }
50744
50863
 
50864
+ // src/index.ts
50865
+ init_src();
50866
+
50745
50867
  // src/migrations/2026-05-20-flatten-sessions.ts
50746
50868
  var fs32 = __toESM(require("fs"), 1);
50747
50869
  var path31 = __toESM(require("path"), 1);
@@ -50954,7 +51076,7 @@ function lookupMime(filePathOrName) {
50954
51076
  }
50955
51077
 
50956
51078
  // src/attachment/sign-url.ts
50957
- var import_node_crypto7 = __toESM(require("crypto"), 1);
51079
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
50958
51080
  var HMAC_ALGO = "sha256";
50959
51081
  function base64urlEncode(buf) {
50960
51082
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -50971,7 +51093,7 @@ function decodeAbsPathFromUrl(encoded) {
50971
51093
  }
50972
51094
  function computeSig(secret, absPath, e) {
50973
51095
  const msg = e === null ? absPath : `${absPath}|${e}`;
50974
- 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();
50975
51097
  }
50976
51098
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
50977
51099
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -51006,7 +51128,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
51006
51128
  if (provided.length !== expected.length) {
51007
51129
  return { ok: false, code: "BAD_SIG" };
51008
51130
  }
51009
- if (!import_node_crypto7.default.timingSafeEqual(provided, expected)) {
51131
+ if (!import_node_crypto6.default.timingSafeEqual(provided, expected)) {
51010
51132
  return { ok: false, code: "BAD_SIG" };
51011
51133
  }
51012
51134
  if (e !== null && now() / 1e3 > e) {
@@ -51018,7 +51140,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
51018
51140
  // src/attachment/upload.ts
51019
51141
  var import_node_fs25 = __toESM(require("fs"), 1);
51020
51142
  var import_node_path25 = __toESM(require("path"), 1);
51021
- var import_node_crypto8 = __toESM(require("crypto"), 1);
51143
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
51022
51144
  var import_promises2 = require("stream/promises");
51023
51145
  var UploadError = class extends Error {
51024
51146
  constructor(code, message) {
@@ -51042,11 +51164,11 @@ async function writeUploadedAttachment(args) {
51042
51164
  } catch (err) {
51043
51165
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
51044
51166
  }
51045
- const hasher = import_node_crypto8.default.createHash("sha256");
51167
+ const hasher = import_node_crypto7.default.createHash("sha256");
51046
51168
  let actualSize = 0;
51047
51169
  const tmpPath = import_node_path25.default.join(
51048
51170
  attachmentsRoot,
51049
- `.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")}`
51050
51172
  );
51051
51173
  try {
51052
51174
  await (0, import_promises2.pipeline)(
@@ -51124,6 +51246,7 @@ var import_promises3 = __toESM(require("fs/promises"), 1);
51124
51246
  var import_node_path26 = __toESM(require("path"), 1);
51125
51247
  var import_node_os12 = __toESM(require("os"), 1);
51126
51248
  var import_jszip = __toESM(require_lib3(), 1);
51249
+ init_src();
51127
51250
  var ImportError = class extends Error {
51128
51251
  constructor(code, message) {
51129
51252
  super(message);
@@ -51922,7 +52045,7 @@ function runAttachmentGc(args) {
51922
52045
  // src/attachment/group.ts
51923
52046
  var import_node_fs28 = __toESM(require("fs"), 1);
51924
52047
  var import_node_path29 = __toESM(require("path"), 1);
51925
- var import_node_crypto9 = __toESM(require("crypto"), 1);
52048
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
51926
52049
  init_protocol();
51927
52050
  var GroupFileStore = class {
51928
52051
  dataDir;
@@ -52011,7 +52134,7 @@ var GroupFileStore = class {
52011
52134
  entries[idx] = next;
52012
52135
  } else {
52013
52136
  next = {
52014
- id: `gf-${import_node_crypto9.default.randomBytes(6).toString("base64url")}`,
52137
+ id: `gf-${import_node_crypto8.default.randomBytes(6).toString("base64url")}`,
52015
52138
  relPath: input.relPath,
52016
52139
  from: input.from,
52017
52140
  label: input.label,
@@ -52130,7 +52253,7 @@ function readDaemonSourceFromEnv(env = process.env) {
52130
52253
  // src/tunnel/tunnel-manager.ts
52131
52254
  var import_node_fs33 = __toESM(require("fs"), 1);
52132
52255
  var import_node_path34 = __toESM(require("path"), 1);
52133
- var import_node_crypto10 = __toESM(require("crypto"), 1);
52256
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
52134
52257
  var import_node_child_process9 = require("child_process");
52135
52258
 
52136
52259
  // src/tunnel/tunnel-store.ts
@@ -52629,7 +52752,7 @@ var TunnelManager = class {
52629
52752
  override: this.deps.frpcBinaryOverride ?? void 0
52630
52753
  });
52631
52754
  const tomlPath = import_node_path34.default.join(this.deps.dataDir, "frpc.toml");
52632
- 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")}`;
52633
52756
  const toml = buildFrpcToml({
52634
52757
  serverAddr: t.frpsHost,
52635
52758
  serverPort: t.frpsPort,
@@ -52884,14 +53007,27 @@ var CLAWD_SSH_JAIL_SCRIPT = String.raw`#!/usr/bin/env bash
52884
53007
 
52885
53008
  set -euo pipefail
52886
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
+
52887
53019
  DEVICE_ID="\${1:-}"
52888
53020
  if [ -z "$DEVICE_ID" ]; then
53021
+ log_line ERROR jail.entered "clawd-ssh-jail 缺 deviceId 参数"
52889
53022
  echo "clawd-ssh-jail: missing deviceId" >&2
52890
53023
  exit 1
52891
53024
  fi
52892
53025
 
53026
+ log_line INFO jail.entered "clawd-ssh-jail 起来 device=\${DEVICE_ID} cmd=\${SSH_ORIGINAL_COMMAND:-<interactive-shell>}"
53027
+
52893
53028
  CONTACTS="\${HOME}/.clawd/contacts.json"
52894
53029
  if [ ! -f "$CONTACTS" ]; then
53030
+ log_line ERROR jail.entered "contacts.json 不存在,无法查 exposedDirs"
52895
53031
  echo "clawd-ssh-jail: contacts.json missing" >&2
52896
53032
  exit 1
52897
53033
  fi
@@ -52912,9 +53048,20 @@ sys.exit(3)
52912
53048
  ")
52913
53049
 
52914
53050
  if [ -z "$EXPOSED_JSON" ]; then
53051
+ log_line ERROR jail.entered "contact \${DEVICE_ID} 不在 store 或 exposedDirs 为空"
52915
53052
  echo "clawd-ssh-jail: contact not found or no exposed dirs" >&2
52916
53053
  exit 1
52917
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
52918
53065
 
52919
53066
  # 校验路径安全(bash 侧二次防御)
52920
53067
  while IFS= read -r line; do
@@ -53206,29 +53353,290 @@ function readIssuedPubkey(sshdDir, deviceId) {
53206
53353
  }
53207
53354
  }
53208
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
+
53209
53617
  // src/tunnel/device-key.ts
53210
53618
  var import_node_os14 = __toESM(require("os"), 1);
53211
- var import_node_path39 = __toESM(require("path"), 1);
53212
- 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);
53213
53621
  var DERIVE_SALT = "clawd-tunnel-device-v1";
53214
53622
  function deriveStableDeviceKey(opts = {}) {
53215
53623
  const hostname = opts.hostname ?? import_node_os14.default.hostname();
53216
53624
  const uid = opts.uid ?? (typeof import_node_os14.default.userInfo === "function" ? import_node_os14.default.userInfo().uid : 0);
53217
53625
  const home = opts.home ?? import_node_os14.default.homedir();
53218
- const defaultDataDir = import_node_path39.default.resolve(import_node_path39.default.join(home, ".clawd"));
53219
- const normalizedDataDir = opts.dataDir ? import_node_path39.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;
53220
53628
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
53221
53629
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
53222
- 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);
53223
53631
  }
53224
53632
 
53225
53633
  // src/auth-store.ts
53226
- var import_node_fs38 = __toESM(require("fs"), 1);
53227
- var import_node_path40 = __toESM(require("path"), 1);
53228
- 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);
53229
53637
  var AUTH_FILE_NAME = "auth.json";
53230
53638
  function authFilePath(dataDir) {
53231
- return import_node_path40.default.join(dataDir, AUTH_FILE_NAME);
53639
+ return import_node_path42.default.join(dataDir, AUTH_FILE_NAME);
53232
53640
  }
53233
53641
  function loadOrCreateAuthFile(opts) {
53234
53642
  const file = authFilePath(opts.dataDir);
@@ -53257,14 +53665,14 @@ function loadOrCreateAuthFile(opts) {
53257
53665
  return next;
53258
53666
  }
53259
53667
  function defaultGenerateToken() {
53260
- return import_node_crypto12.default.randomBytes(32).toString("base64url");
53668
+ return import_node_crypto11.default.randomBytes(32).toString("base64url");
53261
53669
  }
53262
53670
  function defaultGenerateOwnerPrincipalId() {
53263
- return `owner-${import_node_crypto12.default.randomUUID()}`;
53671
+ return `owner-${import_node_crypto11.default.randomUUID()}`;
53264
53672
  }
53265
53673
  function readAuthFile(file) {
53266
53674
  try {
53267
- const raw = import_node_fs38.default.readFileSync(file, "utf8");
53675
+ const raw = import_node_fs40.default.readFileSync(file, "utf8");
53268
53676
  const parsed = JSON.parse(raw);
53269
53677
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
53270
53678
  return null;
@@ -53284,25 +53692,25 @@ function readAuthFile(file) {
53284
53692
  }
53285
53693
  }
53286
53694
  function writeAuthFile(file, content) {
53287
- import_node_fs38.default.mkdirSync(import_node_path40.default.dirname(file), { recursive: true });
53288
- import_node_fs38.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 });
53289
53697
  try {
53290
- import_node_fs38.default.chmodSync(file, 384);
53698
+ import_node_fs40.default.chmodSync(file, 384);
53291
53699
  } catch {
53292
53700
  }
53293
53701
  }
53294
53702
 
53295
53703
  // src/owner-profile.ts
53296
- var import_node_fs39 = __toESM(require("fs"), 1);
53704
+ var import_node_fs41 = __toESM(require("fs"), 1);
53297
53705
  var import_node_os15 = __toESM(require("os"), 1);
53298
- var import_node_path41 = __toESM(require("path"), 1);
53706
+ var import_node_path43 = __toESM(require("path"), 1);
53299
53707
  var PROFILE_FILENAME = "profile.json";
53300
53708
  function loadOwnerDisplayName(dataDir) {
53301
53709
  const fallback = import_node_os15.default.userInfo().username;
53302
- const profilePath = import_node_path41.default.join(dataDir, PROFILE_FILENAME);
53710
+ const profilePath = import_node_path43.default.join(dataDir, PROFILE_FILENAME);
53303
53711
  let raw;
53304
53712
  try {
53305
- raw = import_node_fs39.default.readFileSync(profilePath, "utf8");
53713
+ raw = import_node_fs41.default.readFileSync(profilePath, "utf8");
53306
53714
  } catch {
53307
53715
  return fallback;
53308
53716
  }
@@ -53325,18 +53733,18 @@ function loadOwnerDisplayName(dataDir) {
53325
53733
  }
53326
53734
 
53327
53735
  // src/feishu-auth/owner-identity-store.ts
53328
- var import_node_fs40 = __toESM(require("fs"), 1);
53329
- var import_node_path42 = __toESM(require("path"), 1);
53736
+ var import_node_fs42 = __toESM(require("fs"), 1);
53737
+ var import_node_path44 = __toESM(require("path"), 1);
53330
53738
  var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
53331
53739
  var OwnerIdentityStore = class {
53332
53740
  file;
53333
53741
  constructor(dataDir) {
53334
- this.file = import_node_path42.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
53742
+ this.file = import_node_path44.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
53335
53743
  }
53336
53744
  read() {
53337
53745
  let raw;
53338
53746
  try {
53339
- raw = import_node_fs40.default.readFileSync(this.file, "utf8");
53747
+ raw = import_node_fs42.default.readFileSync(this.file, "utf8");
53340
53748
  } catch {
53341
53749
  return null;
53342
53750
  }
@@ -53364,16 +53772,16 @@ var OwnerIdentityStore = class {
53364
53772
  };
53365
53773
  }
53366
53774
  write(record) {
53367
- import_node_fs40.default.mkdirSync(import_node_path42.default.dirname(this.file), { recursive: true });
53368
- import_node_fs40.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 });
53369
53777
  try {
53370
- import_node_fs40.default.chmodSync(this.file, 384);
53778
+ import_node_fs42.default.chmodSync(this.file, 384);
53371
53779
  } catch {
53372
53780
  }
53373
53781
  }
53374
53782
  clear() {
53375
53783
  try {
53376
- import_node_fs40.default.unlinkSync(this.file);
53784
+ import_node_fs42.default.unlinkSync(this.file);
53377
53785
  } catch (err) {
53378
53786
  const code = err?.code;
53379
53787
  if (code !== "ENOENT") throw err;
@@ -53382,7 +53790,7 @@ var OwnerIdentityStore = class {
53382
53790
  };
53383
53791
 
53384
53792
  // src/feishu-auth/login-flow.ts
53385
- var import_node_crypto13 = __toESM(require("crypto"), 1);
53793
+ var import_node_crypto12 = __toESM(require("crypto"), 1);
53386
53794
  var STATE_TTL_MS = 5 * 60 * 1e3;
53387
53795
  var LoginFlow = class {
53388
53796
  constructor(deps) {
@@ -53391,7 +53799,7 @@ var LoginFlow = class {
53391
53799
  deps;
53392
53800
  pendingStates = /* @__PURE__ */ new Map();
53393
53801
  start() {
53394
- const state = import_node_crypto13.default.randomBytes(16).toString("base64url");
53802
+ const state = import_node_crypto12.default.randomBytes(16).toString("base64url");
53395
53803
  const now = (this.deps.now ?? Date.now)();
53396
53804
  this.pendingStates.set(state, now);
53397
53805
  this.gcExpired(now);
@@ -53494,9 +53902,9 @@ var CentralClientError = class extends Error {
53494
53902
  code;
53495
53903
  cause;
53496
53904
  };
53497
- async function centralRequest(opts, path73, init) {
53905
+ async function centralRequest(opts, path76, init) {
53498
53906
  const f = opts.fetchImpl ?? globalThis.fetch;
53499
- const url = `${opts.api.replace(/\/+$/, "")}${path73}`;
53907
+ const url = `${opts.api.replace(/\/+$/, "")}${path76}`;
53500
53908
  const ctrl = new AbortController();
53501
53909
  const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
53502
53910
  let res;
@@ -53638,8 +54046,8 @@ function verifyConnectToken(args) {
53638
54046
  }
53639
54047
 
53640
54048
  // src/feishu-auth/server-key.ts
53641
- var fs50 = __toESM(require("fs"), 1);
53642
- var path51 = __toESM(require("path"), 1);
54049
+ var fs52 = __toESM(require("fs"), 1);
54050
+ var path53 = __toESM(require("path"), 1);
53643
54051
  var FILE_NAME2 = "server-signing-key.json";
53644
54052
  var ServerKeyStore = class {
53645
54053
  constructor(dataDir) {
@@ -53647,12 +54055,12 @@ var ServerKeyStore = class {
53647
54055
  }
53648
54056
  dataDir;
53649
54057
  filePath() {
53650
- return path51.join(this.dataDir, FILE_NAME2);
54058
+ return path53.join(this.dataDir, FILE_NAME2);
53651
54059
  }
53652
54060
  /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
53653
54061
  read() {
53654
54062
  try {
53655
- const raw = fs50.readFileSync(this.filePath(), "utf8");
54063
+ const raw = fs52.readFileSync(this.filePath(), "utf8");
53656
54064
  const parsed = JSON.parse(raw);
53657
54065
  if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
53658
54066
  return parsed.publicKeyPem;
@@ -53667,12 +54075,12 @@ var ServerKeyStore = class {
53667
54075
  publicKeyPem,
53668
54076
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
53669
54077
  };
53670
- fs50.mkdirSync(this.dataDir, { recursive: true });
53671
- fs50.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 });
53672
54080
  }
53673
54081
  clear() {
53674
54082
  try {
53675
- fs50.unlinkSync(this.filePath());
54083
+ fs52.unlinkSync(this.filePath());
53676
54084
  } catch {
53677
54085
  }
53678
54086
  }
@@ -53685,12 +54093,12 @@ init_protocol();
53685
54093
  init_protocol();
53686
54094
 
53687
54095
  // src/session/fork.ts
53688
- var import_node_fs41 = __toESM(require("fs"), 1);
54096
+ var import_node_fs43 = __toESM(require("fs"), 1);
53689
54097
  var import_node_os16 = __toESM(require("os"), 1);
53690
- var import_node_path43 = __toESM(require("path"), 1);
54098
+ var import_node_path45 = __toESM(require("path"), 1);
53691
54099
  init_claude_history();
53692
54100
  function readJsonlEntries(file) {
53693
- const raw = import_node_fs41.default.readFileSync(file, "utf8");
54101
+ const raw = import_node_fs43.default.readFileSync(file, "utf8");
53694
54102
  const out = [];
53695
54103
  for (const line of raw.split("\n")) {
53696
54104
  const t = line.trim();
@@ -53703,10 +54111,10 @@ function readJsonlEntries(file) {
53703
54111
  return out;
53704
54112
  }
53705
54113
  function forkSession(input) {
53706
- const baseDir = input.baseDir ?? import_node_path43.default.join(import_node_os16.default.homedir(), ".claude");
53707
- const projectDir = import_node_path43.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
53708
- const sourceFile = import_node_path43.default.join(projectDir, `${input.toolSessionId}.jsonl`);
53709
- if (!import_node_fs41.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)) {
53710
54118
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
53711
54119
  }
53712
54120
  const entries = readJsonlEntries(sourceFile);
@@ -53736,9 +54144,9 @@ function forkSession(input) {
53736
54144
  }
53737
54145
  forkedLines.push(JSON.stringify(forked));
53738
54146
  }
53739
- const forkedFilePath = import_node_path43.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
53740
- import_node_fs41.default.mkdirSync(projectDir, { recursive: true });
53741
- import_node_fs41.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 });
53742
54150
  return { forkedToolSessionId, forkedFilePath };
53743
54151
  }
53744
54152
 
@@ -54090,7 +54498,7 @@ function buildPermissionHandlers(deps) {
54090
54498
  }
54091
54499
 
54092
54500
  // src/handlers/history.ts
54093
- var path54 = __toESM(require("path"), 1);
54501
+ var path56 = __toESM(require("path"), 1);
54094
54502
  init_protocol();
54095
54503
 
54096
54504
  // src/session/recent-dirs.ts
@@ -54108,7 +54516,7 @@ function listRecentDirs(store, limit = 50) {
54108
54516
  }
54109
54517
 
54110
54518
  // src/permission/persona-paths.ts
54111
- var path53 = __toESM(require("path"), 1);
54519
+ var path55 = __toESM(require("path"), 1);
54112
54520
  function getAllowedPersonaIds(grants, action) {
54113
54521
  const ids = /* @__PURE__ */ new Set();
54114
54522
  for (const g2 of grants) {
@@ -54121,42 +54529,42 @@ function getAllowedPersonaIds(grants, action) {
54121
54529
  return ids;
54122
54530
  }
54123
54531
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read", userWorkDir) {
54124
- const target = path53.resolve(absPath);
54532
+ const target = path55.resolve(absPath);
54125
54533
  if (userWorkDir) {
54126
- const u = path53.resolve(userWorkDir);
54127
- const usep = u.endsWith(path53.sep) ? "" : path53.sep;
54534
+ const u = path55.resolve(userWorkDir);
54535
+ const usep = u.endsWith(path55.sep) ? "" : path55.sep;
54128
54536
  if (target === u || target.startsWith(u + usep)) return true;
54129
54537
  }
54130
- const root = path53.resolve(personaRoot);
54131
- const sep3 = root.endsWith(path53.sep) ? "" : path53.sep;
54538
+ const root = path55.resolve(personaRoot);
54539
+ const sep3 = root.endsWith(path55.sep) ? "" : path55.sep;
54132
54540
  if (!target.startsWith(root + sep3)) return false;
54133
- const rel = path53.relative(root, target);
54541
+ const rel = path55.relative(root, target);
54134
54542
  if (!rel || rel.startsWith("..")) return false;
54135
- const personaId = rel.split(path53.sep)[0];
54543
+ const personaId = rel.split(path55.sep)[0];
54136
54544
  if (!personaId) return false;
54137
54545
  const allowed = getAllowedPersonaIds(grants, action);
54138
54546
  if (allowed === "*") return true;
54139
54547
  return allowed.has(personaId);
54140
54548
  }
54141
54549
  function personaIdFromPath(absPath, personaRoot) {
54142
- const root = path53.resolve(personaRoot);
54143
- const target = path53.resolve(absPath);
54144
- const sep3 = root.endsWith(path53.sep) ? "" : path53.sep;
54550
+ const root = path55.resolve(personaRoot);
54551
+ const target = path55.resolve(absPath);
54552
+ const sep3 = root.endsWith(path55.sep) ? "" : path55.sep;
54145
54553
  if (!target.startsWith(root + sep3)) return null;
54146
- const rel = path53.relative(root, target);
54554
+ const rel = path55.relative(root, target);
54147
54555
  if (!rel || rel.startsWith("..")) return null;
54148
- const id = rel.split(path53.sep)[0];
54556
+ const id = rel.split(path55.sep)[0];
54149
54557
  return id || null;
54150
54558
  }
54151
54559
  function isPathWithin(dir, absPath) {
54152
- const d = path53.resolve(dir);
54153
- const t = path53.resolve(absPath);
54154
- const sep3 = d.endsWith(path53.sep) ? "" : path53.sep;
54560
+ const d = path55.resolve(dir);
54561
+ const t = path55.resolve(absPath);
54562
+ const sep3 = d.endsWith(path55.sep) ? "" : path55.sep;
54155
54563
  return t === d || t.startsWith(d + sep3);
54156
54564
  }
54157
54565
  function isPathInGuestBoundary(personaRoot, personaId, userWorkDir, absPath) {
54158
54566
  if (userWorkDir && isPathWithin(userWorkDir, absPath)) return true;
54159
- return personaIdFromPath(path53.resolve(absPath), personaRoot) === personaId;
54567
+ return personaIdFromPath(path55.resolve(absPath), personaRoot) === personaId;
54160
54568
  }
54161
54569
 
54162
54570
  // src/handlers/history.ts
@@ -54182,7 +54590,7 @@ function buildHistoryHandlers(deps) {
54182
54590
  if (!pid) return false;
54183
54591
  return isGuestPathAllowed(
54184
54592
  ctx.grants,
54185
- path54.join(personaRoot, pid),
54593
+ path56.join(personaRoot, pid),
54186
54594
  personaRoot,
54187
54595
  "read",
54188
54596
  userWorkDir
@@ -54194,7 +54602,7 @@ function buildHistoryHandlers(deps) {
54194
54602
  };
54195
54603
  const list = async (frame, _client, ctx) => {
54196
54604
  const args = HistoryListArgs.parse(frame);
54197
- assertGuestPath(ctx, path54.resolve(args.projectPath), personaRoot, "history:list");
54605
+ assertGuestPath(ctx, path56.resolve(args.projectPath), personaRoot, "history:list");
54198
54606
  const sessions = await history.listSessions(args);
54199
54607
  return { response: { type: "history:list", sessions } };
54200
54608
  };
@@ -54226,13 +54634,13 @@ function buildHistoryHandlers(deps) {
54226
54634
  };
54227
54635
  const subagents = async (frame, _client, ctx) => {
54228
54636
  const args = HistorySubagentsArgs.parse(frame);
54229
- assertGuestPath(ctx, path54.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
54637
+ assertGuestPath(ctx, path56.resolve(args.cwd), personaRoot, "history:subagents", usersRoot);
54230
54638
  const subs = await history.listSubagents(args);
54231
54639
  return { response: { type: "history:subagents", subagents: subs } };
54232
54640
  };
54233
54641
  const subagentRead = async (frame, _client, ctx) => {
54234
54642
  const args = HistorySubagentReadArgs.parse(frame);
54235
- assertGuestPath(ctx, path54.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
54643
+ assertGuestPath(ctx, path56.resolve(args.cwd), personaRoot, "history:subagent-read", usersRoot);
54236
54644
  const res = await history.readSubagent(args);
54237
54645
  return { response: { type: "history:subagent-read", ...res } };
54238
54646
  };
@@ -54241,7 +54649,7 @@ function buildHistoryHandlers(deps) {
54241
54649
  if (ctx?.principal.kind === "guest" && personaRoot) {
54242
54650
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
54243
54651
  const filtered = dirs.filter(
54244
- (d) => isGuestPathAllowed(ctx.grants, path54.resolve(d.cwd), personaRoot, "read", userWorkDir)
54652
+ (d) => isGuestPathAllowed(ctx.grants, path56.resolve(d.cwd), personaRoot, "read", userWorkDir)
54245
54653
  );
54246
54654
  return { response: { type: "history:recentDirs", dirs: filtered } };
54247
54655
  }
@@ -54258,7 +54666,7 @@ function buildHistoryHandlers(deps) {
54258
54666
  }
54259
54667
 
54260
54668
  // src/handlers/workspace.ts
54261
- var path55 = __toESM(require("path"), 1);
54669
+ var path57 = __toESM(require("path"), 1);
54262
54670
  var os16 = __toESM(require("os"), 1);
54263
54671
  init_protocol();
54264
54672
  init_protocol();
@@ -54300,22 +54708,22 @@ function buildWorkspaceHandlers(deps) {
54300
54708
  const args = WorkspaceListArgs.parse(frame);
54301
54709
  const isGuest = ctx?.principal.kind === "guest";
54302
54710
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os16.homedir();
54303
- const resolvedCwd = path55.resolve(args.cwd ?? fallbackCwd);
54304
- const target = args.path ? path55.resolve(resolvedCwd, args.path) : resolvedCwd;
54711
+ const resolvedCwd = path57.resolve(args.cwd ?? fallbackCwd);
54712
+ const target = args.path ? path57.resolve(resolvedCwd, args.path) : resolvedCwd;
54305
54713
  assertGuestPath2(ctx, target, personaRoot, "workspace:list", usersRoot);
54306
54714
  const res = workspace.list({ ...args, cwd: resolvedCwd });
54307
54715
  return { response: { type: "workspace:list", ...res } };
54308
54716
  };
54309
54717
  const read = async (frame, _client, ctx) => {
54310
54718
  const args = WorkspaceReadArgs.parse(frame);
54311
- const target = path55.isAbsolute(args.path) ? path55.resolve(args.path) : path55.resolve(args.cwd, args.path);
54719
+ const target = path57.isAbsolute(args.path) ? path57.resolve(args.path) : path57.resolve(args.cwd, args.path);
54312
54720
  assertGuestPath2(ctx, target, personaRoot, "workspace:read", usersRoot);
54313
54721
  const res = workspace.read(args);
54314
54722
  return { response: { type: "workspace:read", ...res } };
54315
54723
  };
54316
54724
  const skillsList = async (frame, _client, ctx) => {
54317
54725
  const args = SkillsListArgs.parse(frame);
54318
- const cwdAbs = path55.resolve(args.cwd);
54726
+ const cwdAbs = path57.resolve(args.cwd);
54319
54727
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list", usersRoot);
54320
54728
  const list2 = await getSkillsForTool(args.tool ?? "claude", cwdAbs);
54321
54729
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -54327,7 +54735,7 @@ function buildWorkspaceHandlers(deps) {
54327
54735
  };
54328
54736
  const agentsList = async (frame, _client, ctx) => {
54329
54737
  const args = AgentsListArgs.parse(frame);
54330
- const cwdAbs = path55.resolve(args.cwd);
54738
+ const cwdAbs = path57.resolve(args.cwd);
54331
54739
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list", usersRoot);
54332
54740
  if (args.tool === "codex") {
54333
54741
  return { response: { type: "agents:list", agents: [] } };
@@ -54349,20 +54757,20 @@ function buildWorkspaceHandlers(deps) {
54349
54757
  }
54350
54758
 
54351
54759
  // src/handlers/git.ts
54352
- var path57 = __toESM(require("path"), 1);
54760
+ var path59 = __toESM(require("path"), 1);
54353
54761
  init_protocol();
54354
54762
  init_protocol();
54355
54763
 
54356
54764
  // src/workspace/git.ts
54357
54765
  var import_node_child_process12 = require("child_process");
54358
- var import_node_fs42 = __toESM(require("fs"), 1);
54359
- var import_node_path44 = __toESM(require("path"), 1);
54766
+ var import_node_fs44 = __toESM(require("fs"), 1);
54767
+ var import_node_path46 = __toESM(require("path"), 1);
54360
54768
  var import_node_util = require("util");
54361
54769
  var pexec = (0, import_node_util.promisify)(import_node_child_process12.execFile);
54362
54770
  function normalizePath(p2) {
54363
- const resolved = import_node_path44.default.resolve(p2);
54771
+ const resolved = import_node_path46.default.resolve(p2);
54364
54772
  try {
54365
- return import_node_fs42.default.realpathSync(resolved);
54773
+ return import_node_fs44.default.realpathSync(resolved);
54366
54774
  } catch {
54367
54775
  return resolved;
54368
54776
  }
@@ -54436,7 +54844,7 @@ async function listGitBranches(cwd) {
54436
54844
  function assertGuestCwd(ctx, cwd, personaRoot, method, usersRoot) {
54437
54845
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
54438
54846
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
54439
- if (!isGuestPathAllowed(ctx.grants, path57.resolve(cwd), personaRoot, "read", userWorkDir)) {
54847
+ if (!isGuestPathAllowed(ctx.grants, path59.resolve(cwd), personaRoot, "read", userWorkDir)) {
54440
54848
  throw new ClawdError(
54441
54849
  ERROR_CODES.UNAUTHORIZED,
54442
54850
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -54486,6 +54894,7 @@ function buildCapabilitiesHandlers(deps) {
54486
54894
 
54487
54895
  // src/handlers/capability.ts
54488
54896
  init_zod();
54897
+ init_src();
54489
54898
  init_protocol();
54490
54899
  var DeleteArgsSchema = external_exports.object({
54491
54900
  capabilityId: external_exports.string().min(1)
@@ -54524,6 +54933,7 @@ function buildCapabilityHandlers(deps) {
54524
54933
  }
54525
54934
 
54526
54935
  // src/handlers/inbox.ts
54936
+ init_src();
54527
54937
  init_protocol();
54528
54938
  function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
54529
54939
  if (ctx.principal.kind === "owner") {
@@ -54702,6 +55112,7 @@ function buildInboxHandlers(deps) {
54702
55112
  }
54703
55113
 
54704
55114
  // src/handlers/contact.ts
55115
+ init_src();
54705
55116
  init_protocol();
54706
55117
  function ensureOwner(ctx) {
54707
55118
  if (!ctx || ctx.principal.kind !== "owner") {
@@ -54770,25 +55181,26 @@ function buildContactHandlers(deps) {
54770
55181
  }
54771
55182
 
54772
55183
  // src/handlers/contact-ssh.ts
55184
+ init_src();
54773
55185
  init_protocol();
54774
55186
 
54775
55187
  // src/sshd/key-issue.ts
54776
- var import_node_fs43 = __toESM(require("fs"), 1);
54777
- var import_node_path45 = __toESM(require("path"), 1);
55188
+ var import_node_fs45 = __toESM(require("fs"), 1);
55189
+ var import_node_path47 = __toESM(require("path"), 1);
54778
55190
  var import_node_child_process13 = require("child_process");
54779
55191
  function safeDeviceId(deviceId) {
54780
55192
  return deviceId.replace(/[\/\\]/g, "_");
54781
55193
  }
54782
55194
  async function issueContactSshKey(deviceId, sshdDir, opts = {}) {
54783
55195
  const safeId = safeDeviceId(deviceId);
54784
- const keysDir = import_node_path45.default.join(sshdDir, "keys");
54785
- import_node_fs43.default.mkdirSync(keysDir, { recursive: true, mode: 448 });
54786
- const privPath = import_node_path45.default.join(keysDir, `${safeId}.ed25519`);
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`);
54787
55199
  const pubPath = `${privPath}.pub`;
54788
- if (import_node_fs43.default.existsSync(privPath) && import_node_fs43.default.existsSync(pubPath)) {
55200
+ if (import_node_fs45.default.existsSync(privPath) && import_node_fs45.default.existsSync(pubPath)) {
54789
55201
  return {
54790
- privateKeyPem: import_node_fs43.default.readFileSync(privPath, "utf8"),
54791
- publicKeyLine: import_node_fs43.default.readFileSync(pubPath, "utf8").trim()
55202
+ privateKeyPem: import_node_fs45.default.readFileSync(privPath, "utf8"),
55203
+ publicKeyLine: import_node_fs45.default.readFileSync(pubPath, "utf8").trim()
54792
55204
  };
54793
55205
  }
54794
55206
  const bin = opts.keygenBin ?? "/usr/bin/ssh-keygen";
@@ -54802,16 +55214,16 @@ async function issueContactSshKey(deviceId, sshdDir, opts = {}) {
54802
55214
  p2.on("error", reject);
54803
55215
  });
54804
55216
  try {
54805
- import_node_fs43.default.chmodSync(privPath, 384);
55217
+ import_node_fs45.default.chmodSync(privPath, 384);
54806
55218
  } catch {
54807
55219
  }
54808
55220
  try {
54809
- import_node_fs43.default.chmodSync(pubPath, 420);
55221
+ import_node_fs45.default.chmodSync(pubPath, 420);
54810
55222
  } catch {
54811
55223
  }
54812
55224
  return {
54813
- privateKeyPem: import_node_fs43.default.readFileSync(privPath, "utf8"),
54814
- publicKeyLine: import_node_fs43.default.readFileSync(pubPath, "utf8").trim()
55225
+ privateKeyPem: import_node_fs45.default.readFileSync(privPath, "utf8"),
55226
+ publicKeyLine: import_node_fs45.default.readFileSync(pubPath, "utf8").trim()
54815
55227
  };
54816
55228
  }
54817
55229
 
@@ -54834,6 +55246,7 @@ function ensureGuest(ctx) {
54834
55246
  return ctx.principal.id;
54835
55247
  }
54836
55248
  function buildContactSshHandlers(deps) {
55249
+ const sshLog = deps.sshLog ?? nullContactSshLog;
54837
55250
  const setSshAccess = async (frame, _client, ctx) => {
54838
55251
  ensureOwner2(ctx);
54839
55252
  const { type: _t, requestId: _r, ...rest } = frame;
@@ -54843,12 +55256,20 @@ function buildContactSshHandlers(deps) {
54843
55256
  exposedDirs: args.exposedDirs
54844
55257
  });
54845
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
+ });
54846
55262
  throw new ClawdError(
54847
55263
  ERROR_CODES.CONTACT_NOT_FOUND,
54848
55264
  `CONTACT_NOT_FOUND: contact ${args.deviceId} not in store`
54849
55265
  );
54850
55266
  }
54851
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
+ });
54852
55273
  deps.broadcast({
54853
55274
  type: "contact:ssh-access-updated",
54854
55275
  deviceId: args.deviceId,
@@ -54870,6 +55291,14 @@ function buildContactSshHandlers(deps) {
54870
55291
  ContactSshKeyIssueArgsSchema.parse(rest);
54871
55292
  const contact = deps.store.get(callerDeviceId);
54872
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
+ );
54873
55302
  throw new ClawdError(
54874
55303
  ERROR_CODES.UNAUTHORIZED,
54875
55304
  `UNAUTHORIZED: contact ${callerDeviceId} not authorized for SSH`
@@ -54877,6 +55306,15 @@ function buildContactSshHandlers(deps) {
54877
55306
  }
54878
55307
  const { privateKeyPem, publicKeyLine } = await issueContactSshKey(callerDeviceId, deps.sshdDir);
54879
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
+ );
54880
55318
  return {
54881
55319
  response: {
54882
55320
  type: "contact:sshKey:issue:ok",
@@ -54892,6 +55330,7 @@ function buildContactSshHandlers(deps) {
54892
55330
  }
54893
55331
 
54894
55332
  // src/handlers/whoami.ts
55333
+ init_src();
54895
55334
  init_protocol();
54896
55335
  function buildWhoamiHandler(deps) {
54897
55336
  return async (_frame, _client, ctx) => {
@@ -54985,6 +55424,7 @@ function buildFeishuAuthHandlers(deps) {
54985
55424
  }
54986
55425
 
54987
55426
  // src/handlers/device.ts
55427
+ init_src();
54988
55428
  init_protocol();
54989
55429
  function ensureOwner3(ctx) {
54990
55430
  if (!ctx || ctx.principal.kind !== "owner") {
@@ -55202,7 +55642,7 @@ function buildPersonaHandlers(deps) {
55202
55642
  }
55203
55643
 
55204
55644
  // src/handlers/attachment.ts
55205
- var import_node_path46 = __toESM(require("path"), 1);
55645
+ var import_node_path48 = __toESM(require("path"), 1);
55206
55646
  init_protocol();
55207
55647
  init_protocol();
55208
55648
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -55282,12 +55722,12 @@ function buildAttachmentHandlers(deps) {
55282
55722
  `session ${args.sessionId} scope unresolved`
55283
55723
  );
55284
55724
  }
55285
- const cwdAbs = import_node_path46.default.resolve(sessionFile.cwd);
55286
- const candidateAbs = import_node_path46.default.isAbsolute(args.relPath) ? import_node_path46.default.resolve(args.relPath) : import_node_path46.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);
55287
55727
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.signUrl", "group-acl");
55288
55728
  const entries = deps.groupFileStore.list(scope, args.sessionId);
55289
55729
  const entry = entries.find((e) => {
55290
- const storedAbs = import_node_path46.default.isAbsolute(e.relPath) ? import_node_path46.default.resolve(e.relPath) : import_node_path46.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);
55291
55731
  return storedAbs === candidateAbs && !e.stale;
55292
55732
  });
55293
55733
  if (!entry) {
@@ -55312,7 +55752,7 @@ function buildAttachmentHandlers(deps) {
55312
55752
  if (!ctx || ctx.principal.kind !== "guest" || !deps.personaRoot || !deps.sessionStore) return;
55313
55753
  const f = deps.sessionStore.read(sessionId);
55314
55754
  if (!f) return;
55315
- assertGuestAttachmentPath(ctx, import_node_path46.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
55755
+ assertGuestAttachmentPath(ctx, import_node_path48.default.resolve(f.cwd), deps.personaRoot, method, deps.usersRoot);
55316
55756
  }
55317
55757
  const groupAdd = async (frame, _client, ctx) => {
55318
55758
  if (!deps.groupFileStore || !deps.getSessionScope) {
@@ -55327,8 +55767,8 @@ function buildAttachmentHandlers(deps) {
55327
55767
  if (!scope) {
55328
55768
  throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
55329
55769
  }
55330
- const cwdAbs = import_node_path46.default.resolve(deps.sessionStore?.read(args.sessionId)?.cwd ?? ".");
55331
- const candidateAbs = import_node_path46.default.isAbsolute(args.relPath) ? import_node_path46.default.resolve(args.relPath) : import_node_path46.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);
55332
55772
  guardAttachmentPath(ctx, args.sessionId, candidateAbs, "attachment.groupAdd", "cwd-subtree");
55333
55773
  const from = ctx?.principal.kind === "owner" ? "owner" : "agent";
55334
55774
  const size = 0;
@@ -55387,19 +55827,20 @@ function buildAttachmentHandlers(deps) {
55387
55827
 
55388
55828
  // src/handlers/extension.ts
55389
55829
  var import_promises8 = __toESM(require("fs/promises"), 1);
55390
- var import_node_path51 = __toESM(require("path"), 1);
55830
+ var import_node_path53 = __toESM(require("path"), 1);
55391
55831
  init_protocol();
55832
+ init_src();
55392
55833
 
55393
55834
  // src/extension/bundle-zip.ts
55394
55835
  var import_promises5 = __toESM(require("fs/promises"), 1);
55395
- var import_node_path47 = __toESM(require("path"), 1);
55396
- 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);
55397
55838
  var import_jszip2 = __toESM(require_lib3(), 1);
55398
55839
  async function bundleExtensionDir(dir) {
55399
55840
  const entries = await listFilesSorted(dir);
55400
55841
  const zip = new import_jszip2.default();
55401
55842
  for (const rel of entries) {
55402
- const abs = import_node_path47.default.join(dir, rel);
55843
+ const abs = import_node_path49.default.join(dir, rel);
55403
55844
  const content = await import_promises5.default.readFile(abs);
55404
55845
  zip.file(rel, content, { date: FIXED_DATE });
55405
55846
  }
@@ -55408,7 +55849,7 @@ async function bundleExtensionDir(dir) {
55408
55849
  compression: "DEFLATE",
55409
55850
  compressionOptions: { level: 6 }
55410
55851
  });
55411
- 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");
55412
55853
  return { buffer, sha256 };
55413
55854
  }
55414
55855
  var FIXED_DATE = /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
@@ -55420,7 +55861,7 @@ async function listFilesSorted(rootDir) {
55420
55861
  return out;
55421
55862
  }
55422
55863
  async function walk(absRoot, relPrefix, out) {
55423
- const dirAbs = import_node_path47.default.join(absRoot, relPrefix);
55864
+ const dirAbs = import_node_path49.default.join(absRoot, relPrefix);
55424
55865
  const entries = await import_promises5.default.readdir(dirAbs, { withFileTypes: true });
55425
55866
  for (const e of entries) {
55426
55867
  if (IGNORE_BASENAMES.has(e.name)) continue;
@@ -55434,6 +55875,8 @@ async function walk(absRoot, relPrefix, out) {
55434
55875
  }
55435
55876
 
55436
55877
  // src/extension/publish-check.ts
55878
+ init_src();
55879
+ init_src();
55437
55880
  function computePublishCheck(args) {
55438
55881
  const { localHash, localVersion, head } = args;
55439
55882
  if (head === null) {
@@ -55474,25 +55917,27 @@ function computePublishCheck(args) {
55474
55917
 
55475
55918
  // src/extension/install-flow.ts
55476
55919
  var import_promises6 = __toESM(require("fs/promises"), 1);
55477
- var import_node_path49 = __toESM(require("path"), 1);
55920
+ var import_node_path51 = __toESM(require("path"), 1);
55478
55921
  var import_node_os19 = __toESM(require("os"), 1);
55479
- var import_node_crypto15 = __toESM(require("crypto"), 1);
55922
+ var import_node_crypto14 = __toESM(require("crypto"), 1);
55480
55923
  var import_jszip3 = __toESM(require_lib3(), 1);
55924
+ init_src();
55481
55925
 
55482
55926
  // src/extension/paths.ts
55483
55927
  var import_node_os18 = __toESM(require("os"), 1);
55484
- var import_node_path48 = __toESM(require("path"), 1);
55928
+ var import_node_path50 = __toESM(require("path"), 1);
55929
+ init_src();
55485
55930
  function clawdHomeRoot(override) {
55486
- return override ?? process.env.CLAWD_HOME ?? import_node_path48.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");
55487
55932
  }
55488
55933
  function extensionsRoot(override) {
55489
- return import_node_path48.default.join(clawdHomeRoot(override), "extensions");
55934
+ return import_node_path50.default.join(clawdHomeRoot(override), "extensions");
55490
55935
  }
55491
55936
  function publishedChannelsFile(override) {
55492
- return import_node_path48.default.join(clawdHomeRoot(override), "extensions-published.json");
55937
+ return import_node_path50.default.join(clawdHomeRoot(override), "extensions-published.json");
55493
55938
  }
55494
55939
  function bundleCacheRoot(override) {
55495
- return import_node_path48.default.join(clawdHomeRoot(override), "extension-bundles");
55940
+ return import_node_path50.default.join(clawdHomeRoot(override), "extension-bundles");
55496
55941
  }
55497
55942
 
55498
55943
  // src/extension/install-flow.ts
@@ -55505,7 +55950,7 @@ var InstallError = class extends Error {
55505
55950
  };
55506
55951
  async function installFromChannel(args, deps) {
55507
55952
  const { channelRef, snapshotHash, bundleZip } = args;
55508
- 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");
55509
55954
  if (computed !== snapshotHash) {
55510
55955
  throw new InstallError(
55511
55956
  "HASH_MISMATCH",
@@ -55519,7 +55964,7 @@ async function installFromChannel(args, deps) {
55519
55964
  throw new InstallError("ZIP_INVALID", `failed to load zip: ${e.message}`);
55520
55965
  }
55521
55966
  for (const name of Object.keys(zip.files)) {
55522
- if (name.includes("..") || name.startsWith("/") || import_node_path49.default.isAbsolute(name)) {
55967
+ if (name.includes("..") || name.startsWith("/") || import_node_path51.default.isAbsolute(name)) {
55523
55968
  throw new InstallError("ZIP_INVALID", `unsafe zip entry: ${name}`);
55524
55969
  }
55525
55970
  }
@@ -55551,7 +55996,7 @@ async function installFromChannel(args, deps) {
55551
55996
  );
55552
55997
  }
55553
55998
  const localExtId = namespacedExtId(ownerSlug, channelRef.ownerPrincipalId);
55554
- const destDir = import_node_path49.default.join(deps.extensionsRoot, localExtId);
55999
+ const destDir = import_node_path51.default.join(deps.extensionsRoot, localExtId);
55555
56000
  let destExists = false;
55556
56001
  try {
55557
56002
  await import_promises6.default.access(destDir);
@@ -55565,16 +56010,16 @@ async function installFromChannel(args, deps) {
55565
56010
  );
55566
56011
  }
55567
56012
  const stage = await import_promises6.default.mkdtemp(
55568
- import_node_path49.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}-`)
55569
56014
  );
55570
56015
  try {
55571
56016
  for (const [name, entry] of Object.entries(zip.files)) {
55572
- const dest = import_node_path49.default.join(stage, name);
56017
+ const dest = import_node_path51.default.join(stage, name);
55573
56018
  if (entry.dir) {
55574
56019
  await import_promises6.default.mkdir(dest, { recursive: true });
55575
56020
  continue;
55576
56021
  }
55577
- await import_promises6.default.mkdir(import_node_path49.default.dirname(dest), { recursive: true });
56022
+ await import_promises6.default.mkdir(import_node_path51.default.dirname(dest), { recursive: true });
55578
56023
  if (name === "manifest.json") {
55579
56024
  const rewritten = { ...parsed.data, id: localExtId };
55580
56025
  await import_promises6.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -55595,10 +56040,11 @@ async function installFromChannel(args, deps) {
55595
56040
 
55596
56041
  // src/extension/update-flow.ts
55597
56042
  var import_promises7 = __toESM(require("fs/promises"), 1);
55598
- var import_node_path50 = __toESM(require("path"), 1);
56043
+ var import_node_path52 = __toESM(require("path"), 1);
55599
56044
  var import_node_os20 = __toESM(require("os"), 1);
55600
- var import_node_crypto16 = __toESM(require("crypto"), 1);
56045
+ var import_node_crypto15 = __toESM(require("crypto"), 1);
55601
56046
  var import_jszip4 = __toESM(require_lib3(), 1);
56047
+ init_src();
55602
56048
  var UpdateError = class extends Error {
55603
56049
  constructor(code, message) {
55604
56050
  super(message);
@@ -55612,11 +56058,11 @@ async function updateFromChannel(args, deps) {
55612
56058
  channelRef.extId,
55613
56059
  channelRef.ownerPrincipalId
55614
56060
  );
55615
- const liveDir = import_node_path50.default.join(deps.extensionsRoot, localExtId);
56061
+ const liveDir = import_node_path52.default.join(deps.extensionsRoot, localExtId);
55616
56062
  const prevDir = `${liveDir}.prev`;
55617
56063
  let existingVersion;
55618
56064
  try {
55619
- const raw = await import_promises7.default.readFile(import_node_path50.default.join(liveDir, "manifest.json"), "utf8");
56065
+ const raw = await import_promises7.default.readFile(import_node_path52.default.join(liveDir, "manifest.json"), "utf8");
55620
56066
  const parsed2 = ExtensionManifestSchema.safeParse(JSON.parse(raw));
55621
56067
  if (!parsed2.success) {
55622
56068
  throw new UpdateError(
@@ -55635,7 +56081,7 @@ async function updateFromChannel(args, deps) {
55635
56081
  if (e instanceof UpdateError) throw e;
55636
56082
  throw e;
55637
56083
  }
55638
- 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");
55639
56085
  if (computed !== snapshotHash) {
55640
56086
  throw new UpdateError(
55641
56087
  "HASH_MISMATCH",
@@ -55649,7 +56095,7 @@ async function updateFromChannel(args, deps) {
55649
56095
  throw new UpdateError("ZIP_INVALID", `failed to load zip: ${e.message}`);
55650
56096
  }
55651
56097
  for (const name of Object.keys(zip.files)) {
55652
- if (name.includes("..") || name.startsWith("/") || import_node_path50.default.isAbsolute(name)) {
56098
+ if (name.includes("..") || name.startsWith("/") || import_node_path52.default.isAbsolute(name)) {
55653
56099
  throw new UpdateError("ZIP_INVALID", `unsafe zip entry: ${name}`);
55654
56100
  }
55655
56101
  }
@@ -55684,16 +56130,16 @@ async function updateFromChannel(args, deps) {
55684
56130
  await import_promises7.default.rm(prevDir, { recursive: true, force: true });
55685
56131
  await import_promises7.default.rename(liveDir, prevDir);
55686
56132
  const stage = await import_promises7.default.mkdtemp(
55687
- import_node_path50.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}-`)
55688
56134
  );
55689
56135
  try {
55690
56136
  for (const [name, entry] of Object.entries(zip.files)) {
55691
- const dest = import_node_path50.default.join(stage, name);
56137
+ const dest = import_node_path52.default.join(stage, name);
55692
56138
  if (entry.dir) {
55693
56139
  await import_promises7.default.mkdir(dest, { recursive: true });
55694
56140
  continue;
55695
56141
  }
55696
- await import_promises7.default.mkdir(import_node_path50.default.dirname(dest), { recursive: true });
56142
+ await import_promises7.default.mkdir(import_node_path52.default.dirname(dest), { recursive: true });
55697
56143
  if (name === "manifest.json") {
55698
56144
  const rewritten = { ...parsed.data, id: localExtId };
55699
56145
  await import_promises7.default.writeFile(dest, JSON.stringify(rewritten, null, 2));
@@ -55744,6 +56190,7 @@ async function rollback(deps, localExtId, liveDir, prevDir) {
55744
56190
  }
55745
56191
 
55746
56192
  // src/handlers/extension.ts
56193
+ init_src();
55747
56194
  function pickChannelRef(frame) {
55748
56195
  const ref = frame.channelRef;
55749
56196
  const parsed = ChannelRefSchema.safeParse(ref);
@@ -55786,7 +56233,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55786
56233
  );
55787
56234
  }
55788
56235
  }
55789
- const manifestPath = import_node_path51.default.join(root, extId, "manifest.json");
56236
+ const manifestPath = import_node_path53.default.join(root, extId, "manifest.json");
55790
56237
  const manifest = await readManifest(root, extId);
55791
56238
  const next = { ...manifest, version: newVersion };
55792
56239
  const tmp = `${manifestPath}.tmp`;
@@ -55794,7 +56241,7 @@ async function rewriteManifestVersion(root, extId, newVersion, previousPublished
55794
56241
  await import_promises8.default.rename(tmp, manifestPath);
55795
56242
  }
55796
56243
  async function readManifest(root, extId) {
55797
- const file = import_node_path51.default.join(root, extId, "manifest.json");
56244
+ const file = import_node_path53.default.join(root, extId, "manifest.json");
55798
56245
  let raw;
55799
56246
  try {
55800
56247
  raw = await import_promises8.default.readFile(file, "utf8");
@@ -55885,7 +56332,7 @@ function buildExtensionHandlers(deps) {
55885
56332
  };
55886
56333
  async function buildSnapshotMeta(extId) {
55887
56334
  const manifest = await readManifest(deps.root, extId);
55888
- const { sha256, buffer } = await bundleExtensionDir(import_node_path51.default.join(deps.root, extId));
56335
+ const { sha256, buffer } = await bundleExtensionDir(import_node_path53.default.join(deps.root, extId));
55889
56336
  return { manifest, contentHash: sha256, buffer };
55890
56337
  }
55891
56338
  const publish = async (frame, _client, ctx) => {
@@ -56066,9 +56513,9 @@ function buildExtensionHandlers(deps) {
56066
56513
  }
56067
56514
 
56068
56515
  // src/app-builder/project-store.ts
56069
- var import_node_fs44 = require("fs");
56516
+ var import_node_fs46 = require("fs");
56070
56517
  var import_node_child_process14 = require("child_process");
56071
- var import_node_path52 = require("path");
56518
+ var import_node_path54 = require("path");
56072
56519
  init_protocol();
56073
56520
  var PROJECTS_DIR = "projects";
56074
56521
  var META_FILE = ".clawd-project.json";
@@ -56082,19 +56529,19 @@ var ProjectStore = class {
56082
56529
  root;
56083
56530
  /** projects/<name>/.clawd-project.json 路径 */
56084
56531
  metaPath(name) {
56085
- return (0, import_node_path52.join)(this.projectsRoot(), name, META_FILE);
56532
+ return (0, import_node_path54.join)(this.projectsRoot(), name, META_FILE);
56086
56533
  }
56087
56534
  /** projects/<name>/ 目录路径(cwd 用) */
56088
56535
  projectDir(name) {
56089
- return (0, import_node_path52.join)(this.projectsRoot(), name);
56536
+ return (0, import_node_path54.join)(this.projectsRoot(), name);
56090
56537
  }
56091
56538
  projectsRoot() {
56092
- return (0, import_node_path52.join)(this.root, PROJECTS_DIR);
56539
+ return (0, import_node_path54.join)(this.root, PROJECTS_DIR);
56093
56540
  }
56094
56541
  async list() {
56095
56542
  let entries;
56096
56543
  try {
56097
- entries = await import_node_fs44.promises.readdir(this.projectsRoot());
56544
+ entries = await import_node_fs46.promises.readdir(this.projectsRoot());
56098
56545
  } catch (err) {
56099
56546
  if (err.code === "ENOENT") return [];
56100
56547
  throw err;
@@ -56102,7 +56549,7 @@ var ProjectStore = class {
56102
56549
  const out = [];
56103
56550
  for (const name of entries) {
56104
56551
  try {
56105
- const raw = await import_node_fs44.promises.readFile(this.metaPath(name), "utf8");
56552
+ const raw = await import_node_fs46.promises.readFile(this.metaPath(name), "utf8");
56106
56553
  const json = JSON.parse(raw);
56107
56554
  let migrated = false;
56108
56555
  if (typeof json.devCommand !== "string" || json.devCommand.length === 0) {
@@ -56113,7 +56560,7 @@ var ProjectStore = class {
56113
56560
  if (parsed.success) {
56114
56561
  out.push(parsed.data);
56115
56562
  if (migrated) {
56116
- void import_node_fs44.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(() => {
56117
56564
  });
56118
56565
  }
56119
56566
  }
@@ -56157,8 +56604,8 @@ var ProjectStore = class {
56157
56604
  throw new Error(`invalid name "${name}": ${validated.error.message}`);
56158
56605
  }
56159
56606
  const dir = this.projectDir(name);
56160
- await import_node_fs44.promises.mkdir(dir, { recursive: true });
56161
- await import_node_fs44.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");
56162
56609
  return meta;
56163
56610
  }
56164
56611
  /**
@@ -56201,7 +56648,7 @@ var ProjectStore = class {
56201
56648
  }
56202
56649
  async delete(name) {
56203
56650
  const dir = this.projectDir(name);
56204
- await import_node_fs44.promises.rm(dir, { recursive: true, force: true });
56651
+ await import_node_fs46.promises.rm(dir, { recursive: true, force: true });
56205
56652
  }
56206
56653
  async updatePort(name, newPort) {
56207
56654
  if (newPort < PROJECT_PORT_MIN || newPort > PROJECT_PORT_MAX) {
@@ -56217,7 +56664,7 @@ var ProjectStore = class {
56217
56664
  throw new Error(`port ${newPort} already used / \u5DF2\u88AB project "${conflict.name}" \u5360\u7528`);
56218
56665
  }
56219
56666
  const updated = { ...target, port: newPort };
56220
- await import_node_fs44.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");
56221
56668
  return updated;
56222
56669
  }
56223
56670
  /**
@@ -56234,7 +56681,7 @@ var ProjectStore = class {
56234
56681
  if (!validated.success) {
56235
56682
  throw new Error(`invalid prodUrl "${url}": ${validated.error.message}`);
56236
56683
  }
56237
- await import_node_fs44.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");
56238
56685
  return validated.data;
56239
56686
  }
56240
56687
  /**
@@ -56255,7 +56702,7 @@ var ProjectStore = class {
56255
56702
  if (!validated.success) {
56256
56703
  throw new Error(`invalid publishJob: ${validated.error.message}`);
56257
56704
  }
56258
- await import_node_fs44.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");
56259
56706
  return validated.data;
56260
56707
  }
56261
56708
  /** 清掉 .clawd-project.json.publishJob 字段。其他字段保持原样。 */
@@ -56270,7 +56717,7 @@ var ProjectStore = class {
56270
56717
  if (!validated.success) {
56271
56718
  throw new Error(`failed to clear publishJob: ${validated.error.message}`);
56272
56719
  }
56273
- await import_node_fs44.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");
56274
56721
  return validated.data;
56275
56722
  }
56276
56723
  };
@@ -56340,7 +56787,7 @@ function listPidsOnPort(port) {
56340
56787
  }
56341
56788
 
56342
56789
  // src/app-builder/publish-registry.ts
56343
- var import_node_crypto17 = require("crypto");
56790
+ var import_node_crypto16 = require("crypto");
56344
56791
  var PublishJobRegistry = class {
56345
56792
  jobs = /* @__PURE__ */ new Map();
56346
56793
  has(name) {
@@ -56357,7 +56804,7 @@ var PublishJobRegistry = class {
56357
56804
  if (this.jobs.has(args.name)) {
56358
56805
  throw new Error(`already publishing: ${args.name}`);
56359
56806
  }
56360
- const jobId = args.jobId ?? `job-${(0, import_node_crypto17.randomUUID)()}`;
56807
+ const jobId = args.jobId ?? `job-${(0, import_node_crypto16.randomUUID)()}`;
56361
56808
  this.jobs.set(args.name, {
56362
56809
  jobId,
56363
56810
  name: args.name,
@@ -56391,8 +56838,8 @@ var PublishJobRegistry = class {
56391
56838
 
56392
56839
  // src/app-builder/publish-job-runner.ts
56393
56840
  var import_node_child_process16 = require("child_process");
56394
- var import_node_fs45 = require("fs");
56395
- var import_node_path53 = require("path");
56841
+ var import_node_fs47 = require("fs");
56842
+ var import_node_path55 = require("path");
56396
56843
 
56397
56844
  // src/app-builder/publish-stage-parser.ts
56398
56845
  var STAGE_RE = /^\s*::stage::(build|deploy|verify)\s*$/;
@@ -56424,10 +56871,10 @@ async function startPublishJob(deps, args) {
56424
56871
  return { jobId: registry2.get(args.name).jobId, status: "already-publishing" };
56425
56872
  }
56426
56873
  const projDir = projectDir(args.name);
56427
- const logPath = (0, import_node_path53.join)(projDir, ".publish.log");
56874
+ const logPath = (0, import_node_path55.join)(projDir, ".publish.log");
56428
56875
  let logStream = null;
56429
56876
  try {
56430
- logStream = (0, import_node_fs45.createWriteStream)(logPath, { flags: "w" });
56877
+ logStream = (0, import_node_fs47.createWriteStream)(logPath, { flags: "w" });
56431
56878
  } catch {
56432
56879
  logStream = null;
56433
56880
  }
@@ -56684,8 +57131,8 @@ async function recoverInterruptedJobs(deps) {
56684
57131
 
56685
57132
  // src/handlers/app-builder.ts
56686
57133
  init_protocol();
56687
- var import_node_path54 = require("path");
56688
- var import_node_fs46 = require("fs");
57134
+ var import_node_path56 = require("path");
57135
+ var import_node_fs48 = require("fs");
56689
57136
  var APP_BUILDER_PERSONAS = ["persona-app-builder", "persona-dataclaw-builder"];
56690
57137
  var DEV_SERVER_READY_TIMEOUT_MS = 3e4;
56691
57138
  async function recoverInterruptedPublishJobs(store, logger) {
@@ -56766,7 +57213,7 @@ function buildAppBuilderHandlers(deps) {
56766
57213
  async function listAllUsersProjects() {
56767
57214
  if (!deps.usersRoot || !deps.getStore) return [];
56768
57215
  const getStore = deps.getStore;
56769
- const userIds = await import_node_fs46.promises.readdir(deps.usersRoot).catch(() => []);
57216
+ const userIds = await import_node_fs48.promises.readdir(deps.usersRoot).catch(() => []);
56770
57217
  const perUser = await Promise.all(
56771
57218
  userIds.map((uid) => getStore(uid).list().catch(() => []))
56772
57219
  );
@@ -56842,8 +57289,8 @@ function buildAppBuilderHandlers(deps) {
56842
57289
  const project = await userStore.create(f.name, reservedPorts);
56843
57290
  try {
56844
57291
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(session.ownerPersonaId ?? "") : deps.personaRoot;
56845
- const templateSrcDir = (0, import_node_path54.join)(personaRoot, "extension-kit", "examples", DEFAULT_TEMPLATE);
56846
- const scaffoldScript = (0, import_node_path54.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");
56847
57294
  const scaffoldResult = await userStore.scaffold(project.name, templateSrcDir, scaffoldScript);
56848
57295
  deps.logger?.info("app-builder.scaffold.done", {
56849
57296
  name: project.name,
@@ -57064,7 +57511,7 @@ function buildAppBuilderHandlers(deps) {
57064
57511
  await userStore.clearPublishJob(args.name);
57065
57512
  }
57066
57513
  const personaRoot = deps.resolvePersonaRoot ? deps.resolvePersonaRoot(boundSession.ownerPersonaId ?? "") : deps.personaRoot;
57067
- const scriptPath = (0, import_node_path54.join)(deps.deployKitRoot, "scripts", "publish.sh");
57514
+ const scriptPath = (0, import_node_path56.join)(deps.deployKitRoot, "scripts", "publish.sh");
57068
57515
  deps.logger?.info("app-builder.publish.start", {
57069
57516
  name: args.name,
57070
57517
  sessionId: boundSession.sessionId,
@@ -57233,7 +57680,8 @@ function buildVisitorHandlers(deps) {
57233
57680
 
57234
57681
  // src/extension/registry.ts
57235
57682
  var import_promises9 = __toESM(require("fs/promises"), 1);
57236
- var import_node_path55 = __toESM(require("path"), 1);
57683
+ var import_node_path57 = __toESM(require("path"), 1);
57684
+ init_src();
57237
57685
  async function loadAll(root) {
57238
57686
  let entries;
57239
57687
  try {
@@ -57246,13 +57694,13 @@ async function loadAll(root) {
57246
57694
  for (const ent of entries) {
57247
57695
  if (!ent.isDirectory()) continue;
57248
57696
  if (ent.name.startsWith(".")) continue;
57249
- records.push(await loadOne(import_node_path55.default.join(root, ent.name), ent.name));
57697
+ records.push(await loadOne(import_node_path57.default.join(root, ent.name), ent.name));
57250
57698
  }
57251
57699
  records.sort((a, b2) => a.extId < b2.extId ? -1 : a.extId > b2.extId ? 1 : 0);
57252
57700
  return records;
57253
57701
  }
57254
57702
  async function loadOne(dir, dirName) {
57255
- const manifestPath = import_node_path55.default.join(dir, "manifest.json");
57703
+ const manifestPath = import_node_path57.default.join(dir, "manifest.json");
57256
57704
  let raw;
57257
57705
  try {
57258
57706
  raw = await import_promises9.default.readFile(manifestPath, "utf8");
@@ -57297,7 +57745,7 @@ async function loadOne(dir, dirName) {
57297
57745
 
57298
57746
  // src/extension/uninstall.ts
57299
57747
  var import_promises10 = __toESM(require("fs/promises"), 1);
57300
- var import_node_path56 = __toESM(require("path"), 1);
57748
+ var import_node_path58 = __toESM(require("path"), 1);
57301
57749
  var UninstallError = class extends Error {
57302
57750
  constructor(code, message) {
57303
57751
  super(message);
@@ -57306,7 +57754,7 @@ var UninstallError = class extends Error {
57306
57754
  code;
57307
57755
  };
57308
57756
  async function uninstall(deps) {
57309
- const dir = import_node_path56.default.join(deps.root, deps.extId);
57757
+ const dir = import_node_path58.default.join(deps.root, deps.extId);
57310
57758
  try {
57311
57759
  await import_promises10.default.access(dir);
57312
57760
  } catch {
@@ -57317,7 +57765,8 @@ async function uninstall(deps) {
57317
57765
  }
57318
57766
 
57319
57767
  // src/handlers/index.ts
57320
- var import_node_crypto18 = require("crypto");
57768
+ var import_node_crypto17 = require("crypto");
57769
+ init_peer_forward();
57321
57770
  function buildMethodHandlers(deps) {
57322
57771
  return {
57323
57772
  ...buildSessionHandlers({
@@ -57350,7 +57799,7 @@ function buildMethodHandlers(deps) {
57350
57799
  const c = deps.contactStore.get(deviceId);
57351
57800
  return c ? { deviceId: c.deviceId, remoteUrl: c.remoteUrl, connectToken: c.connectToken } : null;
57352
57801
  },
57353
- genId: () => (0, import_node_crypto18.randomUUID)(),
57802
+ genId: () => (0, import_node_crypto17.randomUUID)(),
57354
57803
  now: () => Date.now(),
57355
57804
  forwardInboxPostToPeer,
57356
57805
  logger: deps.logger
@@ -57364,7 +57813,8 @@ function buildMethodHandlers(deps) {
57364
57813
  ...buildContactSshHandlers({
57365
57814
  store: deps.contactStore,
57366
57815
  broadcast: deps.broadcastToOwners,
57367
- sshdDir: deps.sshdDir
57816
+ sshdDir: deps.sshdDir,
57817
+ sshLog: deps.contactSshLog
57368
57818
  }),
57369
57819
  whoami: buildWhoamiHandler({
57370
57820
  ownerDisplayName: deps.ownerDisplayName,
@@ -57888,11 +58338,12 @@ async function dispatchRpc(method, frame, client, ctx, deps) {
57888
58338
 
57889
58339
  // src/extension/runtime.ts
57890
58340
  var import_node_child_process18 = require("child_process");
57891
- var import_node_path57 = __toESM(require("path"), 1);
58341
+ var import_node_path59 = __toESM(require("path"), 1);
57892
58342
  var import_promises11 = require("timers/promises");
58343
+ init_src();
57893
58344
 
57894
58345
  // src/extension/port-allocator.ts
57895
- var import_node_net2 = __toESM(require("net"), 1);
58346
+ var import_node_net3 = __toESM(require("net"), 1);
57896
58347
  var PortExhaustedError = class extends Error {
57897
58348
  constructor(min, max) {
57898
58349
  super(`no free port in [${min},${max}]`);
@@ -57905,7 +58356,7 @@ var PortExhaustedError = class extends Error {
57905
58356
  };
57906
58357
  function probe(port) {
57907
58358
  return new Promise((resolve6) => {
57908
- const srv = import_node_net2.default.createServer();
58359
+ const srv = import_node_net3.default.createServer();
57909
58360
  srv.once("error", () => resolve6(false));
57910
58361
  srv.once("listening", () => {
57911
58362
  srv.close(() => resolve6(true));
@@ -57989,7 +58440,7 @@ var Runtime = class {
57989
58440
  /\$CLAWOS_EXT_PORT/g,
57990
58441
  String(port)
57991
58442
  );
57992
- const dir = import_node_path57.default.join(this.root, extId);
58443
+ const dir = import_node_path59.default.join(this.root, extId);
57993
58444
  const env = {
57994
58445
  ...process.env,
57995
58446
  CLAWOS_EXT_PORT: String(port),
@@ -58101,7 +58552,8 @@ ${handle.stderrTail}`
58101
58552
 
58102
58553
  // src/extension/published-channels.ts
58103
58554
  var import_promises12 = __toESM(require("fs/promises"), 1);
58104
- var import_node_path58 = __toESM(require("path"), 1);
58555
+ var import_node_path60 = __toESM(require("path"), 1);
58556
+ init_src();
58105
58557
  init_zod();
58106
58558
  var PublishedChannelsError = class extends Error {
58107
58559
  constructor(code, message) {
@@ -58200,7 +58652,7 @@ var PublishedChannelStore = class {
58200
58652
  )
58201
58653
  };
58202
58654
  const tmp = `${this.filePath}.tmp`;
58203
- await import_promises12.default.mkdir(import_node_path58.default.dirname(this.filePath), { recursive: true });
58655
+ await import_promises12.default.mkdir(import_node_path60.default.dirname(this.filePath), { recursive: true });
58204
58656
  await import_promises12.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
58205
58657
  await import_promises12.default.rename(tmp, this.filePath);
58206
58658
  }
@@ -58208,7 +58660,7 @@ var PublishedChannelStore = class {
58208
58660
 
58209
58661
  // src/extension/bundle-cache.ts
58210
58662
  var import_promises13 = __toESM(require("fs/promises"), 1);
58211
- var import_node_path59 = __toESM(require("path"), 1);
58663
+ var import_node_path61 = __toESM(require("path"), 1);
58212
58664
  var BundleCache = class {
58213
58665
  constructor(rootDir) {
58214
58666
  this.rootDir = rootDir;
@@ -58217,14 +58669,14 @@ var BundleCache = class {
58217
58669
  /** Atomic write: stage tmp → rename. Caller passes the hex sha256. */
58218
58670
  async write(snapshotHash, buffer) {
58219
58671
  await import_promises13.default.mkdir(this.rootDir, { recursive: true });
58220
- const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
58672
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
58221
58673
  const tmp = `${file}.tmp`;
58222
58674
  await import_promises13.default.writeFile(tmp, buffer, { mode: 384 });
58223
58675
  await import_promises13.default.rename(tmp, file);
58224
58676
  }
58225
58677
  /** Returns the bundle bytes, or null when the file doesn't exist. */
58226
58678
  async read(snapshotHash) {
58227
- const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
58679
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
58228
58680
  try {
58229
58681
  return await import_promises13.default.readFile(file);
58230
58682
  } catch (e) {
@@ -58234,7 +58686,7 @@ var BundleCache = class {
58234
58686
  }
58235
58687
  /** Idempotent — missing file is not an error. */
58236
58688
  async delete(snapshotHash) {
58237
- const file = import_node_path59.default.join(this.rootDir, `${snapshotHash}.zip`);
58689
+ const file = import_node_path61.default.join(this.rootDir, `${snapshotHash}.zip`);
58238
58690
  await import_promises13.default.rm(file, { force: true });
58239
58691
  }
58240
58692
  };
@@ -58259,17 +58711,10 @@ async function startDaemon(config) {
58259
58711
  });
58260
58712
  const logger = createLogger({
58261
58713
  level: config.logLevel,
58262
- file: import_node_path60.default.join(config.dataDir, "clawd.log"),
58714
+ file: import_node_path62.default.join(config.dataDir, "clawd.log"),
58263
58715
  logClient
58264
58716
  });
58265
58717
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
58266
- const screenIdleProbeLogger = createFileOnlyLogger({
58267
- file: import_node_path60.default.join(config.dataDir, "screen-idle-probe.log"),
58268
- level: "debug"
58269
- });
58270
- logger.info("screen-idle probe logger enabled", {
58271
- file: import_node_path60.default.join(config.dataDir, "screen-idle-probe.log")
58272
- });
58273
58718
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
58274
58719
  const pre = stateMgr.preflight();
58275
58720
  if (pre.status === "active") {
@@ -58406,8 +58851,8 @@ async function startDaemon(config) {
58406
58851
  const agents = new AgentsScanner();
58407
58852
  const history = new ClaudeHistoryReader();
58408
58853
  let transport = null;
58409
- const personaStore = new PersonaStore(import_node_path60.default.join(config.dataDir, "personas"));
58410
- const usersRoot = import_node_path60.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");
58411
58856
  const defaultsRoot = findDefaultsRoot(logger);
58412
58857
  if (defaultsRoot) {
58413
58858
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -58427,17 +58872,17 @@ async function startDaemon(config) {
58427
58872
  migrateCodexSandbox({ store: personaStore, logger });
58428
58873
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
58429
58874
  const personaDispatchManager = new PersonaDispatchManager({ genId: () => v4_default() });
58430
- const here = typeof __dirname === "string" ? __dirname : import_node_path60.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));
58431
58876
  const dispatchServerCandidates = [
58432
- import_node_path60.default.join(here, "dispatch", "mcp-server.cjs"),
58877
+ import_node_path62.default.join(here, "dispatch", "mcp-server.cjs"),
58433
58878
  // 生产 dist/index → dist/dispatch/mcp-server.cjs
58434
- import_node_path60.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
58879
+ import_node_path62.default.join(here, "..", "dist", "dispatch", "mcp-server.cjs")
58435
58880
  // dev tsx src/index → ../dist/dispatch/mcp-server.cjs
58436
58881
  ];
58437
- const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
58882
+ const dispatchServerScriptPath = dispatchServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
58438
58883
  let dispatchMcpConfigPath2;
58439
58884
  if (dispatchServerScriptPath) {
58440
- const dispatchLogPath = import_node_path60.default.join(config.dataDir, "dispatch-mcp-server.log");
58885
+ const dispatchLogPath = import_node_path62.default.join(config.dataDir, "dispatch-mcp-server.log");
58441
58886
  dispatchMcpConfigPath2 = writeDispatchMcpConfig({
58442
58887
  dataDir: config.dataDir,
58443
58888
  serverScriptPath: dispatchServerScriptPath,
@@ -58454,15 +58899,15 @@ async function startDaemon(config) {
58454
58899
  });
58455
58900
  }
58456
58901
  const ticketServerCandidates = [
58457
- import_node_path60.default.join(here, "ticket", "mcp-server.cjs"),
58458
- import_node_path60.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")
58459
58904
  ];
58460
- const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
58905
+ const ticketServerScriptPath = ticketServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
58461
58906
  const ticketOwnerUnionId = feishuIdentity?.identity.unionId ?? "";
58462
58907
  const ticketOwnerName = feishuIdentity?.identity.displayName ?? "";
58463
58908
  let ticketMcpConfigPath2;
58464
58909
  if (ticketServerScriptPath && ticketOwnerUnionId) {
58465
- const ticketLogPath = import_node_path60.default.join(config.dataDir, "ticket-mcp-server.log");
58910
+ const ticketLogPath = import_node_path62.default.join(config.dataDir, "ticket-mcp-server.log");
58466
58911
  ticketMcpConfigPath2 = writeTicketMcpConfig({
58467
58912
  dataDir: config.dataDir,
58468
58913
  serverScriptPath: ticketServerScriptPath,
@@ -58483,13 +58928,13 @@ async function startDaemon(config) {
58483
58928
  });
58484
58929
  }
58485
58930
  const shiftServerCandidates = [
58486
- import_node_path60.default.join(here, "shift", "mcp-server.cjs"),
58487
- import_node_path60.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")
58488
58933
  ];
58489
- const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
58934
+ const shiftServerScriptPath = shiftServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
58490
58935
  let shiftMcpConfigPath2;
58491
58936
  if (shiftServerScriptPath) {
58492
- const shiftLogPath = import_node_path60.default.join(config.dataDir, "shift-mcp-server.log");
58937
+ const shiftLogPath = import_node_path62.default.join(config.dataDir, "shift-mcp-server.log");
58493
58938
  shiftMcpConfigPath2 = await writeShiftMcpConfig({
58494
58939
  dataDir: config.dataDir,
58495
58940
  serverScriptPath: shiftServerScriptPath,
@@ -58507,13 +58952,13 @@ async function startDaemon(config) {
58507
58952
  );
58508
58953
  }
58509
58954
  const inboxServerCandidates = [
58510
- import_node_path60.default.join(here, "inbox", "mcp-server.cjs"),
58511
- import_node_path60.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")
58512
58957
  ];
58513
- const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs47.default.existsSync(p2));
58958
+ const inboxServerScriptPath = inboxServerCandidates.find((p2) => import_node_fs49.default.existsSync(p2));
58514
58959
  let inboxMcpConfigPath2;
58515
58960
  if (inboxServerScriptPath) {
58516
- const inboxLogPath = import_node_path60.default.join(config.dataDir, "inbox-mcp-server.log");
58961
+ const inboxLogPath = import_node_path62.default.join(config.dataDir, "inbox-mcp-server.log");
58517
58962
  inboxMcpConfigPath2 = await writeInboxMcpConfig({
58518
58963
  dataDir: config.dataDir,
58519
58964
  serverScriptPath: inboxServerScriptPath,
@@ -58531,7 +58976,7 @@ async function startDaemon(config) {
58531
58976
  );
58532
58977
  }
58533
58978
  const shiftStore = createShiftStore({
58534
- filePath: import_node_path60.default.join(config.dataDir, "shift.json"),
58979
+ filePath: import_node_path62.default.join(config.dataDir, "shift.json"),
58535
58980
  ownerIdProvider: () => ownerPrincipalId,
58536
58981
  now: () => Date.now()
58537
58982
  });
@@ -58546,14 +58991,10 @@ async function startDaemon(config) {
58546
58991
  // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
58547
58992
  storeFactory: sessionStoreFactory,
58548
58993
  logger,
58549
- // 取证 probe(可选,CLAWD_SCREEN_IDLE_PROBE=1 时启用):manager turn_end 判定链
58550
- // 的所有决策点打到独立文件,跟 adapter 的 observeScreenIdle probe 共用同一份 file logger,
58551
- // 便于 grep sessionId 时 tui 层 + manager 层交叉时序都在同一文件里
58552
- ...screenIdleProbeLogger ? { screenIdleProbeLogger } : {},
58553
58994
  getAdapter,
58554
58995
  historyReader: history,
58555
58996
  dataDir: config.dataDir,
58556
- personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
58997
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
58557
58998
  usersRoot,
58558
58999
  personaStore,
58559
59000
  ownerDisplayName,
@@ -58596,10 +59037,10 @@ async function startDaemon(config) {
58596
59037
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
58597
59038
  attachmentGroup: {
58598
59039
  onFileEdit: (input) => {
58599
- const absPath = import_node_path60.default.isAbsolute(input.relPath) ? input.relPath : import_node_path60.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);
58600
59041
  let size = 0;
58601
59042
  try {
58602
- size = import_node_fs47.default.statSync(absPath).size;
59043
+ size = import_node_fs49.default.statSync(absPath).size;
58603
59044
  } catch (err) {
58604
59045
  logger.warn("attachment.onFileEdit stat failed", {
58605
59046
  sessionId: input.sessionId,
@@ -58667,10 +59108,10 @@ async function startDaemon(config) {
58667
59108
  onSurfaceUnregister: (tsid) => manager.unregisterSurface(tsid),
58668
59109
  // ReadyGate v2:ReadyDetector emit ready 时投递 reducer 'ready-detected' input
58669
59110
  onReady: (tsid) => manager.dispatchReadyDetected(tsid),
58670
- // 屏幕真稳定 5s 的一次性信号 manager pending turn_duration flush turn_end
58671
- onScreenIdle: (tsid) => manager.notifyScreenIdle(tsid),
58672
- // 取证 probe(默认无条件启用;见 createFileOnlyLogger
58673
- 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)
58674
59115
  }) : new ClaudeAdapter({ logger, historyReader: new ClaudeHistoryReader() });
58675
59116
  registerAdapter("claude", claudeAdapter);
58676
59117
  registerAdapter("codex", new CodexAdapter({ logger, historyReader: new CodexHistoryReader() }));
@@ -58754,6 +59195,7 @@ async function startDaemon(config) {
58754
59195
  const effectivePreviewPorts = Array.from(
58755
59196
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
58756
59197
  );
59198
+ const sshLog = createContactSshLog(config.dataDir);
58757
59199
  const makeHandlers = () => buildMethodHandlers({
58758
59200
  manager,
58759
59201
  workspace,
@@ -58797,11 +59239,11 @@ async function startDaemon(config) {
58797
59239
  // 'persona/<pid>/owner',default 走 'default'。
58798
59240
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58799
59241
  // guest path guard:candidate 必须在 personaRoot 子树或调用者自己的 user-dir 下
58800
- personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
59242
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
58801
59243
  usersRoot
58802
59244
  },
58803
59245
  // workspace/git/history/skills/agents handler 共用的 guest path guard 锚点
58804
- personaRoot: import_node_path60.default.join(config.dataDir, "personas"),
59246
+ personaRoot: import_node_path62.default.join(config.dataDir, "personas"),
58805
59247
  // v2 多人 persona 隔离:handler 派生 guest user-dir 放行
58806
59248
  usersRoot,
58807
59249
  // capability:list / delete handler 依赖
@@ -58823,7 +59265,8 @@ async function startDaemon(config) {
58823
59265
  contactStore,
58824
59266
  // <dataDir>/sshd 绝对路径 —— contact-ssh handlers 用它拼 authorized_keys / keys/ 子路径
58825
59267
  // Task 10 会加 SshdManager 起 sshd;handlers wire 提前挂 sshdDir 让 typecheck 过
58826
- sshdDir: import_node_path60.default.join(config.dataDir, "sshd"),
59268
+ sshdDir: import_node_path62.default.join(config.dataDir, "sshd"),
59269
+ contactSshLog: sshLog,
58827
59270
  // inbox:sendDm 用:sessionId → session 出身(复用 attachment 同款 findOwnedSessionScope)
58828
59271
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid),
58829
59272
  // contact:removed broadcast;复用 capability:tokenIssued 同款通路
@@ -58913,11 +59356,11 @@ async function startDaemon(config) {
58913
59356
  // 发布上线脚手架化 (spec 2026-06-03 §5.2):
58914
59357
  // appBuilderPersonaRoot 用于拼 publish.sh 绝对路径(persona-app-builder 安装在
58915
59358
  // dataDir/personas/persona-app-builder 之下,extension-kit/scripts/publish.sh 是相对路径)。
58916
- appBuilderPersonaRoot: import_node_path60.default.join(config.dataDir, "personas", "persona-app-builder"),
59359
+ appBuilderPersonaRoot: import_node_path62.default.join(config.dataDir, "personas", "persona-app-builder"),
58917
59360
  // 共享 deploy-kit 根:scaffold/publish 脚本骨架 + 阿里云凭证单一真源。
58918
- deployKitRoot: import_node_path60.default.join(config.dataDir, "deploy-kit"),
59361
+ deployKitRoot: import_node_path62.default.join(config.dataDir, "deploy-kit"),
58919
59362
  // scaffold/publish 按当前 session 的 persona 解析其安装根,让每个 persona 用自己的模板/注入配置。
58920
- resolvePersonaRoot: (personaId) => import_node_path60.default.join(config.dataDir, "personas", personaId),
59363
+ resolvePersonaRoot: (personaId) => import_node_path62.default.join(config.dataDir, "personas", personaId),
58921
59364
  // 发布上线脚手架化 (spec 2026-06-03 §5.2.2):
58922
59365
  // 复用 SessionManagerDeps.broadcastFrame 同款 dispatch 逻辑 —— runner 调 manager.send
58923
59366
  // 取回 broadcast 帧后逐帧 push 到 transport,跟 manager 自身的 deps 一致。
@@ -58960,7 +59403,7 @@ async function startDaemon(config) {
58960
59403
  }
58961
59404
  let sourceJsonlPath = "(no transcript yet \u2014 operate from the task description alone)";
58962
59405
  if (sourceFile && sourceFile.toolSessionId) {
58963
- sourceJsonlPath = import_node_path60.default.join(
59406
+ sourceJsonlPath = import_node_path62.default.join(
58964
59407
  import_node_os21.default.homedir(),
58965
59408
  ".claude",
58966
59409
  "projects",
@@ -59149,6 +59592,36 @@ async function startDaemon(config) {
59149
59592
  httpRequestHandler: httpRouter,
59150
59593
  // app-builder build 模式:/preview/<port>/ HMR websocket upgrade 转发的端口白名单(同 http-router 配置)
59151
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
+ },
59152
59625
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
59153
59626
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
59154
59627
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -59260,8 +59733,8 @@ async function startDaemon(config) {
59260
59733
  const lines = [
59261
59734
  `Tunnel: ${r.url}`,
59262
59735
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
59263
- `Frpc config: ${import_node_path60.default.join(config.dataDir, "frpc.toml")}`,
59264
- `Frpc log: ${import_node_path60.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")}`
59265
59738
  ];
59266
59739
  const width = Math.max(...lines.map((l) => l.length));
59267
59740
  const bar = "\u2550".repeat(width + 4);
@@ -59274,8 +59747,8 @@ ${bar}
59274
59747
 
59275
59748
  `);
59276
59749
  try {
59277
- const connectPath = import_node_path60.default.join(config.dataDir, "connect.txt");
59278
- import_node_fs47.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 });
59279
59752
  } catch {
59280
59753
  }
59281
59754
  } catch (err) {
@@ -59309,13 +59782,21 @@ ${bar}
59309
59782
  });
59310
59783
  try {
59311
59784
  await sshdMgr.start();
59312
- rebuildAuthorizedKeys(contactStore, import_node_path60.default.join(config.dataDir, "sshd"));
59785
+ rebuildAuthorizedKeys(contactStore, import_node_path62.default.join(config.dataDir, "sshd"));
59313
59786
  logger.info("sshd: contact-ssh sandbox ready", { port: config.sshdPort });
59314
59787
  } catch (err) {
59315
59788
  logger.warn("sshd start failed; contact SSH grant will not work until fixed", {
59316
59789
  err: err.message
59317
59790
  });
59318
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();
59319
59800
  void reportDevice();
59320
59801
  void fetchServerKey();
59321
59802
  const tickAttachmentGc = () => {
@@ -59350,6 +59831,7 @@ ${bar}
59350
59831
  if (tunnelMgr) {
59351
59832
  await tunnelMgr.stop();
59352
59833
  }
59834
+ contactKeyPuller.stop();
59353
59835
  await sshdMgr.stop().catch((err) => {
59354
59836
  logger.warn("shutdown.sshd-stop-failed", {
59355
59837
  error: err instanceof Error ? err.message : String(err)
@@ -59368,9 +59850,9 @@ ${bar}
59368
59850
  };
59369
59851
  }
59370
59852
  function migrateDropPersonsDir(dataDir) {
59371
- const dir = import_node_path60.default.join(dataDir, "persons");
59853
+ const dir = import_node_path62.default.join(dataDir, "persons");
59372
59854
  try {
59373
- import_node_fs47.default.rmSync(dir, { recursive: true, force: true });
59855
+ import_node_fs49.default.rmSync(dir, { recursive: true, force: true });
59374
59856
  } catch {
59375
59857
  }
59376
59858
  }
@@ -59383,6 +59865,11 @@ async function main() {
59383
59865
  const code = await runCase2(argv.slice(1));
59384
59866
  process.exit(code);
59385
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
+ }
59386
59873
  const parsed = parseArgs(argv);
59387
59874
  if (parsed.help) {
59388
59875
  process.stdout.write(HELP_TEXT);