@clawos-dev/clawd 0.2.64 → 0.2.65-beta.108.410b956

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.
Files changed (2) hide show
  1. package/dist/cli.cjs +1239 -217
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -110,6 +110,17 @@ var init_methods = __esm({
110
110
  // 触发频率低(仅 window resize),response 也是 ack。
111
111
  "session:pty:input",
112
112
  "session:pty:resize",
113
+ // ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
114
+ // 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
115
+ // (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
116
+ // 全部管理类 RPC handler 入口 requireOwner(personal token 调任意一个都 403);
117
+ // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,签名验证),不在白名单内。
118
+ "attachment.signUrl",
119
+ "attachment.groupAdd",
120
+ "attachment.groupRemove",
121
+ "attachment.groupList",
122
+ // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
123
+ "attachment.groupListPersona",
113
124
  "info",
114
125
  "ping"
115
126
  ];
@@ -601,8 +612,8 @@ var init_parseUtil = __esm({
601
612
  init_errors2();
602
613
  init_en();
603
614
  makeIssue = (params) => {
604
- const { data, path: path28, errorMaps, issueData } = params;
605
- const fullPath = [...path28, ...issueData.path || []];
615
+ const { data, path: path31, errorMaps, issueData } = params;
616
+ const fullPath = [...path31, ...issueData.path || []];
606
617
  const fullIssue = {
607
618
  ...issueData,
608
619
  path: fullPath
@@ -913,11 +924,11 @@ var init_types = __esm({
913
924
  init_parseUtil();
914
925
  init_util();
915
926
  ParseInputLazyPath = class {
916
- constructor(parent, value, path28, key) {
927
+ constructor(parent, value, path31, key) {
917
928
  this._cachedPath = [];
918
929
  this.parent = parent;
919
930
  this.data = value;
920
- this._path = path28;
931
+ this._path = path31;
921
932
  this._key = key;
922
933
  }
923
934
  get path() {
@@ -4300,6 +4311,80 @@ var init_zod = __esm({
4300
4311
  }
4301
4312
  });
4302
4313
 
4314
+ // ../protocol/src/attachment-schemas.ts
4315
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4316
+ var init_attachment_schemas = __esm({
4317
+ "../protocol/src/attachment-schemas.ts"() {
4318
+ "use strict";
4319
+ init_zod();
4320
+ TOKEN_ROLES = ["owner", "personal"];
4321
+ GROUP_FILE_SOURCES = ["agent", "owner"];
4322
+ GroupFileEntrySchema = external_exports.object({
4323
+ /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
4324
+ id: external_exports.string().min(1),
4325
+ /** 相对 personaDir / sessionCwd 的路径 */
4326
+ relPath: external_exports.string().min(1),
4327
+ from: external_exports.enum(GROUP_FILE_SOURCES),
4328
+ /** owner 手动加入时的可选备注 */
4329
+ label: external_exports.string().optional(),
4330
+ /** 文件字节数(stat 时拍) */
4331
+ size: external_exports.number().int().nonnegative(),
4332
+ mime: external_exports.string().min(1),
4333
+ /** 加入清单时间戳(ms) */
4334
+ addedAt: external_exports.number().int().nonnegative(),
4335
+ /** 最近一次 agent Write/Edit 时间戳(agent 反复改同 path 时更新) */
4336
+ lastEditedAt: external_exports.number().int().nonnegative().optional(),
4337
+ /** agent rm / 文件不见了时打标;UI 灰显,不能再 share */
4338
+ stale: external_exports.boolean().optional()
4339
+ });
4340
+ AttachmentSignUrlArgs = external_exports.object({
4341
+ /** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
4342
+ absPath: external_exports.string().min(1),
4343
+ /** TTL 秒数;缺省 24h;'never' 走 null 不带 exp 字段(永久有效) */
4344
+ ttlSeconds: external_exports.number().int().positive().nullable().optional()
4345
+ });
4346
+ AttachmentSignUrlResponseSchema = external_exports.object({
4347
+ /** 完整 URL(含 httpBaseUrl 前缀),UI 直接 window.open / 复制到剪贴板 */
4348
+ url: external_exports.string().min(1),
4349
+ /** 失效时间戳(ms);null = 永久 */
4350
+ expiresAt: external_exports.number().int().nonnegative().nullable()
4351
+ });
4352
+ AttachmentGroupAddArgs = external_exports.object({
4353
+ sessionId: external_exports.string().min(1),
4354
+ relPath: external_exports.string().min(1),
4355
+ /** owner 手动加入时可选备注;agent 自动入清单不走此 RPC */
4356
+ label: external_exports.string().optional()
4357
+ });
4358
+ AttachmentGroupAddResponseSchema = external_exports.object({
4359
+ entry: GroupFileEntrySchema
4360
+ });
4361
+ AttachmentGroupRemoveArgs = external_exports.object({
4362
+ sessionId: external_exports.string().min(1),
4363
+ relPath: external_exports.string().min(1)
4364
+ });
4365
+ AttachmentGroupRemoveResponseSchema = external_exports.object({
4366
+ removed: external_exports.literal(true)
4367
+ });
4368
+ AttachmentGroupListArgs = external_exports.object({
4369
+ sessionId: external_exports.string().min(1)
4370
+ });
4371
+ AttachmentGroupListResponseSchema = external_exports.object({
4372
+ entries: external_exports.array(GroupFileEntrySchema)
4373
+ });
4374
+ AttachmentGroupListPersonaArgs = external_exports.object({
4375
+ personaId: external_exports.string().min(1)
4376
+ });
4377
+ AttachmentGroupListPersonaResponseSchema = external_exports.object({
4378
+ perSession: external_exports.array(
4379
+ external_exports.object({
4380
+ sessionId: external_exports.string().min(1),
4381
+ entries: external_exports.array(GroupFileEntrySchema)
4382
+ })
4383
+ )
4384
+ });
4385
+ }
4386
+ });
4387
+
4303
4388
  // ../protocol/src/persona-schemas.ts
4304
4389
  var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema, FRAME_TYPE_CHAT_OPEN, FRAME_TYPE_CHAT_RENAME, FRAME_TYPE_CHAT_DELETE, FRAME_TYPE_CHAT_LIST, FRAME_TYPE_CHAT_CREATED, FRAME_TYPE_CHAT_RENAMED, FRAME_TYPE_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_CHAT_CREATED, FRAME_TYPE_PERSONA_LISTENER_CHAT_RENAMED, FRAME_TYPE_PERSONA_LISTENER_CHAT_DELETED, FRAME_TYPE_PERSONA_LISTENER_STATUS, ChatSummarySchema, ChatOpenRequestSchema, ChatRenameRequestSchema, ChatDeleteRequestSchema, ChatRenameResponseSchema, ChatDeleteResponseSchema, ChatListPushSchema, ChatCreatedFrameSchema, ChatRenamedFrameSchema, ChatDeletedFrameSchema, PersonaListenerChatCreatedFrameSchema, PersonaListenerChatRenamedFrameSchema, PersonaListenerChatDeletedFrameSchema, PersonaListenerStatusFrameSchema, PersonaListListenerChatsForTokenArgsSchema, PersonaListListenerChatsForTokenResponseSchema;
4305
4390
  var init_persona_schemas = __esm({
@@ -4505,6 +4590,7 @@ var init_schemas = __esm({
4505
4590
  "use strict";
4506
4591
  init_zod();
4507
4592
  init_events();
4593
+ init_attachment_schemas();
4508
4594
  init_persona_schemas();
4509
4595
  SessionStatusSchema = external_exports.enum(SESSION_STATUS_VALUES);
4510
4596
  UsageSchema = external_exports.object({
@@ -5061,7 +5147,22 @@ var init_schemas = __esm({
5061
5147
  hostname: external_exports.string(),
5062
5148
  os: external_exports.string(),
5063
5149
  tools: external_exports.array(external_exports.object({ id: external_exports.string(), available: external_exports.boolean() })),
5064
- runningSessions: external_exports.array(InfoRunningSessionSchema)
5150
+ runningSessions: external_exports.array(InfoRunningSessionSchema),
5151
+ // ── file-sharing 会话身份回执(spec: superpowers/specs/2026-05-19-clawd-file-sharing-design.md §8) ──
5152
+ // PR 1 阶段标 optional(daemon 还没下发,旧 daemon info 响应不带这五个字段);
5153
+ // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5154
+ // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5155
+ // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5156
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5157
+ tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5158
+ /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5159
+ tokenPersonaId: external_exports.string().min(1).optional(),
5160
+ /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5161
+ isLoopback: external_exports.boolean().optional(),
5162
+ /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
5163
+ httpBaseUrl: external_exports.string().optional(),
5164
+ /** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
5165
+ httpToken: external_exports.string().optional()
5065
5166
  });
5066
5167
  }
5067
5168
  });
@@ -5093,6 +5194,7 @@ var init_runtime = __esm({
5093
5194
  init_frames();
5094
5195
  init_persona_schemas();
5095
5196
  init_persona_mode();
5197
+ init_attachment_schemas();
5096
5198
  }
5097
5199
  });
5098
5200
 
@@ -5367,8 +5469,8 @@ var require_req = __commonJS({
5367
5469
  if (req.originalUrl) {
5368
5470
  _req.url = req.originalUrl;
5369
5471
  } else {
5370
- const path28 = req.path;
5371
- _req.url = typeof path28 === "string" ? path28 : req.url ? req.url.path || req.url : void 0;
5472
+ const path31 = req.path;
5473
+ _req.url = typeof path31 === "string" ? path31 : req.url ? req.url.path || req.url : void 0;
5372
5474
  }
5373
5475
  if (req.query) {
5374
5476
  _req.query = req.query;
@@ -5533,14 +5635,14 @@ var require_redact = __commonJS({
5533
5635
  }
5534
5636
  return obj;
5535
5637
  }
5536
- function parsePath(path28) {
5638
+ function parsePath(path31) {
5537
5639
  const parts = [];
5538
5640
  let current = "";
5539
5641
  let inBrackets = false;
5540
5642
  let inQuotes = false;
5541
5643
  let quoteChar = "";
5542
- for (let i = 0; i < path28.length; i++) {
5543
- const char = path28[i];
5644
+ for (let i = 0; i < path31.length; i++) {
5645
+ const char = path31[i];
5544
5646
  if (!inBrackets && char === ".") {
5545
5647
  if (current) {
5546
5648
  parts.push(current);
@@ -5671,10 +5773,10 @@ var require_redact = __commonJS({
5671
5773
  return current;
5672
5774
  }
5673
5775
  function redactPaths(obj, paths, censor, remove = false) {
5674
- for (const path28 of paths) {
5675
- const parts = parsePath(path28);
5776
+ for (const path31 of paths) {
5777
+ const parts = parsePath(path31);
5676
5778
  if (parts.includes("*")) {
5677
- redactWildcardPath(obj, parts, censor, path28, remove);
5779
+ redactWildcardPath(obj, parts, censor, path31, remove);
5678
5780
  } else {
5679
5781
  if (remove) {
5680
5782
  removeKey(obj, parts);
@@ -5759,8 +5861,8 @@ var require_redact = __commonJS({
5759
5861
  }
5760
5862
  } else {
5761
5863
  if (afterWildcard.includes("*")) {
5762
- const wrappedCensor = typeof censor === "function" ? (value, path28) => {
5763
- const fullPath = [...pathArray.slice(0, pathLength), ...path28];
5864
+ const wrappedCensor = typeof censor === "function" ? (value, path31) => {
5865
+ const fullPath = [...pathArray.slice(0, pathLength), ...path31];
5764
5866
  return censor(value, fullPath);
5765
5867
  } : censor;
5766
5868
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5795,8 +5897,8 @@ var require_redact = __commonJS({
5795
5897
  return null;
5796
5898
  }
5797
5899
  const pathStructure = /* @__PURE__ */ new Map();
5798
- for (const path28 of pathsToClone) {
5799
- const parts = parsePath(path28);
5900
+ for (const path31 of pathsToClone) {
5901
+ const parts = parsePath(path31);
5800
5902
  let current = pathStructure;
5801
5903
  for (let i = 0; i < parts.length; i++) {
5802
5904
  const part = parts[i];
@@ -5848,24 +5950,24 @@ var require_redact = __commonJS({
5848
5950
  }
5849
5951
  return cloneSelectively(obj, pathStructure);
5850
5952
  }
5851
- function validatePath(path28) {
5852
- if (typeof path28 !== "string") {
5953
+ function validatePath(path31) {
5954
+ if (typeof path31 !== "string") {
5853
5955
  throw new Error("Paths must be (non-empty) strings");
5854
5956
  }
5855
- if (path28 === "") {
5957
+ if (path31 === "") {
5856
5958
  throw new Error("Invalid redaction path ()");
5857
5959
  }
5858
- if (path28.includes("..")) {
5859
- throw new Error(`Invalid redaction path (${path28})`);
5960
+ if (path31.includes("..")) {
5961
+ throw new Error(`Invalid redaction path (${path31})`);
5860
5962
  }
5861
- if (path28.includes(",")) {
5862
- throw new Error(`Invalid redaction path (${path28})`);
5963
+ if (path31.includes(",")) {
5964
+ throw new Error(`Invalid redaction path (${path31})`);
5863
5965
  }
5864
5966
  let bracketCount = 0;
5865
5967
  let inQuotes = false;
5866
5968
  let quoteChar = "";
5867
- for (let i = 0; i < path28.length; i++) {
5868
- const char = path28[i];
5969
+ for (let i = 0; i < path31.length; i++) {
5970
+ const char = path31[i];
5869
5971
  if ((char === '"' || char === "'") && bracketCount > 0) {
5870
5972
  if (!inQuotes) {
5871
5973
  inQuotes = true;
@@ -5879,20 +5981,20 @@ var require_redact = __commonJS({
5879
5981
  } else if (char === "]" && !inQuotes) {
5880
5982
  bracketCount--;
5881
5983
  if (bracketCount < 0) {
5882
- throw new Error(`Invalid redaction path (${path28})`);
5984
+ throw new Error(`Invalid redaction path (${path31})`);
5883
5985
  }
5884
5986
  }
5885
5987
  }
5886
5988
  if (bracketCount !== 0) {
5887
- throw new Error(`Invalid redaction path (${path28})`);
5989
+ throw new Error(`Invalid redaction path (${path31})`);
5888
5990
  }
5889
5991
  }
5890
5992
  function validatePaths(paths) {
5891
5993
  if (!Array.isArray(paths)) {
5892
5994
  throw new TypeError("paths must be an array");
5893
5995
  }
5894
- for (const path28 of paths) {
5895
- validatePath(path28);
5996
+ for (const path31 of paths) {
5997
+ validatePath(path31);
5896
5998
  }
5897
5999
  }
5898
6000
  function slowRedact(options = {}) {
@@ -6060,8 +6162,8 @@ var require_redaction = __commonJS({
6060
6162
  if (shape[k2] === null) {
6061
6163
  o[k2] = (value) => topCensor(value, [k2]);
6062
6164
  } else {
6063
- const wrappedCensor = typeof censor === "function" ? (value, path28) => {
6064
- return censor(value, [k2, ...path28]);
6165
+ const wrappedCensor = typeof censor === "function" ? (value, path31) => {
6166
+ return censor(value, [k2, ...path31]);
6065
6167
  } : censor;
6066
6168
  o[k2] = Redact({
6067
6169
  paths: shape[k2],
@@ -6279,10 +6381,10 @@ var require_atomic_sleep = __commonJS({
6279
6381
  var require_sonic_boom = __commonJS({
6280
6382
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6281
6383
  "use strict";
6282
- var fs26 = require("fs");
6384
+ var fs28 = require("fs");
6283
6385
  var EventEmitter2 = require("events");
6284
6386
  var inherits = require("util").inherits;
6285
- var path28 = require("path");
6387
+ var path31 = require("path");
6286
6388
  var sleep = require_atomic_sleep();
6287
6389
  var assert = require("assert");
6288
6390
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6336,20 +6438,20 @@ var require_sonic_boom = __commonJS({
6336
6438
  const mode = sonic.mode;
6337
6439
  if (sonic.sync) {
6338
6440
  try {
6339
- if (sonic.mkdir) fs26.mkdirSync(path28.dirname(file), { recursive: true });
6340
- const fd = fs26.openSync(file, flags, mode);
6441
+ if (sonic.mkdir) fs28.mkdirSync(path31.dirname(file), { recursive: true });
6442
+ const fd = fs28.openSync(file, flags, mode);
6341
6443
  fileOpened(null, fd);
6342
6444
  } catch (err) {
6343
6445
  fileOpened(err);
6344
6446
  throw err;
6345
6447
  }
6346
6448
  } else if (sonic.mkdir) {
6347
- fs26.mkdir(path28.dirname(file), { recursive: true }, (err) => {
6449
+ fs28.mkdir(path31.dirname(file), { recursive: true }, (err) => {
6348
6450
  if (err) return fileOpened(err);
6349
- fs26.open(file, flags, mode, fileOpened);
6451
+ fs28.open(file, flags, mode, fileOpened);
6350
6452
  });
6351
6453
  } else {
6352
- fs26.open(file, flags, mode, fileOpened);
6454
+ fs28.open(file, flags, mode, fileOpened);
6353
6455
  }
6354
6456
  }
6355
6457
  function SonicBoom(opts) {
@@ -6390,8 +6492,8 @@ var require_sonic_boom = __commonJS({
6390
6492
  this.flush = flushBuffer;
6391
6493
  this.flushSync = flushBufferSync;
6392
6494
  this._actualWrite = actualWriteBuffer;
6393
- fsWriteSync = () => fs26.writeSync(this.fd, this._writingBuf);
6394
- fsWrite = () => fs26.write(this.fd, this._writingBuf, this.release);
6495
+ fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
6496
+ fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
6395
6497
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6396
6498
  this._writingBuf = "";
6397
6499
  this.write = write;
@@ -6400,15 +6502,15 @@ var require_sonic_boom = __commonJS({
6400
6502
  this._actualWrite = actualWrite;
6401
6503
  fsWriteSync = () => {
6402
6504
  if (Buffer.isBuffer(this._writingBuf)) {
6403
- return fs26.writeSync(this.fd, this._writingBuf);
6505
+ return fs28.writeSync(this.fd, this._writingBuf);
6404
6506
  }
6405
- return fs26.writeSync(this.fd, this._writingBuf, "utf8");
6507
+ return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6406
6508
  };
6407
6509
  fsWrite = () => {
6408
6510
  if (Buffer.isBuffer(this._writingBuf)) {
6409
- return fs26.write(this.fd, this._writingBuf, this.release);
6511
+ return fs28.write(this.fd, this._writingBuf, this.release);
6410
6512
  }
6411
- return fs26.write(this.fd, this._writingBuf, "utf8", this.release);
6513
+ return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6412
6514
  };
6413
6515
  } else {
6414
6516
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6465,7 +6567,7 @@ var require_sonic_boom = __commonJS({
6465
6567
  }
6466
6568
  }
6467
6569
  if (this._fsync) {
6468
- fs26.fsyncSync(this.fd);
6570
+ fs28.fsyncSync(this.fd);
6469
6571
  }
6470
6572
  const len = this._len;
6471
6573
  if (this._reopening) {
@@ -6579,7 +6681,7 @@ var require_sonic_boom = __commonJS({
6579
6681
  const onDrain = () => {
6580
6682
  if (!this._fsync) {
6581
6683
  try {
6582
- fs26.fsync(this.fd, (err) => {
6684
+ fs28.fsync(this.fd, (err) => {
6583
6685
  this._flushPending = false;
6584
6686
  cb(err);
6585
6687
  });
@@ -6681,7 +6783,7 @@ var require_sonic_boom = __commonJS({
6681
6783
  const fd = this.fd;
6682
6784
  this.once("ready", () => {
6683
6785
  if (fd !== this.fd) {
6684
- fs26.close(fd, (err) => {
6786
+ fs28.close(fd, (err) => {
6685
6787
  if (err) {
6686
6788
  return this.emit("error", err);
6687
6789
  }
@@ -6730,7 +6832,7 @@ var require_sonic_boom = __commonJS({
6730
6832
  buf = this._bufs[0];
6731
6833
  }
6732
6834
  try {
6733
- const n = Buffer.isBuffer(buf) ? fs26.writeSync(this.fd, buf) : fs26.writeSync(this.fd, buf, "utf8");
6835
+ const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
6734
6836
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6735
6837
  buf = releasedBufObj.writingBuf;
6736
6838
  this._len = releasedBufObj.len;
@@ -6746,7 +6848,7 @@ var require_sonic_boom = __commonJS({
6746
6848
  }
6747
6849
  }
6748
6850
  try {
6749
- fs26.fsyncSync(this.fd);
6851
+ fs28.fsyncSync(this.fd);
6750
6852
  } catch {
6751
6853
  }
6752
6854
  }
@@ -6767,7 +6869,7 @@ var require_sonic_boom = __commonJS({
6767
6869
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6768
6870
  }
6769
6871
  try {
6770
- const n = fs26.writeSync(this.fd, buf);
6872
+ const n = fs28.writeSync(this.fd, buf);
6771
6873
  buf = buf.subarray(n);
6772
6874
  this._len = Math.max(this._len - n, 0);
6773
6875
  if (buf.length <= 0) {
@@ -6795,13 +6897,13 @@ var require_sonic_boom = __commonJS({
6795
6897
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6796
6898
  if (this.sync) {
6797
6899
  try {
6798
- const written = Buffer.isBuffer(this._writingBuf) ? fs26.writeSync(this.fd, this._writingBuf) : fs26.writeSync(this.fd, this._writingBuf, "utf8");
6900
+ const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
6799
6901
  release(null, written);
6800
6902
  } catch (err) {
6801
6903
  release(err);
6802
6904
  }
6803
6905
  } else {
6804
- fs26.write(this.fd, this._writingBuf, release);
6906
+ fs28.write(this.fd, this._writingBuf, release);
6805
6907
  }
6806
6908
  }
6807
6909
  function actualWriteBuffer() {
@@ -6810,7 +6912,7 @@ var require_sonic_boom = __commonJS({
6810
6912
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6811
6913
  if (this.sync) {
6812
6914
  try {
6813
- const written = fs26.writeSync(this.fd, this._writingBuf);
6915
+ const written = fs28.writeSync(this.fd, this._writingBuf);
6814
6916
  release(null, written);
6815
6917
  } catch (err) {
6816
6918
  release(err);
@@ -6819,7 +6921,7 @@ var require_sonic_boom = __commonJS({
6819
6921
  if (kCopyBuffer) {
6820
6922
  this._writingBuf = Buffer.from(this._writingBuf);
6821
6923
  }
6822
- fs26.write(this.fd, this._writingBuf, release);
6924
+ fs28.write(this.fd, this._writingBuf, release);
6823
6925
  }
6824
6926
  }
6825
6927
  function actualClose(sonic) {
@@ -6835,12 +6937,12 @@ var require_sonic_boom = __commonJS({
6835
6937
  sonic._lens = [];
6836
6938
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6837
6939
  try {
6838
- fs26.fsync(sonic.fd, closeWrapped);
6940
+ fs28.fsync(sonic.fd, closeWrapped);
6839
6941
  } catch {
6840
6942
  }
6841
6943
  function closeWrapped() {
6842
6944
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6843
- fs26.close(sonic.fd, done);
6945
+ fs28.close(sonic.fd, done);
6844
6946
  } else {
6845
6947
  done();
6846
6948
  }
@@ -9975,11 +10077,11 @@ var init_lib = __esm({
9975
10077
  }
9976
10078
  }
9977
10079
  },
9978
- addToPath: function addToPath(path28, added, removed, oldPosInc, options) {
9979
- var last = path28.lastComponent;
10080
+ addToPath: function addToPath(path31, added, removed, oldPosInc, options) {
10081
+ var last = path31.lastComponent;
9980
10082
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
9981
10083
  return {
9982
- oldPos: path28.oldPos + oldPosInc,
10084
+ oldPos: path31.oldPos + oldPosInc,
9983
10085
  lastComponent: {
9984
10086
  count: last.count + 1,
9985
10087
  added,
@@ -9989,7 +10091,7 @@ var init_lib = __esm({
9989
10091
  };
9990
10092
  } else {
9991
10093
  return {
9992
- oldPos: path28.oldPos + oldPosInc,
10094
+ oldPos: path31.oldPos + oldPosInc,
9993
10095
  lastComponent: {
9994
10096
  count: 1,
9995
10097
  added,
@@ -10420,10 +10522,10 @@ function attachmentToHistoryMessage(o, ts) {
10420
10522
  const memories = raw.map((m2) => {
10421
10523
  if (!m2 || typeof m2 !== "object") return null;
10422
10524
  const rec = m2;
10423
- const path28 = typeof rec.path === "string" ? rec.path : null;
10525
+ const path31 = typeof rec.path === "string" ? rec.path : null;
10424
10526
  const content = typeof rec.content === "string" ? rec.content : null;
10425
- if (!path28 || content == null) return null;
10426
- const entry = { path: path28, content };
10527
+ if (!path31 || content == null) return null;
10528
+ const entry = { path: path31, content };
10427
10529
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10428
10530
  return entry;
10429
10531
  }).filter((m2) => m2 !== null);
@@ -11227,10 +11329,10 @@ function parseAttachment(obj) {
11227
11329
  const memories = raw.map((m2) => {
11228
11330
  if (!m2 || typeof m2 !== "object") return null;
11229
11331
  const rec = m2;
11230
- const path28 = typeof rec.path === "string" ? rec.path : null;
11332
+ const path31 = typeof rec.path === "string" ? rec.path : null;
11231
11333
  const content = typeof rec.content === "string" ? rec.content : null;
11232
- if (!path28 || content == null) return null;
11233
- const out = { path: path28, content };
11334
+ if (!path31 || content == null) return null;
11335
+ const out = { path: path31, content };
11234
11336
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11235
11337
  return out;
11236
11338
  }).filter((m2) => m2 !== null);
@@ -18725,7 +18827,7 @@ var require_websocket = __commonJS({
18725
18827
  "use strict";
18726
18828
  var EventEmitter2 = require("events");
18727
18829
  var https = require("https");
18728
- var http = require("http");
18830
+ var http2 = require("http");
18729
18831
  var net = require("net");
18730
18832
  var tls = require("tls");
18731
18833
  var { randomBytes, createHash } = require("crypto");
@@ -19259,7 +19361,7 @@ var require_websocket = __commonJS({
19259
19361
  }
19260
19362
  const defaultPort = isSecure ? 443 : 80;
19261
19363
  const key = randomBytes(16).toString("base64");
19262
- const request = isSecure ? https.request : http.request;
19364
+ const request = isSecure ? https.request : http2.request;
19263
19365
  const protocolSet = /* @__PURE__ */ new Set();
19264
19366
  let perMessageDeflate;
19265
19367
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -19753,7 +19855,7 @@ var require_websocket_server = __commonJS({
19753
19855
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
19754
19856
  "use strict";
19755
19857
  var EventEmitter2 = require("events");
19756
- var http = require("http");
19858
+ var http2 = require("http");
19757
19859
  var { Duplex } = require("stream");
19758
19860
  var { createHash } = require("crypto");
19759
19861
  var extension2 = require_extension();
@@ -19828,8 +19930,8 @@ var require_websocket_server = __commonJS({
19828
19930
  );
19829
19931
  }
19830
19932
  if (options.port != null) {
19831
- this._server = http.createServer((req, res) => {
19832
- const body = http.STATUS_CODES[426];
19933
+ this._server = http2.createServer((req, res) => {
19934
+ const body = http2.STATUS_CODES[426];
19833
19935
  res.writeHead(426, {
19834
19936
  "Content-Length": body.length,
19835
19937
  "Content-Type": "text/plain"
@@ -20116,7 +20218,7 @@ var require_websocket_server = __commonJS({
20116
20218
  this.destroy();
20117
20219
  }
20118
20220
  function abortHandshake(socket, code, message, headers) {
20119
- message = message || http.STATUS_CODES[code];
20221
+ message = message || http2.STATUS_CODES[code];
20120
20222
  headers = {
20121
20223
  Connection: "close",
20122
20224
  "Content-Type": "text/html",
@@ -20125,7 +20227,7 @@ var require_websocket_server = __commonJS({
20125
20227
  };
20126
20228
  socket.once("finish", socket.destroy);
20127
20229
  socket.end(
20128
- `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r
20230
+ `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20129
20231
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20130
20232
  );
20131
20233
  }
@@ -20144,7 +20246,7 @@ var require_websocket_server = __commonJS({
20144
20246
  // src/run-case/recorder.ts
20145
20247
  function startRunCaseRecorder(opts) {
20146
20248
  const now = opts.now ?? Date.now;
20147
- const dir = import_node_path24.default.dirname(opts.recordPath);
20249
+ const dir = import_node_path27.default.dirname(opts.recordPath);
20148
20250
  let stream = null;
20149
20251
  let closing = false;
20150
20252
  let closedSettled = false;
@@ -20158,8 +20260,8 @@ function startRunCaseRecorder(opts) {
20158
20260
  });
20159
20261
  const ensureStream = () => {
20160
20262
  if (stream) return stream;
20161
- import_node_fs23.default.mkdirSync(dir, { recursive: true });
20162
- stream = import_node_fs23.default.createWriteStream(opts.recordPath, { flags: "a" });
20263
+ import_node_fs25.default.mkdirSync(dir, { recursive: true });
20264
+ stream = import_node_fs25.default.createWriteStream(opts.recordPath, { flags: "a" });
20163
20265
  stream.on("close", () => closedResolve());
20164
20266
  return stream;
20165
20267
  };
@@ -20184,12 +20286,12 @@ function startRunCaseRecorder(opts) {
20184
20286
  };
20185
20287
  return { tap, close, closed };
20186
20288
  }
20187
- var import_node_fs23, import_node_path24;
20289
+ var import_node_fs25, import_node_path27;
20188
20290
  var init_recorder = __esm({
20189
20291
  "src/run-case/recorder.ts"() {
20190
20292
  "use strict";
20191
- import_node_fs23 = __toESM(require("fs"), 1);
20192
- import_node_path24 = __toESM(require("path"), 1);
20293
+ import_node_fs25 = __toESM(require("fs"), 1);
20294
+ import_node_path27 = __toESM(require("path"), 1);
20193
20295
  }
20194
20296
  });
20195
20297
 
@@ -20232,7 +20334,7 @@ var init_wire = __esm({
20232
20334
  // src/run-case/controller.ts
20233
20335
  async function runController(opts) {
20234
20336
  const now = opts.now ?? Date.now;
20235
- const cwd = opts.cwd ?? (0, import_node_fs24.mkdtempSync)(import_node_path25.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
20337
+ const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
20236
20338
  const ownsCwd = opts.cwd === void 0;
20237
20339
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20238
20340
  const spawnCtx = { cwd };
@@ -20393,19 +20495,19 @@ async function runController(opts) {
20393
20495
  if (sigintHandler) process.off("SIGINT", sigintHandler);
20394
20496
  if (ownsCwd) {
20395
20497
  try {
20396
- (0, import_node_fs24.rmSync)(cwd, { recursive: true, force: true });
20498
+ (0, import_node_fs26.rmSync)(cwd, { recursive: true, force: true });
20397
20499
  } catch {
20398
20500
  }
20399
20501
  }
20400
20502
  return exitCode ?? 0;
20401
20503
  }
20402
- var import_node_fs24, import_node_os15, import_node_path25;
20504
+ var import_node_fs26, import_node_os15, import_node_path28;
20403
20505
  var init_controller = __esm({
20404
20506
  "src/run-case/controller.ts"() {
20405
20507
  "use strict";
20406
- import_node_fs24 = require("fs");
20508
+ import_node_fs26 = require("fs");
20407
20509
  import_node_os15 = __toESM(require("os"), 1);
20408
- import_node_path25 = __toESM(require("path"), 1);
20510
+ import_node_path28 = __toESM(require("path"), 1);
20409
20511
  init_claude();
20410
20512
  init_stdout_splitter();
20411
20513
  init_permission_stdio();
@@ -20637,8 +20739,8 @@ Env (advanced):
20637
20739
  `;
20638
20740
 
20639
20741
  // src/index.ts
20640
- var import_node_path23 = __toESM(require("path"), 1);
20641
- var import_node_fs22 = __toESM(require("fs"), 1);
20742
+ var import_node_path26 = __toESM(require("path"), 1);
20743
+ var import_node_fs24 = __toESM(require("fs"), 1);
20642
20744
 
20643
20745
  // src/logger.ts
20644
20746
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -21630,6 +21732,11 @@ var SessionRunner = class {
21630
21732
  // sub-session idle-kill timer ref;key = sessionId(实际同一 runner 只对应一个 session,
21631
21733
  // 但 reducer Effect schema 带 sessionId 作为唯一键,对齐 schedule/cancel 配对)
21632
21734
  idleKillTimers = /* @__PURE__ */ new Map();
21735
+ // file-sharing 待配对的 file-edit tool_use(spec §6 PR 3):在同一 session 流里
21736
+ // tool_use(kind='tool_call')与 tool_result 通过 toolUseId 配对。tool_use 提前到,
21737
+ // 等 tool_result 来时反查 path 调 onFileEdit。条目 in-flight 期间很少(个位数),
21738
+ // 不需要 LRU;session 退出时整个 runner 销毁自然清理。
21739
+ pendingFileEdits = /* @__PURE__ */ new Map();
21633
21740
  getState() {
21634
21741
  return this.state;
21635
21742
  }
@@ -21638,7 +21745,13 @@ var SessionRunner = class {
21638
21745
  const personaStore = this.hooks.personaStore;
21639
21746
  const adapter = this.hooks.adapter;
21640
21747
  const deps = {
21641
- parseLine: (l) => adapter.parseLine(l),
21748
+ // file-sharing (spec §6 PR 3):在 stdout-line 解析时同步观察 file-edit 工具事件,
21749
+ // 配对 tool_use ↔ tool_result 调 onFileEdit。原 reducer 路径不变。
21750
+ parseLine: (l) => {
21751
+ const events = adapter.parseLine(l);
21752
+ if (this.hooks.onFileEdit) this.observeForFileEdit(events);
21753
+ return events;
21754
+ },
21642
21755
  // persona mention injection 在 deps 装配处包一层,对 reducer 完全透明。
21643
21756
  // 命中 mention 时拆成两条 stdin 帧(先 system-reminder 块、后 user 原文):
21644
21757
  // - CC stream-json 按行处理,落盘是两条独立 user message;
@@ -21706,8 +21819,54 @@ var SessionRunner = class {
21706
21819
  // reducer 的 batch dedup 按 events[0].uuid 整组处理,避免跨路径重复。
21707
21820
  feedObserverEvents(events) {
21708
21821
  if (events.length === 0) return;
21822
+ if (this.hooks.onFileEdit) this.observeForFileEdit(events);
21709
21823
  this.input({ kind: "inject-events", events });
21710
21824
  }
21825
+ /**
21826
+ * file-sharing tool_use ↔ tool_result 配对(spec §6 PR 3)。
21827
+ *
21828
+ * 1) tool_call kind + tool ∈ FILE_EDIT_TOOLS → cache toolUseId → { tool, relPath }
21829
+ * relPath 从 input 里取(Write/Edit/MultiEdit 是 file_path,NotebookEdit 是 notebook_path)。
21830
+ * 2) tool_result kind + cache 命中 + 非 error → 调 onFileEdit + 删 cache
21831
+ * 3) tool_result kind + cache 命中 + 是 error → 删 cache,不调(spec 只要"成功"才入清单)
21832
+ * 4) tool_call 但 input 没有 path → 不 cache(防御)
21833
+ *
21834
+ * 不破坏 reducer 路径——纯只读扫描 events 数组。
21835
+ */
21836
+ observeForFileEdit(events) {
21837
+ const cb = this.hooks.onFileEdit;
21838
+ if (!cb) return;
21839
+ for (const ev of events) {
21840
+ if (ev.kind === "tool_call") {
21841
+ const tool = ev.tool;
21842
+ if (!isFileEditTool(tool)) continue;
21843
+ const relPath = extractEditPath(ev.input);
21844
+ if (!relPath) continue;
21845
+ this.pendingFileEdits.set(ev.toolUseId, {
21846
+ tool,
21847
+ relPath
21848
+ });
21849
+ } else if (ev.kind === "tool_result") {
21850
+ const pending = this.pendingFileEdits.get(ev.toolUseId);
21851
+ if (!pending) continue;
21852
+ this.pendingFileEdits.delete(ev.toolUseId);
21853
+ if (ev.error != null) continue;
21854
+ try {
21855
+ cb({
21856
+ toolUseId: ev.toolUseId,
21857
+ tool: pending.tool,
21858
+ relPath: pending.relPath,
21859
+ cwd: this.state.file.cwd
21860
+ });
21861
+ } catch (err) {
21862
+ this.hooks.logger?.warn("onFileEdit hook threw", {
21863
+ err: err.message,
21864
+ sessionId: this.state.file.sessionId
21865
+ });
21866
+ }
21867
+ }
21868
+ }
21869
+ }
21711
21870
  // 向子进程 stdin 写一条 CC IPC `control_request` 并返回 Promise<response>
21712
21871
  // —— CC 侧会异步回写匹配 request_id 的 `control_response` 帧,
21713
21872
  // 在 stdout 行处理入口被 tryHandleControlResponse 拦截并 resolve pending
@@ -21897,6 +22056,15 @@ var SessionRunner = class {
21897
22056
  }
21898
22057
  }
21899
22058
  };
22059
+ function isFileEditTool(name) {
22060
+ return name === "Write" || name === "Edit" || name === "MultiEdit" || name === "NotebookEdit";
22061
+ }
22062
+ function extractEditPath(input) {
22063
+ if (!input || typeof input !== "object") return null;
22064
+ const o = input;
22065
+ const candidate = typeof o.file_path === "string" && o.file_path || typeof o.notebook_path === "string" && o.notebook_path || typeof o.path === "string" && o.path || null;
22066
+ return candidate || null;
22067
+ }
21900
22068
 
21901
22069
  // src/session/manager.ts
21902
22070
  function compressFrameForWire(frame) {
@@ -22139,6 +22307,7 @@ var SessionManager = class {
22139
22307
  });
22140
22308
  }
22141
22309
  }
22310
+ const attachmentGroup = this.deps.attachmentGroup;
22142
22311
  const runner = new SessionRunner(makeInitialState(file, subSessionMeta), {
22143
22312
  broadcastFrame: (frame, target) => this.routeFromRunner(frame, target),
22144
22313
  store,
@@ -22151,7 +22320,15 @@ var SessionManager = class {
22151
22320
  resolveContextWindow: (tool, modelId) => this.deps.getAdapter(tool).resolveContextWindow(modelId),
22152
22321
  dataDir: this.deps.dataDir,
22153
22322
  personaStore: this.deps.personaStore,
22154
- ownerDisplayName: this.deps.ownerDisplayName
22323
+ ownerDisplayName: this.deps.ownerDisplayName,
22324
+ // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22325
+ onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22326
+ scope,
22327
+ sessionId: file.sessionId,
22328
+ tool: input.tool,
22329
+ relPath: input.relPath,
22330
+ cwd: input.cwd
22331
+ }) : void 0
22155
22332
  });
22156
22333
  if (this.deps.mode === "tui" && !file.toolSessionId) {
22157
22334
  const newTsid = v4_default();
@@ -23319,6 +23496,21 @@ var PersonaRegistry = class {
23319
23496
  if (entry.revoked) return { ok: false, code: "TOKEN_REVOKED" };
23320
23497
  return { ok: true, label: entry.label };
23321
23498
  }
23499
+ /**
23500
+ * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23501
+ * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23502
+ * persona 数量通常 < 100,性能可忽略。
23503
+ */
23504
+ findByToken(token) {
23505
+ if (!token) return null;
23506
+ for (const persona of this.cache.values()) {
23507
+ if (!persona.public) continue;
23508
+ const entry = persona.tokenMap[token];
23509
+ if (!entry || entry.revoked) continue;
23510
+ return { personaId: persona.personaId, label: entry.label };
23511
+ }
23512
+ return null;
23513
+ }
23322
23514
  };
23323
23515
 
23324
23516
  // src/persona/manager.ts
@@ -25260,6 +25452,7 @@ var import_websocket = __toESM(require_websocket(), 1);
25260
25452
  var import_websocket_server = __toESM(require_websocket_server(), 1);
25261
25453
 
25262
25454
  // src/transport/local-ws-server.ts
25455
+ var import_node_http = __toESM(require("http"), 1);
25263
25456
  var PERSONA_PATH_RE = /^\/personas\/([a-zA-Z0-9._-]+)$/;
25264
25457
  var LocalWsServer = class {
25265
25458
  constructor(opts) {
@@ -25269,6 +25462,7 @@ var LocalWsServer = class {
25269
25462
  }
25270
25463
  opts;
25271
25464
  wss = null;
25465
+ httpServer = null;
25272
25466
  frameHandler = null;
25273
25467
  clients = /* @__PURE__ */ new Map();
25274
25468
  logger;
@@ -25276,25 +25470,28 @@ var LocalWsServer = class {
25276
25470
  async start() {
25277
25471
  const host = this.opts.host ?? "127.0.0.1";
25278
25472
  await new Promise((resolve2, reject) => {
25279
- const wss = new import_websocket_server.default({
25280
- host,
25281
- port: this.opts.port,
25282
- clientTracking: true
25473
+ const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
25474
+ const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
25475
+ httpServer.on("upgrade", (req, socket, head) => {
25476
+ wss.handleUpgrade(req, socket, head, (ws) => {
25477
+ this.routeConnection(ws, req);
25478
+ });
25283
25479
  });
25284
- wss.on("listening", () => {
25480
+ httpServer.on("listening", () => {
25285
25481
  this.logger?.info("ws listening", { host, port: this.opts.port });
25286
25482
  resolve2();
25287
25483
  });
25288
- wss.on("error", (err) => {
25484
+ httpServer.on("error", (err) => {
25289
25485
  this.logger?.error("ws server error", { err: err.message });
25290
25486
  reject(err);
25291
25487
  });
25292
- wss.on("connection", (socket, req) => this.routeConnection(socket, req));
25488
+ httpServer.listen(this.opts.port, host);
25489
+ this.httpServer = httpServer;
25293
25490
  this.wss = wss;
25294
25491
  });
25295
25492
  }
25296
25493
  async stop() {
25297
- if (!this.wss) return;
25494
+ if (!this.httpServer) return;
25298
25495
  for (const c of this.clients.values()) {
25299
25496
  try {
25300
25497
  c.ws.close(1001, "shutdown");
@@ -25306,7 +25503,32 @@ var LocalWsServer = class {
25306
25503
  await new Promise((resolve2) => {
25307
25504
  this.wss?.close(() => resolve2());
25308
25505
  });
25506
+ await new Promise((resolve2) => {
25507
+ this.httpServer?.close(() => resolve2());
25508
+ });
25309
25509
  this.wss = null;
25510
+ this.httpServer = null;
25511
+ }
25512
+ /** http.createServer 'request' 入口:file-sharing 路由优先,未命中走 404 */
25513
+ async handleHttpRequest(req, res) {
25514
+ const handler = this.opts.httpRequestHandler;
25515
+ if (handler) {
25516
+ try {
25517
+ const handled = await handler(req, res);
25518
+ if (handled) return;
25519
+ } catch (err) {
25520
+ this.logger?.warn("http handler threw", { err: err.message });
25521
+ if (!res.headersSent) {
25522
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
25523
+ res.end(JSON.stringify({ code: "INTERNAL", message: "http handler error" }));
25524
+ }
25525
+ return;
25526
+ }
25527
+ }
25528
+ if (!res.headersSent) {
25529
+ res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
25530
+ res.end(JSON.stringify({ code: "NOT_FOUND", message: "no route" }));
25531
+ }
25310
25532
  }
25311
25533
  onFrame(handler) {
25312
25534
  this.frameHandler = handler;
@@ -25408,7 +25630,7 @@ var LocalWsServer = class {
25408
25630
  }
25409
25631
  try {
25410
25632
  if (authed) {
25411
- const readyFrame = this.opts.readyFrameBuilder();
25633
+ const readyFrame = this.opts.readyFrameBuilder({ remoteAddress });
25412
25634
  this.safeSend(socket, { type: "ready", ...readyFrame });
25413
25635
  } else {
25414
25636
  this.safeSend(socket, { type: "ready", protocolVersion: this.opts.protocolVersion });
@@ -25445,7 +25667,7 @@ var LocalWsServer = class {
25445
25667
  if (verdict !== "pass") {
25446
25668
  if (!wasAuthed && authGate.isAuthed(client.id)) {
25447
25669
  try {
25448
- const full = this.opts.readyFrameBuilder();
25670
+ const full = this.opts.readyFrameBuilder({ remoteAddress });
25449
25671
  this.safeSend(this.clients.get(client.id).ws, { type: "ready", ...full });
25450
25672
  } catch (err) {
25451
25673
  this.logger?.warn("post-auth ready frame build failed", { err: err.message });
@@ -26042,11 +26264,603 @@ function isLocalhost(addr) {
26042
26264
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
26043
26265
  }
26044
26266
 
26045
- // src/discovery/state-file.ts
26267
+ // src/transport/auth-context.ts
26268
+ var AuthContextResolver = class {
26269
+ constructor(opts) {
26270
+ this.opts = opts;
26271
+ }
26272
+ opts;
26273
+ /**
26274
+ * 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26275
+ * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
26276
+ */
26277
+ resolveFromHeader(authHeader, remoteAddress) {
26278
+ const token = parseBearer(authHeader);
26279
+ if (!token) return null;
26280
+ const isLoopback = isLoopbackAddr(remoteAddress);
26281
+ if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
26282
+ return { role: "owner", isLoopback };
26283
+ }
26284
+ const personalHit = this.opts.personaRegistry.findByToken(token);
26285
+ if (personalHit) {
26286
+ return {
26287
+ role: "personal",
26288
+ personaId: personalHit.personaId,
26289
+ label: personalHit.label,
26290
+ isLoopback
26291
+ };
26292
+ }
26293
+ return null;
26294
+ }
26295
+ };
26296
+ function parseBearer(header) {
26297
+ if (!header) return null;
26298
+ const raw = Array.isArray(header) ? header[0] : header;
26299
+ if (typeof raw !== "string") return null;
26300
+ const m2 = /^Bearer\s+(\S+)$/i.exec(raw.trim());
26301
+ return m2 ? m2[1] : null;
26302
+ }
26303
+ function isLoopbackAddr(addr) {
26304
+ if (!addr) return false;
26305
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
26306
+ }
26307
+ function constantTimeEqual2(a, b2) {
26308
+ if (a.length !== b2.length) return false;
26309
+ let diff2 = 0;
26310
+ for (let i = 0; i < a.length; i++) diff2 |= a.charCodeAt(i) ^ b2.charCodeAt(i);
26311
+ return diff2 === 0;
26312
+ }
26313
+
26314
+ // src/transport/http-router.ts
26315
+ var import_node_fs14 = __toESM(require("fs"), 1);
26316
+ var import_node_path16 = __toESM(require("path"), 1);
26317
+
26318
+ // src/attachment/group.ts
26046
26319
  var import_node_fs13 = __toESM(require("fs"), 1);
26047
26320
  var import_node_path14 = __toESM(require("path"), 1);
26321
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
26322
+ init_protocol();
26323
+ var GroupFileStore = class {
26324
+ dataDir;
26325
+ logger;
26326
+ cache = /* @__PURE__ */ new Map();
26327
+ constructor(opts) {
26328
+ this.dataDir = opts.dataDir;
26329
+ this.logger = opts.logger;
26330
+ }
26331
+ rootForScope(scope) {
26332
+ return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26333
+ }
26334
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
26335
+ filePath(scope, sessionId) {
26336
+ return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26337
+ }
26338
+ cacheKey(scope, sessionId) {
26339
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26340
+ }
26341
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
26342
+ readFile(scope, sessionId) {
26343
+ const file = this.filePath(scope, sessionId);
26344
+ try {
26345
+ const raw = import_node_fs13.default.readFileSync(file, "utf8");
26346
+ const parsed = JSON.parse(raw);
26347
+ if (!Array.isArray(parsed)) {
26348
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
26349
+ file
26350
+ });
26351
+ return [];
26352
+ }
26353
+ const out = [];
26354
+ for (const entry of parsed) {
26355
+ const r = GroupFileEntrySchema.safeParse(entry);
26356
+ if (r.success) out.push(r.data);
26357
+ }
26358
+ return out;
26359
+ } catch (err) {
26360
+ const code = err?.code;
26361
+ if (code === "ENOENT") return [];
26362
+ this.logger?.warn("GroupFileStore.readFile failed", {
26363
+ file,
26364
+ err: err.message
26365
+ });
26366
+ return [];
26367
+ }
26368
+ }
26369
+ writeFile(scope, sessionId, entries) {
26370
+ const file = this.filePath(scope, sessionId);
26371
+ import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
26372
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
26373
+ import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
26374
+ import_node_fs13.default.renameSync(tmp, file);
26375
+ }
26376
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
26377
+ list(scope, sessionId) {
26378
+ const key = this.cacheKey(scope, sessionId);
26379
+ const cached = this.cache.get(key);
26380
+ if (cached) return cached.entries;
26381
+ const entries = this.readFile(scope, sessionId);
26382
+ this.cache.set(key, { entries, scope });
26383
+ return entries;
26384
+ }
26385
+ /**
26386
+ * upsert:
26387
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
26388
+ * (保留首次入群人 / 入群时刻)
26389
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
26390
+ *
26391
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
26392
+ */
26393
+ upsert(scope, sessionId, input, now = Date.now()) {
26394
+ const entries = this.list(scope, sessionId).slice();
26395
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
26396
+ let next;
26397
+ if (idx >= 0) {
26398
+ const prev = entries[idx];
26399
+ next = {
26400
+ ...prev,
26401
+ size: input.size,
26402
+ mime: input.mime,
26403
+ lastEditedAt: now,
26404
+ stale: false
26405
+ // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
26406
+ };
26407
+ entries[idx] = next;
26408
+ } else {
26409
+ next = {
26410
+ id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
26411
+ relPath: input.relPath,
26412
+ from: input.from,
26413
+ label: input.label,
26414
+ size: input.size,
26415
+ mime: input.mime,
26416
+ addedAt: now
26417
+ // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
26418
+ };
26419
+ entries.push(next);
26420
+ }
26421
+ this.writeFile(scope, sessionId, entries);
26422
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26423
+ return next;
26424
+ }
26425
+ /**
26426
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
26427
+ * - 命中 → stale=true,UI 灰显
26428
+ * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
26429
+ *
26430
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26431
+ */
26432
+ markStale(scope, sessionId, relPath) {
26433
+ const entries = this.list(scope, sessionId).slice();
26434
+ const idx = entries.findIndex((e) => e.relPath === relPath);
26435
+ if (idx < 0) return;
26436
+ if (entries[idx].stale) return;
26437
+ entries[idx] = { ...entries[idx], stale: true };
26438
+ this.writeFile(scope, sessionId, entries);
26439
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26440
+ }
26441
+ /**
26442
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
26443
+ * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
26444
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
26445
+ *
26446
+ * 返回值:true=命中并删除;false=relPath 不在群里。
26447
+ */
26448
+ remove(scope, sessionId, relPath) {
26449
+ const entries = this.list(scope, sessionId).slice();
26450
+ const idx = entries.findIndex((e) => e.relPath === relPath);
26451
+ if (idx < 0) return false;
26452
+ entries.splice(idx, 1);
26453
+ this.writeFile(scope, sessionId, entries);
26454
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26455
+ return true;
26456
+ }
26457
+ /**
26458
+ * 跨 session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
26459
+ *
26460
+ * 扫 <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
26461
+ * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
26462
+ *
26463
+ * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26464
+ */
26465
+ listByPersona(personaId) {
26466
+ const out = [];
26467
+ for (const mode of ["owner", "listener"]) {
26468
+ const scope = { kind: "persona", personaId, mode };
26469
+ const root = this.rootForScope(scope);
26470
+ let names;
26471
+ try {
26472
+ names = import_node_fs13.default.readdirSync(root);
26473
+ } catch (err) {
26474
+ const code = err?.code;
26475
+ if (code === "ENOENT") continue;
26476
+ continue;
26477
+ }
26478
+ for (const name of names) {
26479
+ if (!name.endsWith(".group-files.json")) continue;
26480
+ const sessionId = name.slice(0, -".group-files.json".length);
26481
+ if (!sessionId) continue;
26482
+ const entries = this.list(scope, sessionId);
26483
+ out.push({ sessionId, entries });
26484
+ }
26485
+ }
26486
+ return out;
26487
+ }
26488
+ };
26489
+ function personalViewable(groupStore, personaDir, personaId, absPath) {
26490
+ const realTarget = safeRealpath(absPath);
26491
+ if (!realTarget) {
26492
+ return false;
26493
+ }
26494
+ const personasUnion = groupStore.listByPersona(personaId);
26495
+ for (const { entries } of personasUnion) {
26496
+ for (const e of entries) {
26497
+ if (e.stale) continue;
26498
+ const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
26499
+ if (realEntry && realEntry === realTarget) return true;
26500
+ }
26501
+ }
26502
+ return false;
26503
+ }
26504
+ function safeRealpath(p2) {
26505
+ try {
26506
+ return import_node_fs13.default.realpathSync(p2);
26507
+ } catch {
26508
+ return null;
26509
+ }
26510
+ }
26511
+
26512
+ // src/attachment/mime.ts
26513
+ var import_node_path15 = __toESM(require("path"), 1);
26514
+ var TEXT_PLAIN = "text/plain; charset=utf-8";
26515
+ var EXT_TO_NATIVE_MIME = {
26516
+ // 图片
26517
+ ".png": "image/png",
26518
+ ".jpg": "image/jpeg",
26519
+ ".jpeg": "image/jpeg",
26520
+ ".gif": "image/gif",
26521
+ ".webp": "image/webp",
26522
+ ".svg": "image/svg+xml",
26523
+ ".bmp": "image/bmp",
26524
+ ".ico": "image/x-icon",
26525
+ ".avif": "image/avif",
26526
+ // 文档 / 富文本
26527
+ ".pdf": "application/pdf",
26528
+ ".html": "text/html; charset=utf-8",
26529
+ ".htm": "text/html; charset=utf-8",
26530
+ // 视频 / 音频
26531
+ ".mp4": "video/mp4",
26532
+ ".webm": "video/webm",
26533
+ ".mov": "video/quicktime",
26534
+ ".mp3": "audio/mpeg",
26535
+ ".wav": "audio/wav",
26536
+ ".ogg": "audio/ogg",
26537
+ ".flac": "audio/flac",
26538
+ // 真二进制(让浏览器下载而不是错把它当文本明文)
26539
+ ".zip": "application/zip",
26540
+ ".gz": "application/gzip",
26541
+ ".tar": "application/x-tar",
26542
+ ".7z": "application/x-7z-compressed",
26543
+ ".rar": "application/x-rar-compressed"
26544
+ };
26545
+ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
26546
+ // 文档
26547
+ ".md",
26548
+ ".markdown",
26549
+ ".rst",
26550
+ ".adoc",
26551
+ ".txt",
26552
+ ".log",
26553
+ // 通用结构化
26554
+ ".json",
26555
+ ".jsonc",
26556
+ ".json5",
26557
+ ".yaml",
26558
+ ".yml",
26559
+ ".toml",
26560
+ ".xml",
26561
+ ".csv",
26562
+ ".tsv",
26563
+ ".ini",
26564
+ ".conf",
26565
+ ".cfg",
26566
+ ".env",
26567
+ // 前端
26568
+ ".js",
26569
+ ".jsx",
26570
+ ".mjs",
26571
+ ".cjs",
26572
+ ".ts",
26573
+ ".tsx",
26574
+ ".css",
26575
+ ".scss",
26576
+ ".sass",
26577
+ ".less",
26578
+ ".vue",
26579
+ ".svelte",
26580
+ ".graphql",
26581
+ ".gql",
26582
+ // 后端 / 各语言
26583
+ ".py",
26584
+ ".rb",
26585
+ ".go",
26586
+ ".rs",
26587
+ ".java",
26588
+ ".kt",
26589
+ ".kts",
26590
+ ".scala",
26591
+ ".c",
26592
+ ".h",
26593
+ ".cpp",
26594
+ ".hpp",
26595
+ ".cc",
26596
+ ".hh",
26597
+ ".cs",
26598
+ ".php",
26599
+ ".swift",
26600
+ ".m",
26601
+ ".mm",
26602
+ ".dart",
26603
+ ".lua",
26604
+ ".pl",
26605
+ ".r",
26606
+ ".sh",
26607
+ ".bash",
26608
+ ".zsh",
26609
+ ".fish",
26610
+ // 其他
26611
+ ".sql",
26612
+ ".proto",
26613
+ ".dockerfile",
26614
+ ".diff",
26615
+ ".patch",
26616
+ ".makefile",
26617
+ ".mk"
26618
+ ]);
26619
+ function lookupMime(filePathOrName) {
26620
+ const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
26621
+ if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
26622
+ if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
26623
+ return "application/octet-stream";
26624
+ }
26625
+
26626
+ // src/attachment/sign-url.ts
26627
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
26628
+ var HMAC_ALGO = "sha256";
26629
+ function base64urlEncode(buf) {
26630
+ const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
26631
+ return b2.toString("base64url");
26632
+ }
26633
+ function base64urlDecodeBuf(s) {
26634
+ return Buffer.from(s, "base64url");
26635
+ }
26636
+ function encodeAbsPathForUrl(absPath) {
26637
+ return absPath.split("/").map(encodeURIComponent).join("/");
26638
+ }
26639
+ function decodeAbsPathFromUrl(encoded) {
26640
+ return encoded.split("/").map(decodeURIComponent).join("/");
26641
+ }
26642
+ function computeSig(secret, absPath, e) {
26643
+ const msg = e === null ? absPath : `${absPath}|${e}`;
26644
+ return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26645
+ }
26646
+ function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
26647
+ const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
26648
+ const s = base64urlEncode(computeSig(secret, absPath, e));
26649
+ return { absPath, e, s };
26650
+ }
26651
+ function buildSignedFileUrl(httpBaseUrl, parts) {
26652
+ const encodedPath = encodeAbsPathForUrl(parts.absPath);
26653
+ const query = parts.e === null ? `s=${encodeURIComponent(parts.s)}` : `e=${parts.e}&s=${encodeURIComponent(parts.s)}`;
26654
+ const base = httpBaseUrl.endsWith("/") ? httpBaseUrl.slice(0, -1) : httpBaseUrl;
26655
+ return `${base}/files${encodedPath}?${query}`;
26656
+ }
26657
+ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
26658
+ let e;
26659
+ if (eRaw === null || eRaw === void 0 || eRaw === "") {
26660
+ e = null;
26661
+ } else {
26662
+ const parsed = Number.parseInt(eRaw, 10);
26663
+ if (!Number.isFinite(parsed) || parsed < 0) {
26664
+ return { ok: false, code: "MALFORMED" };
26665
+ }
26666
+ e = parsed;
26667
+ }
26668
+ if (!absPath || !s) return { ok: false, code: "MALFORMED" };
26669
+ const expected = computeSig(secret, absPath, e);
26670
+ let provided;
26671
+ try {
26672
+ provided = base64urlDecodeBuf(s);
26673
+ } catch {
26674
+ return { ok: false, code: "MALFORMED" };
26675
+ }
26676
+ if (provided.length !== expected.length) {
26677
+ return { ok: false, code: "BAD_SIG" };
26678
+ }
26679
+ if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
26680
+ return { ok: false, code: "BAD_SIG" };
26681
+ }
26682
+ if (e !== null && now() / 1e3 > e) {
26683
+ return { ok: false, code: "EXPIRED" };
26684
+ }
26685
+ return { ok: true, absPath };
26686
+ }
26687
+
26688
+ // src/transport/http-router.ts
26689
+ function createHttpRouter(deps) {
26690
+ return async (req, res) => {
26691
+ const url = parseUrl(req.url);
26692
+ if (!url) {
26693
+ sendJson(res, 400, { code: "INVALID_URL", message: "malformed request URL" });
26694
+ return true;
26695
+ }
26696
+ if (url.pathname === "/healthz" && req.method === "GET") {
26697
+ sendJson(res, 200, { ok: true, version: deps.daemonVersion });
26698
+ return true;
26699
+ }
26700
+ if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26701
+ return false;
26702
+ }
26703
+ if (url.pathname.startsWith("/files/") && req.method === "GET") {
26704
+ const secret = deps.getSignSecret?.();
26705
+ if (!secret) {
26706
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "signed URL secret unavailable (noAuth?)" });
26707
+ return true;
26708
+ }
26709
+ const encodedPath = url.pathname.slice("/files".length);
26710
+ let absPath;
26711
+ try {
26712
+ absPath = decodeAbsPathFromUrl(encodedPath);
26713
+ } catch {
26714
+ sendJson(res, 400, { code: "MALFORMED", message: "invalid path encoding" });
26715
+ return true;
26716
+ }
26717
+ const e = url.searchParams.get("e");
26718
+ const s = url.searchParams.get("s") ?? "";
26719
+ const r = verifySignedUrl(secret, absPath, e, s);
26720
+ if (!r.ok) {
26721
+ const statusByCode = {
26722
+ BAD_SIG: 403,
26723
+ EXPIRED: 410,
26724
+ MALFORMED: 400
26725
+ };
26726
+ sendJson(res, statusByCode[r.code], { code: r.code, message: "signed URL invalid" });
26727
+ return true;
26728
+ }
26729
+ streamFile(res, r.absPath, deps.logger);
26730
+ return true;
26731
+ }
26732
+ const ctx = deps.authResolver.resolveFromHeader(
26733
+ req.headers.authorization,
26734
+ req.socket.remoteAddress ?? void 0
26735
+ );
26736
+ if (!ctx) {
26737
+ sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
26738
+ return true;
26739
+ }
26740
+ const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
26741
+ if (personaFilesMatch && req.method === "GET") {
26742
+ const pid = personaFilesMatch[1];
26743
+ const pathParam = url.searchParams.get("path");
26744
+ if (!pathParam) {
26745
+ sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26746
+ return true;
26747
+ }
26748
+ if (!deps.personaStore || !deps.groupFileStore) {
26749
+ sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
26750
+ return true;
26751
+ }
26752
+ const personaDir = deps.personaStore.personaDirPath(pid);
26753
+ const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
26754
+ if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
26755
+ sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
26756
+ return true;
26757
+ }
26758
+ if (ctx.role === "personal") {
26759
+ if (ctx.personaId !== pid) {
26760
+ sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
26761
+ return true;
26762
+ }
26763
+ if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
26764
+ sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
26765
+ return true;
26766
+ }
26767
+ }
26768
+ streamFile(res, absPath, deps.logger);
26769
+ return true;
26770
+ }
26771
+ const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
26772
+ if (sessionFilesMatch && req.method === "GET") {
26773
+ if (ctx.role !== "owner") {
26774
+ sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
26775
+ return true;
26776
+ }
26777
+ const sid = sessionFilesMatch[1];
26778
+ const pathParam = url.searchParams.get("path");
26779
+ if (!pathParam) {
26780
+ sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26781
+ return true;
26782
+ }
26783
+ let absPath;
26784
+ if (import_node_path16.default.isAbsolute(pathParam)) {
26785
+ absPath = pathParam;
26786
+ } else if (deps.sessionStore) {
26787
+ const file = deps.sessionStore.read(sid);
26788
+ if (!file) {
26789
+ sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
26790
+ return true;
26791
+ }
26792
+ absPath = import_node_path16.default.join(file.cwd, pathParam);
26793
+ } else {
26794
+ sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
26795
+ return true;
26796
+ }
26797
+ streamFile(res, absPath, deps.logger);
26798
+ return true;
26799
+ }
26800
+ sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
26801
+ return true;
26802
+ };
26803
+ }
26804
+ function parseUrl(rawUrl) {
26805
+ if (!rawUrl) return null;
26806
+ try {
26807
+ return new URL(rawUrl, "http://placeholder");
26808
+ } catch {
26809
+ return null;
26810
+ }
26811
+ }
26812
+ function sendJson(res, status, body) {
26813
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
26814
+ res.end(JSON.stringify(body));
26815
+ }
26816
+ function withCtx(ctx, body) {
26817
+ return { ...body, role: ctx.role, personaId: ctx.personaId };
26818
+ }
26819
+ function isContainedIn(abs, root) {
26820
+ const normalized = import_node_path16.default.resolve(abs);
26821
+ const normalizedRoot = import_node_path16.default.resolve(root);
26822
+ if (normalized === normalizedRoot) return true;
26823
+ return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
26824
+ }
26825
+ function streamFile(res, absPath, logger) {
26826
+ let stat;
26827
+ try {
26828
+ stat = import_node_fs14.default.statSync(absPath);
26829
+ } catch (err) {
26830
+ const code = err?.code;
26831
+ if (code === "ENOENT") {
26832
+ sendJson(res, 404, { code: "NOT_FOUND", message: "file not found" });
26833
+ } else {
26834
+ sendJson(res, 500, { code: "STAT_FAILED", message: err.message });
26835
+ }
26836
+ return;
26837
+ }
26838
+ if (!stat.isFile()) {
26839
+ sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
26840
+ return;
26841
+ }
26842
+ const mime = lookupMime(absPath);
26843
+ const basename = import_node_path16.default.basename(absPath);
26844
+ res.writeHead(200, {
26845
+ "Content-Type": mime,
26846
+ "Content-Length": String(stat.size),
26847
+ "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
26848
+ // 防止浏览器把任意 mime 当 html 渲染
26849
+ "X-Content-Type-Options": "nosniff"
26850
+ });
26851
+ const stream = import_node_fs14.default.createReadStream(absPath);
26852
+ stream.on("error", (err) => {
26853
+ logger?.warn("streamFile read error", { absPath, err: err.message });
26854
+ res.destroy();
26855
+ });
26856
+ stream.pipe(res);
26857
+ }
26858
+
26859
+ // src/discovery/state-file.ts
26860
+ var import_node_fs15 = __toESM(require("fs"), 1);
26861
+ var import_node_path17 = __toESM(require("path"), 1);
26048
26862
  function defaultStateFilePath(dataDir) {
26049
- return import_node_path14.default.join(dataDir, "state.json");
26863
+ return import_node_path17.default.join(dataDir, "state.json");
26050
26864
  }
26051
26865
  function isPidAlive(pid) {
26052
26866
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -26068,7 +26882,7 @@ var StateFileManager = class {
26068
26882
  }
26069
26883
  read() {
26070
26884
  try {
26071
- const raw = import_node_fs13.default.readFileSync(this.file, "utf8");
26885
+ const raw = import_node_fs15.default.readFileSync(this.file, "utf8");
26072
26886
  const parsed = JSON.parse(raw);
26073
26887
  return parsed;
26074
26888
  } catch {
@@ -26082,34 +26896,34 @@ var StateFileManager = class {
26082
26896
  return { status: "stale", existing };
26083
26897
  }
26084
26898
  write(state) {
26085
- import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(this.file), { recursive: true });
26899
+ import_node_fs15.default.mkdirSync(import_node_path17.default.dirname(this.file), { recursive: true });
26086
26900
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
26087
- import_node_fs13.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
26088
- import_node_fs13.default.renameSync(tmp, this.file);
26901
+ import_node_fs15.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
26902
+ import_node_fs15.default.renameSync(tmp, this.file);
26089
26903
  if (process.platform !== "win32") {
26090
26904
  try {
26091
- import_node_fs13.default.chmodSync(this.file, 384);
26905
+ import_node_fs15.default.chmodSync(this.file, 384);
26092
26906
  } catch {
26093
26907
  }
26094
26908
  }
26095
26909
  }
26096
26910
  delete() {
26097
26911
  try {
26098
- import_node_fs13.default.unlinkSync(this.file);
26912
+ import_node_fs15.default.unlinkSync(this.file);
26099
26913
  } catch {
26100
26914
  }
26101
26915
  }
26102
26916
  };
26103
26917
 
26104
26918
  // src/tunnel/tunnel-manager.ts
26105
- var import_node_fs17 = __toESM(require("fs"), 1);
26106
- var import_node_path18 = __toESM(require("path"), 1);
26107
- var import_node_crypto4 = __toESM(require("crypto"), 1);
26919
+ var import_node_fs19 = __toESM(require("fs"), 1);
26920
+ var import_node_path21 = __toESM(require("path"), 1);
26921
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
26108
26922
  var import_node_child_process5 = require("child_process");
26109
26923
 
26110
26924
  // src/tunnel/tunnel-store.ts
26111
- var import_node_fs14 = __toESM(require("fs"), 1);
26112
- var import_node_path15 = __toESM(require("path"), 1);
26925
+ var import_node_fs16 = __toESM(require("fs"), 1);
26926
+ var import_node_path18 = __toESM(require("path"), 1);
26113
26927
  var TunnelStore = class {
26114
26928
  constructor(filePath) {
26115
26929
  this.filePath = filePath;
@@ -26117,7 +26931,7 @@ var TunnelStore = class {
26117
26931
  filePath;
26118
26932
  async get() {
26119
26933
  try {
26120
- const raw = await import_node_fs14.default.promises.readFile(this.filePath, "utf8");
26934
+ const raw = await import_node_fs16.default.promises.readFile(this.filePath, "utf8");
26121
26935
  const obj = JSON.parse(raw);
26122
26936
  if (!isPersistedTunnel(obj)) return null;
26123
26937
  return obj;
@@ -26128,22 +26942,22 @@ var TunnelStore = class {
26128
26942
  }
26129
26943
  }
26130
26944
  async set(v2) {
26131
- const dir = import_node_path15.default.dirname(this.filePath);
26132
- await import_node_fs14.default.promises.mkdir(dir, { recursive: true });
26945
+ const dir = import_node_path18.default.dirname(this.filePath);
26946
+ await import_node_fs16.default.promises.mkdir(dir, { recursive: true });
26133
26947
  const data = JSON.stringify(v2, null, 2);
26134
26948
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
26135
- await import_node_fs14.default.promises.writeFile(tmp, data, { mode: 384 });
26949
+ await import_node_fs16.default.promises.writeFile(tmp, data, { mode: 384 });
26136
26950
  if (process.platform !== "win32") {
26137
26951
  try {
26138
- await import_node_fs14.default.promises.chmod(tmp, 384);
26952
+ await import_node_fs16.default.promises.chmod(tmp, 384);
26139
26953
  } catch {
26140
26954
  }
26141
26955
  }
26142
- await import_node_fs14.default.promises.rename(tmp, this.filePath);
26956
+ await import_node_fs16.default.promises.rename(tmp, this.filePath);
26143
26957
  }
26144
26958
  async clear() {
26145
26959
  try {
26146
- await import_node_fs14.default.promises.unlink(this.filePath);
26960
+ await import_node_fs16.default.promises.unlink(this.filePath);
26147
26961
  } catch (err) {
26148
26962
  const code = err?.code;
26149
26963
  if (code !== "ENOENT") throw err;
@@ -26238,9 +27052,9 @@ function escape(v2) {
26238
27052
  }
26239
27053
 
26240
27054
  // src/tunnel/frpc-binary.ts
26241
- var import_node_fs15 = __toESM(require("fs"), 1);
27055
+ var import_node_fs17 = __toESM(require("fs"), 1);
26242
27056
  var import_node_os9 = __toESM(require("os"), 1);
26243
- var import_node_path16 = __toESM(require("path"), 1);
27057
+ var import_node_path19 = __toESM(require("path"), 1);
26244
27058
  var import_node_child_process3 = require("child_process");
26245
27059
  var import_node_stream2 = require("stream");
26246
27060
  var import_promises = require("stream/promises");
@@ -26272,20 +27086,20 @@ function frpcDownloadUrl(version2, p2) {
26272
27086
  }
26273
27087
  async function ensureFrpcBinary(opts) {
26274
27088
  if (opts.override) {
26275
- if (!import_node_fs15.default.existsSync(opts.override)) {
27089
+ if (!import_node_fs17.default.existsSync(opts.override)) {
26276
27090
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
26277
27091
  }
26278
27092
  return opts.override;
26279
27093
  }
26280
27094
  const version2 = opts.version ?? FRPC_VERSION;
26281
27095
  const platform = opts.platform ?? detectPlatform();
26282
- const binDir = import_node_path16.default.join(opts.dataDir, "bin");
26283
- import_node_fs15.default.mkdirSync(binDir, { recursive: true });
27096
+ const binDir = import_node_path19.default.join(opts.dataDir, "bin");
27097
+ import_node_fs17.default.mkdirSync(binDir, { recursive: true });
26284
27098
  cleanupStaleArtifacts(binDir);
26285
- const stableBin = import_node_path16.default.join(binDir, "frpc");
26286
- if (import_node_fs15.default.existsSync(stableBin)) return stableBin;
27099
+ const stableBin = import_node_path19.default.join(binDir, "frpc");
27100
+ if (import_node_fs17.default.existsSync(stableBin)) return stableBin;
26287
27101
  const partialBin = `${stableBin}.partial`;
26288
- const tarballPath = import_node_path16.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
27102
+ const tarballPath = import_node_path19.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
26289
27103
  try {
26290
27104
  const url = frpcDownloadUrl(version2, platform);
26291
27105
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -26294,8 +27108,8 @@ async function ensureFrpcBinary(opts) {
26294
27108
  } else {
26295
27109
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
26296
27110
  }
26297
- import_node_fs15.default.chmodSync(partialBin, 493);
26298
- import_node_fs15.default.renameSync(partialBin, stableBin);
27111
+ import_node_fs17.default.chmodSync(partialBin, 493);
27112
+ import_node_fs17.default.renameSync(partialBin, stableBin);
26299
27113
  } finally {
26300
27114
  safeUnlink(tarballPath);
26301
27115
  safeUnlink(partialBin);
@@ -26305,15 +27119,15 @@ async function ensureFrpcBinary(opts) {
26305
27119
  function cleanupStaleArtifacts(binDir) {
26306
27120
  let entries;
26307
27121
  try {
26308
- entries = import_node_fs15.default.readdirSync(binDir);
27122
+ entries = import_node_fs17.default.readdirSync(binDir);
26309
27123
  } catch {
26310
27124
  return;
26311
27125
  }
26312
27126
  for (const name of entries) {
26313
27127
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
26314
- const full = import_node_path16.default.join(binDir, name);
27128
+ const full = import_node_path19.default.join(binDir, name);
26315
27129
  try {
26316
- import_node_fs15.default.rmSync(full, { recursive: true, force: true });
27130
+ import_node_fs17.default.rmSync(full, { recursive: true, force: true });
26317
27131
  } catch {
26318
27132
  }
26319
27133
  }
@@ -26321,7 +27135,7 @@ function cleanupStaleArtifacts(binDir) {
26321
27135
  }
26322
27136
  function safeUnlink(p2) {
26323
27137
  try {
26324
- import_node_fs15.default.unlinkSync(p2);
27138
+ import_node_fs17.default.unlinkSync(p2);
26325
27139
  } catch {
26326
27140
  }
26327
27141
  }
@@ -26332,13 +27146,13 @@ async function downloadToFile(url, dest, fetchImpl) {
26332
27146
  if (!res.ok || !res.body) {
26333
27147
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
26334
27148
  }
26335
- const out = import_node_fs15.default.createWriteStream(dest);
27149
+ const out = import_node_fs17.default.createWriteStream(dest);
26336
27150
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
26337
27151
  await (0, import_promises.pipeline)(nodeStream, out);
26338
27152
  }
26339
27153
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
26340
- const work = import_node_path16.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
26341
- import_node_fs15.default.mkdirSync(work, { recursive: true });
27154
+ const work = import_node_path19.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
27155
+ import_node_fs17.default.mkdirSync(work, { recursive: true });
26342
27156
  try {
26343
27157
  await new Promise((resolve2, reject) => {
26344
27158
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -26346,32 +27160,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
26346
27160
  proc.on("exit", (code) => code === 0 ? resolve2() : reject(new Error(`tar exited ${code}`)));
26347
27161
  });
26348
27162
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
26349
- const src = import_node_path16.default.join(work, dirName, "frpc");
26350
- if (!import_node_fs15.default.existsSync(src)) {
27163
+ const src = import_node_path19.default.join(work, dirName, "frpc");
27164
+ if (!import_node_fs17.default.existsSync(src)) {
26351
27165
  throw new Error(`frpc not found inside tarball at ${src}`);
26352
27166
  }
26353
- import_node_fs15.default.copyFileSync(src, destBin);
27167
+ import_node_fs17.default.copyFileSync(src, destBin);
26354
27168
  } finally {
26355
- import_node_fs15.default.rmSync(work, { recursive: true, force: true });
27169
+ import_node_fs17.default.rmSync(work, { recursive: true, force: true });
26356
27170
  }
26357
27171
  }
26358
27172
 
26359
27173
  // src/tunnel/frpc-process.ts
26360
- var import_node_fs16 = __toESM(require("fs"), 1);
26361
- var import_node_path17 = __toESM(require("path"), 1);
27174
+ var import_node_fs18 = __toESM(require("fs"), 1);
27175
+ var import_node_path20 = __toESM(require("path"), 1);
26362
27176
  var import_node_child_process4 = require("child_process");
26363
27177
  function frpcPidFilePath(dataDir) {
26364
- return import_node_path17.default.join(dataDir, "frpc.pid");
27178
+ return import_node_path20.default.join(dataDir, "frpc.pid");
26365
27179
  }
26366
27180
  function writeFrpcPid(dataDir, pid) {
26367
27181
  try {
26368
- import_node_fs16.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
27182
+ import_node_fs18.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
26369
27183
  } catch {
26370
27184
  }
26371
27185
  }
26372
27186
  function clearFrpcPid(dataDir) {
26373
27187
  try {
26374
- import_node_fs16.default.unlinkSync(frpcPidFilePath(dataDir));
27188
+ import_node_fs18.default.unlinkSync(frpcPidFilePath(dataDir));
26375
27189
  } catch {
26376
27190
  }
26377
27191
  }
@@ -26387,7 +27201,7 @@ function defaultIsPidAlive(pid) {
26387
27201
  }
26388
27202
  function defaultReadPidFile(file) {
26389
27203
  try {
26390
- return import_node_fs16.default.readFileSync(file, "utf8");
27204
+ return import_node_fs18.default.readFileSync(file, "utf8");
26391
27205
  } catch {
26392
27206
  return null;
26393
27207
  }
@@ -26403,7 +27217,7 @@ function defaultSleep(ms) {
26403
27217
  }
26404
27218
  async function killStaleFrpc(deps) {
26405
27219
  const pidFile = frpcPidFilePath(deps.dataDir);
26406
- const tomlPath = import_node_path17.default.join(deps.dataDir, "frpc.toml");
27220
+ const tomlPath = import_node_path20.default.join(deps.dataDir, "frpc.toml");
26407
27221
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
26408
27222
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
26409
27223
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -26427,7 +27241,7 @@ async function killStaleFrpc(deps) {
26427
27241
  }
26428
27242
  if (victims.size === 0) {
26429
27243
  try {
26430
- import_node_fs16.default.unlinkSync(pidFile);
27244
+ import_node_fs18.default.unlinkSync(pidFile);
26431
27245
  } catch {
26432
27246
  }
26433
27247
  return;
@@ -26438,7 +27252,7 @@ async function killStaleFrpc(deps) {
26438
27252
  }
26439
27253
  await sleep(deps.reapWaitMs ?? 300);
26440
27254
  try {
26441
- import_node_fs16.default.unlinkSync(pidFile);
27255
+ import_node_fs18.default.unlinkSync(pidFile);
26442
27256
  } catch {
26443
27257
  }
26444
27258
  }
@@ -26475,7 +27289,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
26475
27289
  var TunnelManager = class {
26476
27290
  constructor(deps) {
26477
27291
  this.deps = deps;
26478
- this.store = deps.store ?? new TunnelStore(import_node_path18.default.join(deps.dataDir, "tunnel.json"));
27292
+ this.store = deps.store ?? new TunnelStore(import_node_path21.default.join(deps.dataDir, "tunnel.json"));
26479
27293
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
26480
27294
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
26481
27295
  }
@@ -26602,8 +27416,8 @@ var TunnelManager = class {
26602
27416
  dataDir: this.deps.dataDir,
26603
27417
  override: this.deps.frpcBinaryOverride ?? void 0
26604
27418
  });
26605
- const tomlPath = import_node_path18.default.join(this.deps.dataDir, "frpc.toml");
26606
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto4.default.randomBytes(3).toString("hex")}`;
27419
+ const tomlPath = import_node_path21.default.join(this.deps.dataDir, "frpc.toml");
27420
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto6.default.randomBytes(3).toString("hex")}`;
26607
27421
  const toml = buildFrpcToml({
26608
27422
  serverAddr: t.frpsHost,
26609
27423
  serverPort: t.frpsPort,
@@ -26613,12 +27427,12 @@ var TunnelManager = class {
26613
27427
  localPort,
26614
27428
  logLevel: "info"
26615
27429
  });
26616
- await import_node_fs17.default.promises.writeFile(tomlPath, toml, { mode: 384 });
27430
+ await import_node_fs19.default.promises.writeFile(tomlPath, toml, { mode: 384 });
26617
27431
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
26618
27432
  stdio: ["ignore", "pipe", "pipe"]
26619
27433
  });
26620
- const logFilePath = import_node_path18.default.join(this.deps.dataDir, "frpc.log");
26621
- const logStream = import_node_fs17.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
27434
+ const logFilePath = import_node_path21.default.join(this.deps.dataDir, "frpc.log");
27435
+ const logStream = import_node_fs19.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
26622
27436
  logStream.on("error", () => {
26623
27437
  });
26624
27438
  const tee = (chunk) => {
@@ -26701,22 +27515,22 @@ async function waitForFrpcReady(proc, timeoutMs) {
26701
27515
 
26702
27516
  // src/tunnel/device-key.ts
26703
27517
  var import_node_os10 = __toESM(require("os"), 1);
26704
- var import_node_crypto5 = __toESM(require("crypto"), 1);
27518
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
26705
27519
  var DERIVE_SALT = "clawd-tunnel-device-v1";
26706
27520
  function deriveStableDeviceKey(opts = {}) {
26707
27521
  const hostname = opts.hostname ?? import_node_os10.default.hostname();
26708
27522
  const uid = opts.uid ?? (typeof import_node_os10.default.userInfo === "function" ? import_node_os10.default.userInfo().uid : 0);
26709
27523
  const input = `${hostname}::${uid}`;
26710
- return import_node_crypto5.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
27524
+ return import_node_crypto7.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
26711
27525
  }
26712
27526
 
26713
27527
  // src/auth-store.ts
26714
- var import_node_fs18 = __toESM(require("fs"), 1);
26715
- var import_node_path19 = __toESM(require("path"), 1);
26716
- var import_node_crypto6 = __toESM(require("crypto"), 1);
27528
+ var import_node_fs20 = __toESM(require("fs"), 1);
27529
+ var import_node_path22 = __toESM(require("path"), 1);
27530
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
26717
27531
  var AUTH_FILE_NAME = "auth.json";
26718
27532
  function authFilePath(dataDir) {
26719
- return import_node_path19.default.join(dataDir, AUTH_FILE_NAME);
27533
+ return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
26720
27534
  }
26721
27535
  function loadOrCreateAuthToken(opts) {
26722
27536
  const file = authFilePath(opts.dataDir);
@@ -26728,11 +27542,11 @@ function loadOrCreateAuthToken(opts) {
26728
27542
  return token;
26729
27543
  }
26730
27544
  function defaultGenerate() {
26731
- return import_node_crypto6.default.randomBytes(32).toString("base64url");
27545
+ return import_node_crypto8.default.randomBytes(32).toString("base64url");
26732
27546
  }
26733
27547
  function readAuthFile(file) {
26734
27548
  try {
26735
- const raw = import_node_fs18.default.readFileSync(file, "utf8");
27549
+ const raw = import_node_fs20.default.readFileSync(file, "utf8");
26736
27550
  const parsed = JSON.parse(raw);
26737
27551
  if (typeof parsed?.token === "string" && parsed.token.length > 0) {
26738
27552
  return {
@@ -26748,25 +27562,25 @@ function readAuthFile(file) {
26748
27562
  }
26749
27563
  }
26750
27564
  function writeAuthFile(file, content) {
26751
- import_node_fs18.default.mkdirSync(import_node_path19.default.dirname(file), { recursive: true });
26752
- import_node_fs18.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
27565
+ import_node_fs20.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
27566
+ import_node_fs20.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
26753
27567
  try {
26754
- import_node_fs18.default.chmodSync(file, 384);
27568
+ import_node_fs20.default.chmodSync(file, 384);
26755
27569
  } catch {
26756
27570
  }
26757
27571
  }
26758
27572
 
26759
27573
  // src/owner-profile.ts
26760
- var import_node_fs19 = __toESM(require("fs"), 1);
27574
+ var import_node_fs21 = __toESM(require("fs"), 1);
26761
27575
  var import_node_os11 = __toESM(require("os"), 1);
26762
- var import_node_path20 = __toESM(require("path"), 1);
27576
+ var import_node_path23 = __toESM(require("path"), 1);
26763
27577
  var PROFILE_FILENAME = "profile.json";
26764
27578
  function loadOwnerDisplayName(dataDir) {
26765
27579
  const fallback = import_node_os11.default.userInfo().username;
26766
- const profilePath = import_node_path20.default.join(dataDir, PROFILE_FILENAME);
27580
+ const profilePath = import_node_path23.default.join(dataDir, PROFILE_FILENAME);
26767
27581
  let raw;
26768
27582
  try {
26769
- raw = import_node_fs19.default.readFileSync(profilePath, "utf8");
27583
+ raw = import_node_fs21.default.readFileSync(profilePath, "utf8");
26770
27584
  } catch {
26771
27585
  return fallback;
26772
27586
  }
@@ -26795,12 +27609,12 @@ init_protocol();
26795
27609
  init_protocol();
26796
27610
 
26797
27611
  // src/session/fork.ts
26798
- var import_node_fs20 = __toESM(require("fs"), 1);
27612
+ var import_node_fs22 = __toESM(require("fs"), 1);
26799
27613
  var import_node_os12 = __toESM(require("os"), 1);
26800
- var import_node_path21 = __toESM(require("path"), 1);
27614
+ var import_node_path24 = __toESM(require("path"), 1);
26801
27615
  init_claude_history();
26802
27616
  function readJsonlEntries(file) {
26803
- const raw = import_node_fs20.default.readFileSync(file, "utf8");
27617
+ const raw = import_node_fs22.default.readFileSync(file, "utf8");
26804
27618
  const out = [];
26805
27619
  for (const line of raw.split("\n")) {
26806
27620
  const t = line.trim();
@@ -26813,10 +27627,10 @@ function readJsonlEntries(file) {
26813
27627
  return out;
26814
27628
  }
26815
27629
  function forkSession(input) {
26816
- const baseDir = input.baseDir ?? import_node_path21.default.join(import_node_os12.default.homedir(), ".claude");
26817
- const projectDir = import_node_path21.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
26818
- const sourceFile = import_node_path21.default.join(projectDir, `${input.toolSessionId}.jsonl`);
26819
- if (!import_node_fs20.default.existsSync(sourceFile)) {
27630
+ const baseDir = input.baseDir ?? import_node_path24.default.join(import_node_os12.default.homedir(), ".claude");
27631
+ const projectDir = import_node_path24.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
27632
+ const sourceFile = import_node_path24.default.join(projectDir, `${input.toolSessionId}.jsonl`);
27633
+ if (!import_node_fs22.default.existsSync(sourceFile)) {
26820
27634
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
26821
27635
  }
26822
27636
  const entries = readJsonlEntries(sourceFile);
@@ -26846,9 +27660,9 @@ function forkSession(input) {
26846
27660
  }
26847
27661
  forkedLines.push(JSON.stringify(forked));
26848
27662
  }
26849
- const forkedFilePath = import_node_path21.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
26850
- import_node_fs20.default.mkdirSync(projectDir, { recursive: true });
26851
- import_node_fs20.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
27663
+ const forkedFilePath = import_node_path24.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
27664
+ import_node_fs22.default.mkdirSync(projectDir, { recursive: true });
27665
+ import_node_fs22.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
26852
27666
  return { forkedToolSessionId, forkedFilePath };
26853
27667
  }
26854
27668
 
@@ -27169,9 +27983,9 @@ init_protocol();
27169
27983
 
27170
27984
  // src/workspace/git.ts
27171
27985
  var import_node_child_process6 = require("child_process");
27172
- var import_node_fs21 = __toESM(require("fs"), 1);
27986
+ var import_node_fs23 = __toESM(require("fs"), 1);
27173
27987
  var import_node_os13 = __toESM(require("os"), 1);
27174
- var import_node_path22 = __toESM(require("path"), 1);
27988
+ var import_node_path25 = __toESM(require("path"), 1);
27175
27989
  var import_node_util = require("util");
27176
27990
  var pexec = (0, import_node_util.promisify)(import_node_child_process6.execFile);
27177
27991
  function formatChildProcessError(err) {
@@ -27186,9 +28000,9 @@ function formatChildProcessError(err) {
27186
28000
  return e.message ?? "unknown error";
27187
28001
  }
27188
28002
  function normalizePath(p2) {
27189
- const resolved = import_node_path22.default.resolve(p2);
28003
+ const resolved = import_node_path25.default.resolve(p2);
27190
28004
  try {
27191
- return import_node_fs21.default.realpathSync(resolved);
28005
+ return import_node_fs23.default.realpathSync(resolved);
27192
28006
  } catch {
27193
28007
  return resolved;
27194
28008
  }
@@ -27289,13 +28103,13 @@ function flattenToDirName(branch) {
27289
28103
  }
27290
28104
  function encodeClaudeProjectDir(absPath) {
27291
28105
  if (!absPath || typeof absPath !== "string") return "";
27292
- let canonical = import_node_path22.default.resolve(absPath);
28106
+ let canonical = import_node_path25.default.resolve(absPath);
27293
28107
  try {
27294
- canonical = import_node_fs21.default.realpathSync(canonical);
28108
+ canonical = import_node_fs23.default.realpathSync(canonical);
27295
28109
  } catch {
27296
28110
  try {
27297
- const parent = import_node_fs21.default.realpathSync(import_node_path22.default.dirname(canonical));
27298
- canonical = import_node_path22.default.join(parent, import_node_path22.default.basename(canonical));
28111
+ const parent = import_node_fs23.default.realpathSync(import_node_path25.default.dirname(canonical));
28112
+ canonical = import_node_path25.default.join(parent, import_node_path25.default.basename(canonical));
27299
28113
  } catch {
27300
28114
  }
27301
28115
  }
@@ -27319,11 +28133,11 @@ async function createWorktree(input) {
27319
28133
  if (!isGitRoot) {
27320
28134
  throw new Error(`\u76EE\u5F55 ${cwd} \u4E0D\u662F git repo \u6839`);
27321
28135
  }
27322
- const parent = import_node_path22.default.dirname(import_node_path22.default.resolve(cwd));
27323
- if (parent === "/" || parent === import_node_path22.default.resolve(cwd)) {
28136
+ const parent = import_node_path25.default.dirname(import_node_path25.default.resolve(cwd));
28137
+ if (parent === "/" || parent === import_node_path25.default.resolve(cwd)) {
27324
28138
  throw new Error("repo \u5728\u78C1\u76D8\u6839\u76EE\u5F55\uFF0C\u65E0\u6CD5\u5728\u540C\u7EA7\u521B\u5EFA worktree");
27325
28139
  }
27326
- const worktreeRoot = import_node_path22.default.join(parent, dirName);
28140
+ const worktreeRoot = import_node_path25.default.join(parent, dirName);
27327
28141
  try {
27328
28142
  await pexec("git", ["-C", cwd, "fetch", "origin", baseBranch, "--no-tags"], {
27329
28143
  timeout: 3e4
@@ -27342,7 +28156,7 @@ async function createWorktree(input) {
27342
28156
  const msg = err.message;
27343
28157
  if (msg.startsWith("\u5206\u652F ")) throw err;
27344
28158
  }
27345
- if (import_node_fs21.default.existsSync(worktreeRoot)) {
28159
+ if (import_node_fs23.default.existsSync(worktreeRoot)) {
27346
28160
  throw new Error(`\u76EE\u5F55 ${worktreeRoot} \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E00\u4E2A label \u6216\u6E05\u7406\u540E\u91CD\u8BD5`);
27347
28161
  }
27348
28162
  try {
@@ -27370,8 +28184,8 @@ async function removeWorktree(input) {
27370
28184
  );
27371
28185
  const gitCommonDir = stdout.trim();
27372
28186
  if (!gitCommonDir) throw new Error("empty git-common-dir");
27373
- const absGitCommon = import_node_path22.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path22.default.resolve(worktreeRoot, gitCommonDir);
27374
- repoRoot = import_node_path22.default.dirname(absGitCommon);
28187
+ const absGitCommon = import_node_path25.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path25.default.resolve(worktreeRoot, gitCommonDir);
28188
+ repoRoot = import_node_path25.default.dirname(absGitCommon);
27375
28189
  } catch {
27376
28190
  repoRoot = null;
27377
28191
  }
@@ -27383,7 +28197,7 @@ async function removeWorktree(input) {
27383
28197
  } catch (err) {
27384
28198
  const stderr = err.stderr ?? "";
27385
28199
  const lower = stderr.toLowerCase();
27386
- const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs21.default.existsSync(worktreeRoot);
28200
+ const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs23.default.existsSync(worktreeRoot);
27387
28201
  if (!vanished) {
27388
28202
  throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
27389
28203
  }
@@ -27402,10 +28216,10 @@ async function removeWorktree(input) {
27402
28216
  try {
27403
28217
  const encoded = encodeClaudeProjectDir(worktreeRoot);
27404
28218
  if (encoded) {
27405
- const projectsRoot = import_node_path22.default.join(import_node_os13.default.homedir(), ".claude", "projects");
27406
- const target = import_node_path22.default.resolve(projectsRoot, encoded);
27407
- if (target.startsWith(projectsRoot + import_node_path22.default.sep) && target !== projectsRoot) {
27408
- import_node_fs21.default.rmSync(target, { recursive: true, force: true });
28219
+ const projectsRoot = import_node_path25.default.join(import_node_os13.default.homedir(), ".claude", "projects");
28220
+ const target = import_node_path25.default.resolve(projectsRoot, encoded);
28221
+ if (target.startsWith(projectsRoot + import_node_path25.default.sep) && target !== projectsRoot) {
28222
+ import_node_fs23.default.rmSync(target, { recursive: true, force: true });
27409
28223
  }
27410
28224
  }
27411
28225
  } catch {
@@ -27484,7 +28298,7 @@ init_protocol();
27484
28298
  var version = "0.2.6".length > 0 ? "0.2.6" : "dev";
27485
28299
 
27486
28300
  // src/handlers/meta.ts
27487
- function buildReadyFrame(deps) {
28301
+ function buildReadyFrame(deps, client) {
27488
28302
  const info = deps.manager.info();
27489
28303
  const tools = [];
27490
28304
  for (const id of listRegistered()) {
@@ -27496,6 +28310,14 @@ function buildReadyFrame(deps) {
27496
28310
  }
27497
28311
  }
27498
28312
  const tunnelUrl = deps.getTunnelUrl ? deps.getTunnelUrl() : null;
28313
+ const fileSharing = {};
28314
+ const httpBaseUrl = deps.getHttpBaseUrl ? deps.getHttpBaseUrl() : null;
28315
+ if (httpBaseUrl) {
28316
+ fileSharing.tokenRole = "owner";
28317
+ fileSharing.isLoopback = isLoopbackAddr(client?.remoteAddress);
28318
+ fileSharing.httpBaseUrl = httpBaseUrl;
28319
+ if (deps.httpToken) fileSharing.httpToken = deps.httpToken;
28320
+ }
27499
28321
  return {
27500
28322
  version,
27501
28323
  protocolVersion: PROTOCOL_VERSION,
@@ -27504,7 +28326,8 @@ function buildReadyFrame(deps) {
27504
28326
  tools,
27505
28327
  runningSessions: info.runningSessions,
27506
28328
  tunnelUrl,
27507
- mode: deps.mode
28329
+ mode: deps.mode,
28330
+ ...fileSharing
27508
28331
  };
27509
28332
  }
27510
28333
  function buildMetaHandlers(deps) {
@@ -27615,6 +28438,119 @@ function buildPersonaHandlers(deps) {
27615
28438
  };
27616
28439
  }
27617
28440
 
28441
+ // src/handlers/attachment.ts
28442
+ init_protocol();
28443
+ init_protocol();
28444
+ var DEFAULT_TTL_SECONDS = 24 * 3600;
28445
+ function buildAttachmentHandlers(deps) {
28446
+ const signUrl = async (frame) => {
28447
+ const parsed = AttachmentSignUrlArgs.safeParse(frame);
28448
+ if (!parsed.success) {
28449
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28450
+ }
28451
+ const args = parsed.data;
28452
+ const secret = deps.getSignSecret();
28453
+ if (!secret) {
28454
+ throw new ClawdError(
28455
+ ERROR_CODES.METHOD_NOT_IMPLEMENTED,
28456
+ "signUrl requires an owner token (daemon noAuth mode disables share URLs)"
28457
+ );
28458
+ }
28459
+ const httpBaseUrl = deps.getHttpBaseUrl();
28460
+ if (!httpBaseUrl) {
28461
+ throw new ClawdError(
28462
+ ERROR_CODES.METHOD_NOT_IMPLEMENTED,
28463
+ "httpBaseUrl unavailable (daemon HTTP not ready)"
28464
+ );
28465
+ }
28466
+ const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
28467
+ const parts = signUrlParts(secret, args.absPath, ttl);
28468
+ const url = buildSignedFileUrl(httpBaseUrl, parts);
28469
+ return {
28470
+ response: {
28471
+ type: "attachment.signUrl",
28472
+ url,
28473
+ expiresAt: parts.e === null ? null : parts.e * 1e3
28474
+ }
28475
+ };
28476
+ };
28477
+ const groupAdd = async (frame) => {
28478
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28479
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28480
+ }
28481
+ const parsed = AttachmentGroupAddArgs.safeParse(frame);
28482
+ if (!parsed.success) {
28483
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28484
+ }
28485
+ const args = parsed.data;
28486
+ const scope = deps.getSessionScope(args.sessionId);
28487
+ if (!scope) {
28488
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
28489
+ }
28490
+ const size = 0;
28491
+ const entry = deps.groupFileStore.upsert(scope, args.sessionId, {
28492
+ relPath: args.relPath,
28493
+ from: "owner",
28494
+ label: args.label,
28495
+ size,
28496
+ mime: lookupMime(args.relPath)
28497
+ });
28498
+ return { response: { type: "attachment.groupAdd", entry } };
28499
+ };
28500
+ const groupRemove = async (frame) => {
28501
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28502
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28503
+ }
28504
+ const parsed = AttachmentGroupRemoveArgs.safeParse(frame);
28505
+ if (!parsed.success) {
28506
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28507
+ }
28508
+ const scope = deps.getSessionScope(parsed.data.sessionId);
28509
+ if (!scope) {
28510
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "session not found");
28511
+ }
28512
+ const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
28513
+ const target = entries.find((e) => e.relPath === parsed.data.relPath);
28514
+ if (target?.from === "owner") {
28515
+ deps.groupFileStore.remove(scope, parsed.data.sessionId, parsed.data.relPath);
28516
+ } else {
28517
+ deps.groupFileStore.markStale(scope, parsed.data.sessionId, parsed.data.relPath);
28518
+ }
28519
+ return { response: { type: "attachment.groupRemove", removed: true } };
28520
+ };
28521
+ const groupList = async (frame) => {
28522
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28523
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28524
+ }
28525
+ const parsed = AttachmentGroupListArgs.safeParse(frame);
28526
+ if (!parsed.success) {
28527
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28528
+ }
28529
+ const scope = deps.getSessionScope(parsed.data.sessionId);
28530
+ if (!scope) return { response: { type: "attachment.groupList", entries: [] } };
28531
+ const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
28532
+ return { response: { type: "attachment.groupList", entries } };
28533
+ };
28534
+ const groupListPersona = async (frame) => {
28535
+ if (!deps.groupFileStore) {
28536
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28537
+ }
28538
+ const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
28539
+ if (!parsed.success) {
28540
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28541
+ }
28542
+ const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
28543
+ return { response: { type: "attachment.groupListPersona", perSession } };
28544
+ };
28545
+ return {
28546
+ "attachment.signUrl": signUrl,
28547
+ "attachment.groupAdd": groupAdd,
28548
+ "attachment.groupRemove": groupRemove,
28549
+ "attachment.groupList": groupList,
28550
+ "attachment.groupListPersona": groupListPersona
28551
+ };
28552
+ }
28553
+
27618
28554
  // src/handlers/index.ts
27619
28555
  function buildMethodHandlers(deps) {
27620
28556
  return {
@@ -27630,7 +28566,8 @@ function buildMethodHandlers(deps) {
27630
28566
  personaRegistry: deps.personaRegistry,
27631
28567
  sessionManager: deps.manager,
27632
28568
  personaBoundHandler: deps.personaBoundHandler
27633
- })
28569
+ }),
28570
+ ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27634
28571
  };
27635
28572
  }
27636
28573
 
@@ -27638,7 +28575,7 @@ function buildMethodHandlers(deps) {
27638
28575
  async function startDaemon(config) {
27639
28576
  const logger = createLogger({
27640
28577
  level: config.logLevel,
27641
- file: import_node_path23.default.join(config.dataDir, "clawd.log")
28578
+ file: import_node_path26.default.join(config.dataDir, "clawd.log")
27642
28579
  });
27643
28580
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
27644
28581
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -27670,7 +28607,7 @@ async function startDaemon(config) {
27670
28607
  const agents = new AgentsScanner();
27671
28608
  const history = new ClaudeHistoryReader();
27672
28609
  let transport = null;
27673
- const personaStore = new PersonaStore(import_node_path23.default.join(config.dataDir, "personas"));
28610
+ const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
27674
28611
  const defaultsRoot = findDefaultsRoot();
27675
28612
  if (defaultsRoot) {
27676
28613
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -27678,13 +28615,14 @@ async function startDaemon(config) {
27678
28615
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
27679
28616
  }
27680
28617
  const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
28618
+ const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
27681
28619
  const manager = new SessionManager({
27682
28620
  store,
27683
28621
  logger,
27684
28622
  getAdapter,
27685
28623
  historyReader: history,
27686
28624
  dataDir: config.dataDir,
27687
- personaRoot: import_node_path23.default.join(config.dataDir, "personas"),
28625
+ personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
27688
28626
  personaStore,
27689
28627
  ownerDisplayName,
27690
28628
  mode: config.mode,
@@ -27701,6 +28639,38 @@ async function startDaemon(config) {
27701
28639
  return;
27702
28640
  }
27703
28641
  transport?.broadcastToSession(sid, frame);
28642
+ },
28643
+ // file-sharing (spec §6 PR 3):runner 检测到成功 file-edit tool_result 时,
28644
+ // 闭包 stat + mime 写入群清单。stat 失败不阻塞主流程(log warn + 跳过本条),
28645
+ // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
28646
+ attachmentGroup: {
28647
+ onFileEdit: (input) => {
28648
+ const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
28649
+ let size = 0;
28650
+ try {
28651
+ size = import_node_fs24.default.statSync(absPath).size;
28652
+ } catch (err) {
28653
+ logger.warn("attachment.onFileEdit stat failed", {
28654
+ sessionId: input.sessionId,
28655
+ absPath,
28656
+ err: err.message
28657
+ });
28658
+ }
28659
+ try {
28660
+ groupFileStore.upsert(input.scope, input.sessionId, {
28661
+ relPath: input.relPath,
28662
+ from: "agent",
28663
+ size,
28664
+ mime: lookupMime(input.relPath)
28665
+ });
28666
+ } catch (err) {
28667
+ logger.warn("attachment.onFileEdit upsert failed", {
28668
+ sessionId: input.sessionId,
28669
+ relPath: input.relPath,
28670
+ err: err.message
28671
+ });
28672
+ }
28673
+ }
27704
28674
  }
27705
28675
  });
27706
28676
  const observer = new SessionObserver({
@@ -27746,6 +28716,12 @@ async function startDaemon(config) {
27746
28716
  sessionManager: manager
27747
28717
  });
27748
28718
  let currentTunnelUrl = null;
28719
+ const getHttpBaseUrl = () => {
28720
+ if (currentTunnelUrl) {
28721
+ return currentTunnelUrl.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:");
28722
+ }
28723
+ return `http://${config.host}:${config.port}`;
28724
+ };
27749
28725
  const personaBoundHandler = new PersonaBoundHandler({
27750
28726
  registry: personaRegistry,
27751
28727
  personaManager,
@@ -27771,22 +28747,68 @@ async function startDaemon(config) {
27771
28747
  getTunnelUrl: () => currentTunnelUrl,
27772
28748
  // ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
27773
28749
  // 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
27774
- mode: config.mode
28750
+ mode: config.mode,
28751
+ // file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
28752
+ // PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 null(UI 看到无 httpToken
28753
+ // 时禁用文件 GET/POST,保持 1.0 行为兼容)。
28754
+ getHttpBaseUrl,
28755
+ httpToken: resolvedAuthToken,
28756
+ // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
28757
+ // 根据 sessionId 反查 scope 写盘。
28758
+ attachment: {
28759
+ groupFileStore,
28760
+ getHttpBaseUrl,
28761
+ // HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
28762
+ // noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
28763
+ getSignSecret: () => resolvedAuthToken ?? "",
28764
+ // group RPC:根据 sessionId 反查 scope;owner-mode persona session 走
28765
+ // 'persona/<pid>/owner',default 走 'default'。
28766
+ getSessionScope: (sessionId) => {
28767
+ const file = store.read(sessionId);
28768
+ if (!file) return null;
28769
+ if (file.ownerPersonaId) {
28770
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
28771
+ }
28772
+ return { kind: "default" };
28773
+ }
28774
+ }
28775
+ });
28776
+ const authResolver = new AuthContextResolver({
28777
+ ownerToken: resolvedAuthToken,
28778
+ personaRegistry
28779
+ });
28780
+ const httpRouter = createHttpRouter({
28781
+ authResolver,
28782
+ daemonVersion: version,
28783
+ logger,
28784
+ personaStore,
28785
+ groupFileStore,
28786
+ sessionStore: store,
28787
+ // /files HMAC verify 用同一份 owner token 做 secret(与 attachment.signUrl 同源)
28788
+ getSignSecret: () => resolvedAuthToken ?? null
27775
28789
  });
27776
28790
  wsServer = new LocalWsServer({
27777
28791
  host: config.host,
27778
28792
  port: config.port,
27779
28793
  logger,
27780
- readyFrameBuilder: () => buildReadyFrame({
27781
- manager,
27782
- getAdapter,
27783
- getTunnelUrl: () => currentTunnelUrl,
27784
- // ready mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
27785
- mode: config.mode
27786
- }),
28794
+ readyFrameBuilder: (ctx) => buildReadyFrame(
28795
+ {
28796
+ manager,
28797
+ getAdapter,
28798
+ getTunnelUrl: () => currentTunnelUrl,
28799
+ // ready 帧 mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
28800
+ mode: config.mode,
28801
+ // file-sharing 字段:httpBaseUrl 跟 tunnel 状态走;httpToken 复用 owner WS token
28802
+ getHttpBaseUrl,
28803
+ httpToken: resolvedAuthToken
28804
+ },
28805
+ ctx
28806
+ ),
27787
28807
  protocolVersion: PROTOCOL_VERSION,
27788
28808
  authGate: authGate ?? void 0,
27789
28809
  personaBoundHandler,
28810
+ // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
28811
+ httpRequestHandler: httpRouter,
27790
28812
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
27791
28813
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
27792
28814
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -27902,8 +28924,8 @@ async function startDaemon(config) {
27902
28924
  const lines = [
27903
28925
  `Tunnel: ${r.url}`,
27904
28926
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
27905
- `Frpc config: ${import_node_path23.default.join(config.dataDir, "frpc.toml")}`,
27906
- `Frpc log: ${import_node_path23.default.join(config.dataDir, "frpc.log")}`
28927
+ `Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
28928
+ `Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
27907
28929
  ];
27908
28930
  const width = Math.max(...lines.map((l) => l.length));
27909
28931
  const bar = "\u2550".repeat(width + 4);
@@ -27916,8 +28938,8 @@ ${bar}
27916
28938
 
27917
28939
  `);
27918
28940
  try {
27919
- const connectPath = import_node_path23.default.join(config.dataDir, "connect.txt");
27920
- import_node_fs22.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
28941
+ const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
28942
+ import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
27921
28943
  } catch {
27922
28944
  }
27923
28945
  } catch (err) {