@clawos-dev/clawd 0.2.64-beta.103.774930b → 0.2.64-beta.104.6f4b611

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 +1821 -340
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -110,6 +110,22 @@ var init_methods = __esm({
110
110
  // 触发频率低(仅 window resize),response 也是 ack。
111
111
  "session:pty:input",
112
112
  "session:pty:resize",
113
+ // ---- attachment.* file-sharing(spec §5;详见 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/POST,详见 spec §5),不在白名单内。
118
+ "attachment.outboxCreate",
119
+ "attachment.outboxRevoke",
120
+ "attachment.outboxList",
121
+ "attachment.mountAdd",
122
+ "attachment.mountRemove",
123
+ "attachment.mountList",
124
+ "attachment.groupAdd",
125
+ "attachment.groupRemove",
126
+ "attachment.groupList",
127
+ // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
128
+ "attachment.groupListPersona",
113
129
  "info",
114
130
  "ping"
115
131
  ];
@@ -601,8 +617,8 @@ var init_parseUtil = __esm({
601
617
  init_errors2();
602
618
  init_en();
603
619
  makeIssue = (params) => {
604
- const { data, path: path28, errorMaps, issueData } = params;
605
- const fullPath = [...path28, ...issueData.path || []];
620
+ const { data, path: path34, errorMaps, issueData } = params;
621
+ const fullPath = [...path34, ...issueData.path || []];
606
622
  const fullIssue = {
607
623
  ...issueData,
608
624
  path: fullPath
@@ -913,11 +929,11 @@ var init_types = __esm({
913
929
  init_parseUtil();
914
930
  init_util();
915
931
  ParseInputLazyPath = class {
916
- constructor(parent, value, path28, key) {
932
+ constructor(parent, value, path34, key) {
917
933
  this._cachedPath = [];
918
934
  this.parent = parent;
919
935
  this.data = value;
920
- this._path = path28;
936
+ this._path = path34;
921
937
  this._key = key;
922
938
  }
923
939
  get path() {
@@ -4300,6 +4316,151 @@ var init_zod = __esm({
4300
4316
  }
4301
4317
  });
4302
4318
 
4319
+ // ../protocol/src/attachment-schemas.ts
4320
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, MOUNT_MODES, GroupFileEntrySchema, MountEntrySchema, OutboxScopeSchema, OutboxCapEntrySchema, AttachmentOutboxCreateArgs, AttachmentOutboxCreateResponseSchema, AttachmentOutboxRevokeArgs, AttachmentOutboxRevokeResponseSchema, AttachmentOutboxListArgs, AttachmentOutboxListResponseSchema, AttachmentMountAddArgs, AttachmentMountAddResponseSchema, AttachmentMountRemoveArgs, AttachmentMountRemoveResponseSchema, AttachmentMountListArgs, AttachmentMountListResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4321
+ var init_attachment_schemas = __esm({
4322
+ "../protocol/src/attachment-schemas.ts"() {
4323
+ "use strict";
4324
+ init_zod();
4325
+ TOKEN_ROLES = ["owner", "personal"];
4326
+ GROUP_FILE_SOURCES = ["agent", "user", "owner"];
4327
+ MOUNT_MODES = ["link", "copy"];
4328
+ GroupFileEntrySchema = external_exports.object({
4329
+ /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
4330
+ id: external_exports.string().min(1),
4331
+ /** 相对 personaDir / sessionCwd 的路径 */
4332
+ relPath: external_exports.string().min(1),
4333
+ from: external_exports.enum(GROUP_FILE_SOURCES),
4334
+ /** user 上传时的客户端标识(personal label / hostname),from='user' 才有 */
4335
+ addedBy: external_exports.string().optional(),
4336
+ /** owner 手动加群时的可选备注 */
4337
+ label: external_exports.string().optional(),
4338
+ /** 文件字节数(stat 时拍) */
4339
+ size: external_exports.number().int().nonnegative(),
4340
+ mime: external_exports.string().min(1),
4341
+ /** 入群时间戳(ms) */
4342
+ addedAt: external_exports.number().int().nonnegative(),
4343
+ /** 最近一次 agent Write/Edit 时间戳(agent 反复改同 path 时更新) */
4344
+ lastEditedAt: external_exports.number().int().nonnegative().optional(),
4345
+ /** agent rm / 文件不见了时打标;UI 灰显,不能再 share */
4346
+ stale: external_exports.boolean().optional()
4347
+ });
4348
+ MountEntrySchema = external_exports.object({
4349
+ /** personaDir 下的 basename(link 目标 / copy 目的名) */
4350
+ basename: external_exports.string().min(1),
4351
+ /** 原始绝对路径(link 模式下 realpath 后用于 sandbox.allowRead 派生) */
4352
+ absPath: external_exports.string().min(1),
4353
+ mode: external_exports.enum(MOUNT_MODES)
4354
+ });
4355
+ OutboxScopeSchema = external_exports.discriminatedUnion("kind", [
4356
+ external_exports.object({ kind: external_exports.literal("public") }),
4357
+ external_exports.object({ kind: external_exports.literal("personal"), personalId: external_exports.string().min(1) })
4358
+ ]);
4359
+ OutboxCapEntrySchema = external_exports.object({
4360
+ capToken: external_exports.string().min(1),
4361
+ /** 归属:persona 域 cap 带 personaId;direct 会话 cap 带 sessionId */
4362
+ scopeRef: external_exports.union([
4363
+ external_exports.object({ kind: external_exports.literal("persona"), personaId: external_exports.string().min(1) }),
4364
+ external_exports.object({ kind: external_exports.literal("session"), sessionId: external_exports.string().min(1) })
4365
+ ]),
4366
+ absPath: external_exports.string().min(1),
4367
+ /** display name(UI 列表显示 + Share dialog 标题),通常等于 basename */
4368
+ name: external_exports.string().min(1),
4369
+ expiresAt: external_exports.number().int().nonnegative().nullable(),
4370
+ oneShot: external_exports.boolean(),
4371
+ scope: OutboxScopeSchema,
4372
+ hits: external_exports.number().int().nonnegative(),
4373
+ revoked: external_exports.boolean().optional(),
4374
+ createdAt: external_exports.number().int().nonnegative()
4375
+ });
4376
+ AttachmentOutboxCreateArgs = external_exports.object({
4377
+ /** persona 域:传 personaId;direct 会话:传 sessionId(二选一) */
4378
+ personaId: external_exports.string().min(1).optional(),
4379
+ sessionId: external_exports.string().min(1).optional(),
4380
+ absPath: external_exports.string().min(1),
4381
+ /** TTL 秒数;缺省 24h;'never' 走 null */
4382
+ ttlSeconds: external_exports.number().int().positive().nullable().optional(),
4383
+ oneShot: external_exports.boolean().optional(),
4384
+ scope: OutboxScopeSchema.optional()
4385
+ });
4386
+ AttachmentOutboxCreateResponseSchema = external_exports.object({
4387
+ capToken: external_exports.string().min(1),
4388
+ /** 完整 URL(含 httpBaseUrl 前缀),UI 直接复制到剪贴板 */
4389
+ url: external_exports.string().min(1),
4390
+ expiresAt: external_exports.number().int().nonnegative().nullable()
4391
+ });
4392
+ AttachmentOutboxRevokeArgs = external_exports.object({
4393
+ capToken: external_exports.string().min(1)
4394
+ });
4395
+ AttachmentOutboxRevokeResponseSchema = external_exports.object({
4396
+ revoked: external_exports.literal(true)
4397
+ });
4398
+ AttachmentOutboxListArgs = external_exports.object({
4399
+ /** 缺省 = 全部(含 direct 会话);传 personaId 过滤到该 persona */
4400
+ personaId: external_exports.string().min(1).optional()
4401
+ });
4402
+ AttachmentOutboxListResponseSchema = external_exports.object({
4403
+ entries: external_exports.array(OutboxCapEntrySchema)
4404
+ });
4405
+ AttachmentMountAddArgs = external_exports.object({
4406
+ personaId: external_exports.string().min(1),
4407
+ absPath: external_exports.string().min(1),
4408
+ mode: external_exports.enum(MOUNT_MODES)
4409
+ });
4410
+ AttachmentMountAddResponseSchema = external_exports.object({
4411
+ entry: MountEntrySchema,
4412
+ /** sandbox.allowRead 派生后是否需要重启 session 才能让 CC 子进程跟 symlink */
4413
+ sandboxRestartNeeded: external_exports.boolean()
4414
+ });
4415
+ AttachmentMountRemoveArgs = external_exports.object({
4416
+ personaId: external_exports.string().min(1),
4417
+ basename: external_exports.string().min(1)
4418
+ });
4419
+ AttachmentMountRemoveResponseSchema = external_exports.object({
4420
+ removed: external_exports.literal(true)
4421
+ });
4422
+ AttachmentMountListArgs = external_exports.object({
4423
+ personaId: external_exports.string().min(1)
4424
+ });
4425
+ AttachmentMountListResponseSchema = external_exports.object({
4426
+ entries: external_exports.array(MountEntrySchema)
4427
+ });
4428
+ AttachmentGroupAddArgs = external_exports.object({
4429
+ sessionId: external_exports.string().min(1),
4430
+ relPath: external_exports.string().min(1),
4431
+ /** owner 手动加群时可选备注;agent / inbox 自动入清单不走此 RPC */
4432
+ label: external_exports.string().optional()
4433
+ });
4434
+ AttachmentGroupAddResponseSchema = external_exports.object({
4435
+ entry: GroupFileEntrySchema
4436
+ });
4437
+ AttachmentGroupRemoveArgs = external_exports.object({
4438
+ sessionId: external_exports.string().min(1),
4439
+ relPath: external_exports.string().min(1)
4440
+ });
4441
+ AttachmentGroupRemoveResponseSchema = external_exports.object({
4442
+ removed: external_exports.literal(true)
4443
+ });
4444
+ AttachmentGroupListArgs = external_exports.object({
4445
+ sessionId: external_exports.string().min(1)
4446
+ });
4447
+ AttachmentGroupListResponseSchema = external_exports.object({
4448
+ entries: external_exports.array(GroupFileEntrySchema)
4449
+ });
4450
+ AttachmentGroupListPersonaArgs = external_exports.object({
4451
+ personaId: external_exports.string().min(1)
4452
+ });
4453
+ AttachmentGroupListPersonaResponseSchema = external_exports.object({
4454
+ perSession: external_exports.array(
4455
+ external_exports.object({
4456
+ sessionId: external_exports.string().min(1),
4457
+ entries: external_exports.array(GroupFileEntrySchema)
4458
+ })
4459
+ )
4460
+ });
4461
+ }
4462
+ });
4463
+
4303
4464
  // ../protocol/src/persona-schemas.ts
4304
4465
  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
4466
  var init_persona_schemas = __esm({
@@ -4505,6 +4666,7 @@ var init_schemas = __esm({
4505
4666
  "use strict";
4506
4667
  init_zod();
4507
4668
  init_events();
4669
+ init_attachment_schemas();
4508
4670
  init_persona_schemas();
4509
4671
  SessionStatusSchema = external_exports.enum(SESSION_STATUS_VALUES);
4510
4672
  UsageSchema = external_exports.object({
@@ -5061,7 +5223,22 @@ var init_schemas = __esm({
5061
5223
  hostname: external_exports.string(),
5062
5224
  os: external_exports.string(),
5063
5225
  tools: external_exports.array(external_exports.object({ id: external_exports.string(), available: external_exports.boolean() })),
5064
- runningSessions: external_exports.array(InfoRunningSessionSchema)
5226
+ runningSessions: external_exports.array(InfoRunningSessionSchema),
5227
+ // ── file-sharing 会话身份回执(spec: superpowers/specs/2026-05-19-clawd-file-sharing-design.md §8) ──
5228
+ // PR 1 阶段标 optional(daemon 还没下发,旧 daemon info 响应不带这五个字段);
5229
+ // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5230
+ // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5231
+ // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5232
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5233
+ tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5234
+ /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5235
+ tokenPersonaId: external_exports.string().min(1).optional(),
5236
+ /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5237
+ isLoopback: external_exports.boolean().optional(),
5238
+ /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
5239
+ httpBaseUrl: external_exports.string().optional(),
5240
+ /** file-sharing HTTP 路由的 Authorization: Bearer token;与 WS token 解耦,HTTP 401 仅触发 ReAuth 不强踢 WS(spec §11 第 4 条) */
5241
+ httpToken: external_exports.string().optional()
5065
5242
  });
5066
5243
  }
5067
5244
  });
@@ -5093,6 +5270,7 @@ var init_runtime = __esm({
5093
5270
  init_frames();
5094
5271
  init_persona_schemas();
5095
5272
  init_persona_mode();
5273
+ init_attachment_schemas();
5096
5274
  }
5097
5275
  });
5098
5276
 
@@ -5367,8 +5545,8 @@ var require_req = __commonJS({
5367
5545
  if (req.originalUrl) {
5368
5546
  _req.url = req.originalUrl;
5369
5547
  } else {
5370
- const path28 = req.path;
5371
- _req.url = typeof path28 === "string" ? path28 : req.url ? req.url.path || req.url : void 0;
5548
+ const path34 = req.path;
5549
+ _req.url = typeof path34 === "string" ? path34 : req.url ? req.url.path || req.url : void 0;
5372
5550
  }
5373
5551
  if (req.query) {
5374
5552
  _req.query = req.query;
@@ -5533,14 +5711,14 @@ var require_redact = __commonJS({
5533
5711
  }
5534
5712
  return obj;
5535
5713
  }
5536
- function parsePath(path28) {
5714
+ function parsePath(path34) {
5537
5715
  const parts = [];
5538
5716
  let current = "";
5539
5717
  let inBrackets = false;
5540
5718
  let inQuotes = false;
5541
5719
  let quoteChar = "";
5542
- for (let i = 0; i < path28.length; i++) {
5543
- const char = path28[i];
5720
+ for (let i = 0; i < path34.length; i++) {
5721
+ const char = path34[i];
5544
5722
  if (!inBrackets && char === ".") {
5545
5723
  if (current) {
5546
5724
  parts.push(current);
@@ -5671,10 +5849,10 @@ var require_redact = __commonJS({
5671
5849
  return current;
5672
5850
  }
5673
5851
  function redactPaths(obj, paths, censor, remove = false) {
5674
- for (const path28 of paths) {
5675
- const parts = parsePath(path28);
5852
+ for (const path34 of paths) {
5853
+ const parts = parsePath(path34);
5676
5854
  if (parts.includes("*")) {
5677
- redactWildcardPath(obj, parts, censor, path28, remove);
5855
+ redactWildcardPath(obj, parts, censor, path34, remove);
5678
5856
  } else {
5679
5857
  if (remove) {
5680
5858
  removeKey(obj, parts);
@@ -5759,8 +5937,8 @@ var require_redact = __commonJS({
5759
5937
  }
5760
5938
  } else {
5761
5939
  if (afterWildcard.includes("*")) {
5762
- const wrappedCensor = typeof censor === "function" ? (value, path28) => {
5763
- const fullPath = [...pathArray.slice(0, pathLength), ...path28];
5940
+ const wrappedCensor = typeof censor === "function" ? (value, path34) => {
5941
+ const fullPath = [...pathArray.slice(0, pathLength), ...path34];
5764
5942
  return censor(value, fullPath);
5765
5943
  } : censor;
5766
5944
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5795,8 +5973,8 @@ var require_redact = __commonJS({
5795
5973
  return null;
5796
5974
  }
5797
5975
  const pathStructure = /* @__PURE__ */ new Map();
5798
- for (const path28 of pathsToClone) {
5799
- const parts = parsePath(path28);
5976
+ for (const path34 of pathsToClone) {
5977
+ const parts = parsePath(path34);
5800
5978
  let current = pathStructure;
5801
5979
  for (let i = 0; i < parts.length; i++) {
5802
5980
  const part = parts[i];
@@ -5848,24 +6026,24 @@ var require_redact = __commonJS({
5848
6026
  }
5849
6027
  return cloneSelectively(obj, pathStructure);
5850
6028
  }
5851
- function validatePath(path28) {
5852
- if (typeof path28 !== "string") {
6029
+ function validatePath(path34) {
6030
+ if (typeof path34 !== "string") {
5853
6031
  throw new Error("Paths must be (non-empty) strings");
5854
6032
  }
5855
- if (path28 === "") {
6033
+ if (path34 === "") {
5856
6034
  throw new Error("Invalid redaction path ()");
5857
6035
  }
5858
- if (path28.includes("..")) {
5859
- throw new Error(`Invalid redaction path (${path28})`);
6036
+ if (path34.includes("..")) {
6037
+ throw new Error(`Invalid redaction path (${path34})`);
5860
6038
  }
5861
- if (path28.includes(",")) {
5862
- throw new Error(`Invalid redaction path (${path28})`);
6039
+ if (path34.includes(",")) {
6040
+ throw new Error(`Invalid redaction path (${path34})`);
5863
6041
  }
5864
6042
  let bracketCount = 0;
5865
6043
  let inQuotes = false;
5866
6044
  let quoteChar = "";
5867
- for (let i = 0; i < path28.length; i++) {
5868
- const char = path28[i];
6045
+ for (let i = 0; i < path34.length; i++) {
6046
+ const char = path34[i];
5869
6047
  if ((char === '"' || char === "'") && bracketCount > 0) {
5870
6048
  if (!inQuotes) {
5871
6049
  inQuotes = true;
@@ -5879,20 +6057,20 @@ var require_redact = __commonJS({
5879
6057
  } else if (char === "]" && !inQuotes) {
5880
6058
  bracketCount--;
5881
6059
  if (bracketCount < 0) {
5882
- throw new Error(`Invalid redaction path (${path28})`);
6060
+ throw new Error(`Invalid redaction path (${path34})`);
5883
6061
  }
5884
6062
  }
5885
6063
  }
5886
6064
  if (bracketCount !== 0) {
5887
- throw new Error(`Invalid redaction path (${path28})`);
6065
+ throw new Error(`Invalid redaction path (${path34})`);
5888
6066
  }
5889
6067
  }
5890
6068
  function validatePaths(paths) {
5891
6069
  if (!Array.isArray(paths)) {
5892
6070
  throw new TypeError("paths must be an array");
5893
6071
  }
5894
- for (const path28 of paths) {
5895
- validatePath(path28);
6072
+ for (const path34 of paths) {
6073
+ validatePath(path34);
5896
6074
  }
5897
6075
  }
5898
6076
  function slowRedact(options = {}) {
@@ -6060,8 +6238,8 @@ var require_redaction = __commonJS({
6060
6238
  if (shape[k2] === null) {
6061
6239
  o[k2] = (value) => topCensor(value, [k2]);
6062
6240
  } else {
6063
- const wrappedCensor = typeof censor === "function" ? (value, path28) => {
6064
- return censor(value, [k2, ...path28]);
6241
+ const wrappedCensor = typeof censor === "function" ? (value, path34) => {
6242
+ return censor(value, [k2, ...path34]);
6065
6243
  } : censor;
6066
6244
  o[k2] = Redact({
6067
6245
  paths: shape[k2],
@@ -6279,10 +6457,10 @@ var require_atomic_sleep = __commonJS({
6279
6457
  var require_sonic_boom = __commonJS({
6280
6458
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6281
6459
  "use strict";
6282
- var fs26 = require("fs");
6460
+ var fs31 = require("fs");
6283
6461
  var EventEmitter2 = require("events");
6284
6462
  var inherits = require("util").inherits;
6285
- var path28 = require("path");
6463
+ var path34 = require("path");
6286
6464
  var sleep = require_atomic_sleep();
6287
6465
  var assert = require("assert");
6288
6466
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6336,20 +6514,20 @@ var require_sonic_boom = __commonJS({
6336
6514
  const mode = sonic.mode;
6337
6515
  if (sonic.sync) {
6338
6516
  try {
6339
- if (sonic.mkdir) fs26.mkdirSync(path28.dirname(file), { recursive: true });
6340
- const fd = fs26.openSync(file, flags, mode);
6517
+ if (sonic.mkdir) fs31.mkdirSync(path34.dirname(file), { recursive: true });
6518
+ const fd = fs31.openSync(file, flags, mode);
6341
6519
  fileOpened(null, fd);
6342
6520
  } catch (err) {
6343
6521
  fileOpened(err);
6344
6522
  throw err;
6345
6523
  }
6346
6524
  } else if (sonic.mkdir) {
6347
- fs26.mkdir(path28.dirname(file), { recursive: true }, (err) => {
6525
+ fs31.mkdir(path34.dirname(file), { recursive: true }, (err) => {
6348
6526
  if (err) return fileOpened(err);
6349
- fs26.open(file, flags, mode, fileOpened);
6527
+ fs31.open(file, flags, mode, fileOpened);
6350
6528
  });
6351
6529
  } else {
6352
- fs26.open(file, flags, mode, fileOpened);
6530
+ fs31.open(file, flags, mode, fileOpened);
6353
6531
  }
6354
6532
  }
6355
6533
  function SonicBoom(opts) {
@@ -6390,8 +6568,8 @@ var require_sonic_boom = __commonJS({
6390
6568
  this.flush = flushBuffer;
6391
6569
  this.flushSync = flushBufferSync;
6392
6570
  this._actualWrite = actualWriteBuffer;
6393
- fsWriteSync = () => fs26.writeSync(this.fd, this._writingBuf);
6394
- fsWrite = () => fs26.write(this.fd, this._writingBuf, this.release);
6571
+ fsWriteSync = () => fs31.writeSync(this.fd, this._writingBuf);
6572
+ fsWrite = () => fs31.write(this.fd, this._writingBuf, this.release);
6395
6573
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6396
6574
  this._writingBuf = "";
6397
6575
  this.write = write;
@@ -6400,15 +6578,15 @@ var require_sonic_boom = __commonJS({
6400
6578
  this._actualWrite = actualWrite;
6401
6579
  fsWriteSync = () => {
6402
6580
  if (Buffer.isBuffer(this._writingBuf)) {
6403
- return fs26.writeSync(this.fd, this._writingBuf);
6581
+ return fs31.writeSync(this.fd, this._writingBuf);
6404
6582
  }
6405
- return fs26.writeSync(this.fd, this._writingBuf, "utf8");
6583
+ return fs31.writeSync(this.fd, this._writingBuf, "utf8");
6406
6584
  };
6407
6585
  fsWrite = () => {
6408
6586
  if (Buffer.isBuffer(this._writingBuf)) {
6409
- return fs26.write(this.fd, this._writingBuf, this.release);
6587
+ return fs31.write(this.fd, this._writingBuf, this.release);
6410
6588
  }
6411
- return fs26.write(this.fd, this._writingBuf, "utf8", this.release);
6589
+ return fs31.write(this.fd, this._writingBuf, "utf8", this.release);
6412
6590
  };
6413
6591
  } else {
6414
6592
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6465,7 +6643,7 @@ var require_sonic_boom = __commonJS({
6465
6643
  }
6466
6644
  }
6467
6645
  if (this._fsync) {
6468
- fs26.fsyncSync(this.fd);
6646
+ fs31.fsyncSync(this.fd);
6469
6647
  }
6470
6648
  const len = this._len;
6471
6649
  if (this._reopening) {
@@ -6579,7 +6757,7 @@ var require_sonic_boom = __commonJS({
6579
6757
  const onDrain = () => {
6580
6758
  if (!this._fsync) {
6581
6759
  try {
6582
- fs26.fsync(this.fd, (err) => {
6760
+ fs31.fsync(this.fd, (err) => {
6583
6761
  this._flushPending = false;
6584
6762
  cb(err);
6585
6763
  });
@@ -6681,7 +6859,7 @@ var require_sonic_boom = __commonJS({
6681
6859
  const fd = this.fd;
6682
6860
  this.once("ready", () => {
6683
6861
  if (fd !== this.fd) {
6684
- fs26.close(fd, (err) => {
6862
+ fs31.close(fd, (err) => {
6685
6863
  if (err) {
6686
6864
  return this.emit("error", err);
6687
6865
  }
@@ -6730,7 +6908,7 @@ var require_sonic_boom = __commonJS({
6730
6908
  buf = this._bufs[0];
6731
6909
  }
6732
6910
  try {
6733
- const n = Buffer.isBuffer(buf) ? fs26.writeSync(this.fd, buf) : fs26.writeSync(this.fd, buf, "utf8");
6911
+ const n = Buffer.isBuffer(buf) ? fs31.writeSync(this.fd, buf) : fs31.writeSync(this.fd, buf, "utf8");
6734
6912
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6735
6913
  buf = releasedBufObj.writingBuf;
6736
6914
  this._len = releasedBufObj.len;
@@ -6746,7 +6924,7 @@ var require_sonic_boom = __commonJS({
6746
6924
  }
6747
6925
  }
6748
6926
  try {
6749
- fs26.fsyncSync(this.fd);
6927
+ fs31.fsyncSync(this.fd);
6750
6928
  } catch {
6751
6929
  }
6752
6930
  }
@@ -6767,7 +6945,7 @@ var require_sonic_boom = __commonJS({
6767
6945
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6768
6946
  }
6769
6947
  try {
6770
- const n = fs26.writeSync(this.fd, buf);
6948
+ const n = fs31.writeSync(this.fd, buf);
6771
6949
  buf = buf.subarray(n);
6772
6950
  this._len = Math.max(this._len - n, 0);
6773
6951
  if (buf.length <= 0) {
@@ -6795,13 +6973,13 @@ var require_sonic_boom = __commonJS({
6795
6973
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6796
6974
  if (this.sync) {
6797
6975
  try {
6798
- const written = Buffer.isBuffer(this._writingBuf) ? fs26.writeSync(this.fd, this._writingBuf) : fs26.writeSync(this.fd, this._writingBuf, "utf8");
6976
+ const written = Buffer.isBuffer(this._writingBuf) ? fs31.writeSync(this.fd, this._writingBuf) : fs31.writeSync(this.fd, this._writingBuf, "utf8");
6799
6977
  release(null, written);
6800
6978
  } catch (err) {
6801
6979
  release(err);
6802
6980
  }
6803
6981
  } else {
6804
- fs26.write(this.fd, this._writingBuf, release);
6982
+ fs31.write(this.fd, this._writingBuf, release);
6805
6983
  }
6806
6984
  }
6807
6985
  function actualWriteBuffer() {
@@ -6810,7 +6988,7 @@ var require_sonic_boom = __commonJS({
6810
6988
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6811
6989
  if (this.sync) {
6812
6990
  try {
6813
- const written = fs26.writeSync(this.fd, this._writingBuf);
6991
+ const written = fs31.writeSync(this.fd, this._writingBuf);
6814
6992
  release(null, written);
6815
6993
  } catch (err) {
6816
6994
  release(err);
@@ -6819,7 +6997,7 @@ var require_sonic_boom = __commonJS({
6819
6997
  if (kCopyBuffer) {
6820
6998
  this._writingBuf = Buffer.from(this._writingBuf);
6821
6999
  }
6822
- fs26.write(this.fd, this._writingBuf, release);
7000
+ fs31.write(this.fd, this._writingBuf, release);
6823
7001
  }
6824
7002
  }
6825
7003
  function actualClose(sonic) {
@@ -6835,12 +7013,12 @@ var require_sonic_boom = __commonJS({
6835
7013
  sonic._lens = [];
6836
7014
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6837
7015
  try {
6838
- fs26.fsync(sonic.fd, closeWrapped);
7016
+ fs31.fsync(sonic.fd, closeWrapped);
6839
7017
  } catch {
6840
7018
  }
6841
7019
  function closeWrapped() {
6842
7020
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6843
- fs26.close(sonic.fd, done);
7021
+ fs31.close(sonic.fd, done);
6844
7022
  } else {
6845
7023
  done();
6846
7024
  }
@@ -9975,11 +10153,11 @@ var init_lib = __esm({
9975
10153
  }
9976
10154
  }
9977
10155
  },
9978
- addToPath: function addToPath(path28, added, removed, oldPosInc, options) {
9979
- var last = path28.lastComponent;
10156
+ addToPath: function addToPath(path34, added, removed, oldPosInc, options) {
10157
+ var last = path34.lastComponent;
9980
10158
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
9981
10159
  return {
9982
- oldPos: path28.oldPos + oldPosInc,
10160
+ oldPos: path34.oldPos + oldPosInc,
9983
10161
  lastComponent: {
9984
10162
  count: last.count + 1,
9985
10163
  added,
@@ -9989,7 +10167,7 @@ var init_lib = __esm({
9989
10167
  };
9990
10168
  } else {
9991
10169
  return {
9992
- oldPos: path28.oldPos + oldPosInc,
10170
+ oldPos: path34.oldPos + oldPosInc,
9993
10171
  lastComponent: {
9994
10172
  count: 1,
9995
10173
  added,
@@ -10420,10 +10598,10 @@ function attachmentToHistoryMessage(o, ts) {
10420
10598
  const memories = raw.map((m2) => {
10421
10599
  if (!m2 || typeof m2 !== "object") return null;
10422
10600
  const rec = m2;
10423
- const path28 = typeof rec.path === "string" ? rec.path : null;
10601
+ const path34 = typeof rec.path === "string" ? rec.path : null;
10424
10602
  const content = typeof rec.content === "string" ? rec.content : null;
10425
- if (!path28 || content == null) return null;
10426
- const entry = { path: path28, content };
10603
+ if (!path34 || content == null) return null;
10604
+ const entry = { path: path34, content };
10427
10605
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10428
10606
  return entry;
10429
10607
  }).filter((m2) => m2 !== null);
@@ -11227,10 +11405,10 @@ function parseAttachment(obj) {
11227
11405
  const memories = raw.map((m2) => {
11228
11406
  if (!m2 || typeof m2 !== "object") return null;
11229
11407
  const rec = m2;
11230
- const path28 = typeof rec.path === "string" ? rec.path : null;
11408
+ const path34 = typeof rec.path === "string" ? rec.path : null;
11231
11409
  const content = typeof rec.content === "string" ? rec.content : null;
11232
- if (!path28 || content == null) return null;
11233
- const out = { path: path28, content };
11410
+ if (!path34 || content == null) return null;
11411
+ const out = { path: path34, content };
11234
11412
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11235
11413
  return out;
11236
11414
  }).filter((m2) => m2 !== null);
@@ -18725,7 +18903,7 @@ var require_websocket = __commonJS({
18725
18903
  "use strict";
18726
18904
  var EventEmitter2 = require("events");
18727
18905
  var https = require("https");
18728
- var http = require("http");
18906
+ var http2 = require("http");
18729
18907
  var net = require("net");
18730
18908
  var tls = require("tls");
18731
18909
  var { randomBytes, createHash } = require("crypto");
@@ -19259,7 +19437,7 @@ var require_websocket = __commonJS({
19259
19437
  }
19260
19438
  const defaultPort = isSecure ? 443 : 80;
19261
19439
  const key = randomBytes(16).toString("base64");
19262
- const request = isSecure ? https.request : http.request;
19440
+ const request = isSecure ? https.request : http2.request;
19263
19441
  const protocolSet = /* @__PURE__ */ new Set();
19264
19442
  let perMessageDeflate;
19265
19443
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -19753,7 +19931,7 @@ var require_websocket_server = __commonJS({
19753
19931
  "../node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
19754
19932
  "use strict";
19755
19933
  var EventEmitter2 = require("events");
19756
- var http = require("http");
19934
+ var http2 = require("http");
19757
19935
  var { Duplex } = require("stream");
19758
19936
  var { createHash } = require("crypto");
19759
19937
  var extension2 = require_extension();
@@ -19828,8 +20006,8 @@ var require_websocket_server = __commonJS({
19828
20006
  );
19829
20007
  }
19830
20008
  if (options.port != null) {
19831
- this._server = http.createServer((req, res) => {
19832
- const body = http.STATUS_CODES[426];
20009
+ this._server = http2.createServer((req, res) => {
20010
+ const body = http2.STATUS_CODES[426];
19833
20011
  res.writeHead(426, {
19834
20012
  "Content-Length": body.length,
19835
20013
  "Content-Type": "text/plain"
@@ -20116,7 +20294,7 @@ var require_websocket_server = __commonJS({
20116
20294
  this.destroy();
20117
20295
  }
20118
20296
  function abortHandshake(socket, code, message, headers) {
20119
- message = message || http.STATUS_CODES[code];
20297
+ message = message || http2.STATUS_CODES[code];
20120
20298
  headers = {
20121
20299
  Connection: "close",
20122
20300
  "Content-Type": "text/html",
@@ -20125,7 +20303,7 @@ var require_websocket_server = __commonJS({
20125
20303
  };
20126
20304
  socket.once("finish", socket.destroy);
20127
20305
  socket.end(
20128
- `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r
20306
+ `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
20129
20307
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
20130
20308
  );
20131
20309
  }
@@ -20144,7 +20322,7 @@ var require_websocket_server = __commonJS({
20144
20322
  // src/run-case/recorder.ts
20145
20323
  function startRunCaseRecorder(opts) {
20146
20324
  const now = opts.now ?? Date.now;
20147
- const dir = import_node_path24.default.dirname(opts.recordPath);
20325
+ const dir = import_node_path30.default.dirname(opts.recordPath);
20148
20326
  let stream = null;
20149
20327
  let closing = false;
20150
20328
  let closedSettled = false;
@@ -20158,8 +20336,8 @@ function startRunCaseRecorder(opts) {
20158
20336
  });
20159
20337
  const ensureStream = () => {
20160
20338
  if (stream) return stream;
20161
- import_node_fs23.default.mkdirSync(dir, { recursive: true });
20162
- stream = import_node_fs23.default.createWriteStream(opts.recordPath, { flags: "a" });
20339
+ import_node_fs28.default.mkdirSync(dir, { recursive: true });
20340
+ stream = import_node_fs28.default.createWriteStream(opts.recordPath, { flags: "a" });
20163
20341
  stream.on("close", () => closedResolve());
20164
20342
  return stream;
20165
20343
  };
@@ -20184,12 +20362,12 @@ function startRunCaseRecorder(opts) {
20184
20362
  };
20185
20363
  return { tap, close, closed };
20186
20364
  }
20187
- var import_node_fs23, import_node_path24;
20365
+ var import_node_fs28, import_node_path30;
20188
20366
  var init_recorder = __esm({
20189
20367
  "src/run-case/recorder.ts"() {
20190
20368
  "use strict";
20191
- import_node_fs23 = __toESM(require("fs"), 1);
20192
- import_node_path24 = __toESM(require("path"), 1);
20369
+ import_node_fs28 = __toESM(require("fs"), 1);
20370
+ import_node_path30 = __toESM(require("path"), 1);
20193
20371
  }
20194
20372
  });
20195
20373
 
@@ -20232,7 +20410,7 @@ var init_wire = __esm({
20232
20410
  // src/run-case/controller.ts
20233
20411
  async function runController(opts) {
20234
20412
  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-"));
20413
+ const cwd = opts.cwd ?? (0, import_node_fs29.mkdtempSync)(import_node_path31.default.join(import_node_os15.default.tmpdir(), "clawd-runcase-"));
20236
20414
  const ownsCwd = opts.cwd === void 0;
20237
20415
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20238
20416
  const spawnCtx = { cwd };
@@ -20393,19 +20571,19 @@ async function runController(opts) {
20393
20571
  if (sigintHandler) process.off("SIGINT", sigintHandler);
20394
20572
  if (ownsCwd) {
20395
20573
  try {
20396
- (0, import_node_fs24.rmSync)(cwd, { recursive: true, force: true });
20574
+ (0, import_node_fs29.rmSync)(cwd, { recursive: true, force: true });
20397
20575
  } catch {
20398
20576
  }
20399
20577
  }
20400
20578
  return exitCode ?? 0;
20401
20579
  }
20402
- var import_node_fs24, import_node_os15, import_node_path25;
20580
+ var import_node_fs29, import_node_os15, import_node_path31;
20403
20581
  var init_controller = __esm({
20404
20582
  "src/run-case/controller.ts"() {
20405
20583
  "use strict";
20406
- import_node_fs24 = require("fs");
20584
+ import_node_fs29 = require("fs");
20407
20585
  import_node_os15 = __toESM(require("os"), 1);
20408
- import_node_path25 = __toESM(require("path"), 1);
20586
+ import_node_path31 = __toESM(require("path"), 1);
20409
20587
  init_claude();
20410
20588
  init_stdout_splitter();
20411
20589
  init_permission_stdio();
@@ -20637,8 +20815,8 @@ Env (advanced):
20637
20815
  `;
20638
20816
 
20639
20817
  // src/index.ts
20640
- var import_node_path23 = __toESM(require("path"), 1);
20641
- var import_node_fs22 = __toESM(require("fs"), 1);
20818
+ var import_node_path29 = __toESM(require("path"), 1);
20819
+ var import_node_fs27 = __toESM(require("fs"), 1);
20642
20820
 
20643
20821
  // src/logger.ts
20644
20822
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -21630,6 +21808,11 @@ var SessionRunner = class {
21630
21808
  // sub-session idle-kill timer ref;key = sessionId(实际同一 runner 只对应一个 session,
21631
21809
  // 但 reducer Effect schema 带 sessionId 作为唯一键,对齐 schedule/cancel 配对)
21632
21810
  idleKillTimers = /* @__PURE__ */ new Map();
21811
+ // file-sharing 待配对的 file-edit tool_use(spec §6 PR 3):在同一 session 流里
21812
+ // tool_use(kind='tool_call')与 tool_result 通过 toolUseId 配对。tool_use 提前到,
21813
+ // 等 tool_result 来时反查 path 调 onFileEdit。条目 in-flight 期间很少(个位数),
21814
+ // 不需要 LRU;session 退出时整个 runner 销毁自然清理。
21815
+ pendingFileEdits = /* @__PURE__ */ new Map();
21633
21816
  getState() {
21634
21817
  return this.state;
21635
21818
  }
@@ -21638,7 +21821,13 @@ var SessionRunner = class {
21638
21821
  const personaStore = this.hooks.personaStore;
21639
21822
  const adapter = this.hooks.adapter;
21640
21823
  const deps = {
21641
- parseLine: (l) => adapter.parseLine(l),
21824
+ // file-sharing (spec §6 PR 3):在 stdout-line 解析时同步观察 file-edit 工具事件,
21825
+ // 配对 tool_use ↔ tool_result 调 onFileEdit。原 reducer 路径不变。
21826
+ parseLine: (l) => {
21827
+ const events = adapter.parseLine(l);
21828
+ if (this.hooks.onFileEdit) this.observeForFileEdit(events);
21829
+ return events;
21830
+ },
21642
21831
  // persona mention injection 在 deps 装配处包一层,对 reducer 完全透明。
21643
21832
  // 命中 mention 时拆成两条 stdin 帧(先 system-reminder 块、后 user 原文):
21644
21833
  // - CC stream-json 按行处理,落盘是两条独立 user message;
@@ -21706,8 +21895,54 @@ var SessionRunner = class {
21706
21895
  // reducer 的 batch dedup 按 events[0].uuid 整组处理,避免跨路径重复。
21707
21896
  feedObserverEvents(events) {
21708
21897
  if (events.length === 0) return;
21898
+ if (this.hooks.onFileEdit) this.observeForFileEdit(events);
21709
21899
  this.input({ kind: "inject-events", events });
21710
21900
  }
21901
+ /**
21902
+ * file-sharing tool_use ↔ tool_result 配对(spec §6 PR 3)。
21903
+ *
21904
+ * 1) tool_call kind + tool ∈ FILE_EDIT_TOOLS → cache toolUseId → { tool, relPath }
21905
+ * relPath 从 input 里取(Write/Edit/MultiEdit 是 file_path,NotebookEdit 是 notebook_path)。
21906
+ * 2) tool_result kind + cache 命中 + 非 error → 调 onFileEdit + 删 cache
21907
+ * 3) tool_result kind + cache 命中 + 是 error → 删 cache,不调(spec 只要"成功"才入清单)
21908
+ * 4) tool_call 但 input 没有 path → 不 cache(防御)
21909
+ *
21910
+ * 不破坏 reducer 路径——纯只读扫描 events 数组。
21911
+ */
21912
+ observeForFileEdit(events) {
21913
+ const cb = this.hooks.onFileEdit;
21914
+ if (!cb) return;
21915
+ for (const ev of events) {
21916
+ if (ev.kind === "tool_call") {
21917
+ const tool = ev.tool;
21918
+ if (!isFileEditTool(tool)) continue;
21919
+ const relPath = extractEditPath(ev.input);
21920
+ if (!relPath) continue;
21921
+ this.pendingFileEdits.set(ev.toolUseId, {
21922
+ tool,
21923
+ relPath
21924
+ });
21925
+ } else if (ev.kind === "tool_result") {
21926
+ const pending = this.pendingFileEdits.get(ev.toolUseId);
21927
+ if (!pending) continue;
21928
+ this.pendingFileEdits.delete(ev.toolUseId);
21929
+ if (ev.error != null) continue;
21930
+ try {
21931
+ cb({
21932
+ toolUseId: ev.toolUseId,
21933
+ tool: pending.tool,
21934
+ relPath: pending.relPath,
21935
+ cwd: this.state.file.cwd
21936
+ });
21937
+ } catch (err) {
21938
+ this.hooks.logger?.warn("onFileEdit hook threw", {
21939
+ err: err.message,
21940
+ sessionId: this.state.file.sessionId
21941
+ });
21942
+ }
21943
+ }
21944
+ }
21945
+ }
21711
21946
  // 向子进程 stdin 写一条 CC IPC `control_request` 并返回 Promise<response>
21712
21947
  // —— CC 侧会异步回写匹配 request_id 的 `control_response` 帧,
21713
21948
  // 在 stdout 行处理入口被 tryHandleControlResponse 拦截并 resolve pending
@@ -21897,6 +22132,15 @@ var SessionRunner = class {
21897
22132
  }
21898
22133
  }
21899
22134
  };
22135
+ function isFileEditTool(name) {
22136
+ return name === "Write" || name === "Edit" || name === "MultiEdit" || name === "NotebookEdit";
22137
+ }
22138
+ function extractEditPath(input) {
22139
+ if (!input || typeof input !== "object") return null;
22140
+ const o = input;
22141
+ 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;
22142
+ return candidate || null;
22143
+ }
21900
22144
 
21901
22145
  // src/session/manager.ts
21902
22146
  function compressFrameForWire(frame) {
@@ -21953,16 +22197,6 @@ function nowIso2(deps) {
21953
22197
  function newSessionId() {
21954
22198
  return v4_default().replace(/-/g, "").slice(0, 16);
21955
22199
  }
21956
- function derivePersonaSpawnCwd(file, personaRoot) {
21957
- const personaId = file.ownerPersonaId;
21958
- if (!personaId) return file.cwd;
21959
- if (!personaRoot) {
21960
- throw new Error(
21961
- `derivePersonaSpawnCwd: personaRoot missing for owner session ${file.sessionId} (ownerPersonaId=${personaId})`
21962
- );
21963
- }
21964
- return import_node_path6.default.join(personaRoot, safeFileName(personaId));
21965
- }
21966
22200
  function makeInitialState(file, subSessionMeta) {
21967
22201
  return {
21968
22202
  file,
@@ -22121,24 +22355,7 @@ var SessionManager = class {
22121
22355
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
22122
22356
  const store = this.storeFor(scope);
22123
22357
  const subSessionMeta = metaFromScope(scope, this.deps.personaRoot ?? "");
22124
- const correctedCwd = derivePersonaSpawnCwd(file, this.deps.personaRoot);
22125
- if (correctedCwd !== file.cwd) {
22126
- this.deps.logger?.warn("owner session cwd drifted from persona dir; auto-correcting", {
22127
- sessionId: file.sessionId,
22128
- ownerPersonaId: file.ownerPersonaId,
22129
- stale: file.cwd,
22130
- corrected: correctedCwd
22131
- });
22132
- file = { ...file, cwd: correctedCwd, updatedAt: nowIso2(this.deps) };
22133
- try {
22134
- store.write(file);
22135
- } catch (err) {
22136
- this.deps.logger?.warn("failed to persist auto-corrected owner cwd", {
22137
- sessionId: file.sessionId,
22138
- error: err.message
22139
- });
22140
- }
22141
- }
22358
+ const attachmentGroup = this.deps.attachmentGroup;
22142
22359
  const runner = new SessionRunner(makeInitialState(file, subSessionMeta), {
22143
22360
  broadcastFrame: (frame, target) => this.routeFromRunner(frame, target),
22144
22361
  store,
@@ -22151,7 +22368,15 @@ var SessionManager = class {
22151
22368
  resolveContextWindow: (tool, modelId) => this.deps.getAdapter(tool).resolveContextWindow(modelId),
22152
22369
  dataDir: this.deps.dataDir,
22153
22370
  personaStore: this.deps.personaStore,
22154
- ownerDisplayName: this.deps.ownerDisplayName
22371
+ ownerDisplayName: this.deps.ownerDisplayName,
22372
+ // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22373
+ onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22374
+ scope,
22375
+ sessionId: file.sessionId,
22376
+ tool: input.tool,
22377
+ relPath: input.relPath,
22378
+ cwd: input.cwd
22379
+ }) : void 0
22155
22380
  });
22156
22381
  if (this.deps.mode === "tui" && !file.toolSessionId) {
22157
22382
  const newTsid = v4_default();
@@ -22239,22 +22464,14 @@ var SessionManager = class {
22239
22464
  }
22240
22465
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
22241
22466
  create(args) {
22242
- let cwd;
22243
- if (args.ownerPersonaId) {
22244
- cwd = derivePersonaSpawnCwd(
22245
- {
22246
- sessionId: "",
22247
- cwd: args.cwd ?? "",
22248
- tool: "claude",
22249
- ownerPersonaId: args.ownerPersonaId,
22250
- createdAt: "",
22251
- updatedAt: ""
22252
- },
22253
- this.deps.personaRoot
22254
- );
22255
- } else if (args.cwd) {
22256
- cwd = args.cwd;
22257
- } else {
22467
+ let cwd = args.cwd;
22468
+ if (args.ownerPersonaId && !cwd) {
22469
+ if (!this.deps.personaRoot) {
22470
+ throw new Error("personaRoot required to derive cwd from ownerPersonaId");
22471
+ }
22472
+ cwd = import_node_path6.default.join(this.deps.personaRoot, safeFileName(args.ownerPersonaId));
22473
+ }
22474
+ if (!cwd) {
22258
22475
  throw new ClawdError(ERROR_CODES.INVALID_CWD, "cwd required when ownerPersonaId is absent");
22259
22476
  }
22260
22477
  try {
@@ -23310,6 +23527,21 @@ var PersonaRegistry = class {
23310
23527
  if (entry.revoked) return { ok: false, code: "TOKEN_REVOKED" };
23311
23528
  return { ok: true, label: entry.label };
23312
23529
  }
23530
+ /**
23531
+ * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23532
+ * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23533
+ * persona 数量通常 < 100,性能可忽略。
23534
+ */
23535
+ findByToken(token) {
23536
+ if (!token) return null;
23537
+ for (const persona of this.cache.values()) {
23538
+ if (!persona.public) continue;
23539
+ const entry = persona.tokenMap[token];
23540
+ if (!entry || entry.revoked) continue;
23541
+ return { personaId: persona.personaId, label: entry.label };
23542
+ }
23543
+ return null;
23544
+ }
23313
23545
  };
23314
23546
 
23315
23547
  // src/persona/manager.ts
@@ -25251,6 +25483,7 @@ var import_websocket = __toESM(require_websocket(), 1);
25251
25483
  var import_websocket_server = __toESM(require_websocket_server(), 1);
25252
25484
 
25253
25485
  // src/transport/local-ws-server.ts
25486
+ var import_node_http = __toESM(require("http"), 1);
25254
25487
  var PERSONA_PATH_RE = /^\/personas\/([a-zA-Z0-9._-]+)$/;
25255
25488
  var LocalWsServer = class {
25256
25489
  constructor(opts) {
@@ -25260,6 +25493,7 @@ var LocalWsServer = class {
25260
25493
  }
25261
25494
  opts;
25262
25495
  wss = null;
25496
+ httpServer = null;
25263
25497
  frameHandler = null;
25264
25498
  clients = /* @__PURE__ */ new Map();
25265
25499
  logger;
@@ -25267,25 +25501,28 @@ var LocalWsServer = class {
25267
25501
  async start() {
25268
25502
  const host = this.opts.host ?? "127.0.0.1";
25269
25503
  await new Promise((resolve2, reject) => {
25270
- const wss = new import_websocket_server.default({
25271
- host,
25272
- port: this.opts.port,
25273
- clientTracking: true
25504
+ const httpServer = import_node_http.default.createServer((req, res) => this.handleHttpRequest(req, res));
25505
+ const wss = new import_websocket_server.default({ noServer: true, clientTracking: true });
25506
+ httpServer.on("upgrade", (req, socket, head) => {
25507
+ wss.handleUpgrade(req, socket, head, (ws) => {
25508
+ this.routeConnection(ws, req);
25509
+ });
25274
25510
  });
25275
- wss.on("listening", () => {
25511
+ httpServer.on("listening", () => {
25276
25512
  this.logger?.info("ws listening", { host, port: this.opts.port });
25277
25513
  resolve2();
25278
25514
  });
25279
- wss.on("error", (err) => {
25515
+ httpServer.on("error", (err) => {
25280
25516
  this.logger?.error("ws server error", { err: err.message });
25281
25517
  reject(err);
25282
25518
  });
25283
- wss.on("connection", (socket, req) => this.routeConnection(socket, req));
25519
+ httpServer.listen(this.opts.port, host);
25520
+ this.httpServer = httpServer;
25284
25521
  this.wss = wss;
25285
25522
  });
25286
25523
  }
25287
25524
  async stop() {
25288
- if (!this.wss) return;
25525
+ if (!this.httpServer) return;
25289
25526
  for (const c of this.clients.values()) {
25290
25527
  try {
25291
25528
  c.ws.close(1001, "shutdown");
@@ -25297,7 +25534,32 @@ var LocalWsServer = class {
25297
25534
  await new Promise((resolve2) => {
25298
25535
  this.wss?.close(() => resolve2());
25299
25536
  });
25537
+ await new Promise((resolve2) => {
25538
+ this.httpServer?.close(() => resolve2());
25539
+ });
25300
25540
  this.wss = null;
25541
+ this.httpServer = null;
25542
+ }
25543
+ /** http.createServer 'request' 入口:file-sharing 路由优先,未命中走 404 */
25544
+ async handleHttpRequest(req, res) {
25545
+ const handler = this.opts.httpRequestHandler;
25546
+ if (handler) {
25547
+ try {
25548
+ const handled = await handler(req, res);
25549
+ if (handled) return;
25550
+ } catch (err) {
25551
+ this.logger?.warn("http handler threw", { err: err.message });
25552
+ if (!res.headersSent) {
25553
+ res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
25554
+ res.end(JSON.stringify({ code: "INTERNAL", message: "http handler error" }));
25555
+ }
25556
+ return;
25557
+ }
25558
+ }
25559
+ if (!res.headersSent) {
25560
+ res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
25561
+ res.end(JSON.stringify({ code: "NOT_FOUND", message: "no route" }));
25562
+ }
25301
25563
  }
25302
25564
  onFrame(handler) {
25303
25565
  this.frameHandler = handler;
@@ -25399,7 +25661,7 @@ var LocalWsServer = class {
25399
25661
  }
25400
25662
  try {
25401
25663
  if (authed) {
25402
- const readyFrame = this.opts.readyFrameBuilder();
25664
+ const readyFrame = this.opts.readyFrameBuilder({ remoteAddress });
25403
25665
  this.safeSend(socket, { type: "ready", ...readyFrame });
25404
25666
  } else {
25405
25667
  this.safeSend(socket, { type: "ready", protocolVersion: this.opts.protocolVersion });
@@ -25436,7 +25698,7 @@ var LocalWsServer = class {
25436
25698
  if (verdict !== "pass") {
25437
25699
  if (!wasAuthed && authGate.isAuthed(client.id)) {
25438
25700
  try {
25439
- const full = this.opts.readyFrameBuilder();
25701
+ const full = this.opts.readyFrameBuilder({ remoteAddress });
25440
25702
  this.safeSend(this.clients.get(client.id).ws, { type: "ready", ...full });
25441
25703
  } catch (err) {
25442
25704
  this.logger?.warn("post-auth ready frame build failed", { err: err.message });
@@ -26033,128 +26295,1049 @@ function isLocalhost(addr) {
26033
26295
  return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
26034
26296
  }
26035
26297
 
26036
- // src/discovery/state-file.ts
26037
- var import_node_fs13 = __toESM(require("fs"), 1);
26038
- var import_node_path14 = __toESM(require("path"), 1);
26039
- function defaultStateFilePath(dataDir) {
26040
- return import_node_path14.default.join(dataDir, "state.json");
26041
- }
26042
- function isPidAlive(pid) {
26043
- if (!Number.isFinite(pid) || pid <= 0) return false;
26044
- try {
26045
- process.kill(pid, 0);
26046
- return true;
26047
- } catch (err) {
26048
- const code = err.code;
26049
- return code === "EPERM";
26298
+ // src/transport/auth-context.ts
26299
+ var AuthContextResolver = class {
26300
+ constructor(opts) {
26301
+ this.opts = opts;
26302
+ }
26303
+ opts;
26304
+ /**
26305
+ * `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26306
+ * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
26307
+ */
26308
+ resolveFromHeader(authHeader, remoteAddress) {
26309
+ const token = parseBearer(authHeader);
26310
+ if (!token) return null;
26311
+ const isLoopback = isLoopbackAddr(remoteAddress);
26312
+ if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
26313
+ return { role: "owner", isLoopback };
26314
+ }
26315
+ const personalHit = this.opts.personaRegistry.findByToken(token);
26316
+ if (personalHit) {
26317
+ return {
26318
+ role: "personal",
26319
+ personaId: personalHit.personaId,
26320
+ label: personalHit.label,
26321
+ isLoopback
26322
+ };
26323
+ }
26324
+ return null;
26050
26325
  }
26326
+ };
26327
+ function parseBearer(header) {
26328
+ if (!header) return null;
26329
+ const raw = Array.isArray(header) ? header[0] : header;
26330
+ if (typeof raw !== "string") return null;
26331
+ const m2 = /^Bearer\s+(\S+)$/i.exec(raw.trim());
26332
+ return m2 ? m2[1] : null;
26051
26333
  }
26052
- var StateFileManager = class {
26053
- file;
26334
+ function isLoopbackAddr(addr) {
26335
+ if (!addr) return false;
26336
+ return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1";
26337
+ }
26338
+ function constantTimeEqual2(a, b2) {
26339
+ if (a.length !== b2.length) return false;
26340
+ let diff2 = 0;
26341
+ for (let i = 0; i < a.length; i++) diff2 |= a.charCodeAt(i) ^ b2.charCodeAt(i);
26342
+ return diff2 === 0;
26343
+ }
26344
+
26345
+ // src/transport/http-router.ts
26346
+ var import_node_fs15 = __toESM(require("fs"), 1);
26347
+ var import_node_path17 = __toESM(require("path"), 1);
26348
+
26349
+ // src/attachment/group.ts
26350
+ var import_node_fs13 = __toESM(require("fs"), 1);
26351
+ var import_node_path14 = __toESM(require("path"), 1);
26352
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
26353
+ init_protocol();
26354
+ var GroupFileStore = class {
26355
+ dataDir;
26356
+ logger;
26357
+ cache = /* @__PURE__ */ new Map();
26054
26358
  constructor(opts) {
26055
- this.file = defaultStateFilePath(opts.dataDir);
26359
+ this.dataDir = opts.dataDir;
26360
+ this.logger = opts.logger;
26056
26361
  }
26057
- filePath() {
26058
- return this.file;
26362
+ rootForScope(scope) {
26363
+ return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26059
26364
  }
26060
- read() {
26365
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
26366
+ filePath(scope, sessionId) {
26367
+ return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26368
+ }
26369
+ cacheKey(scope, sessionId) {
26370
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26371
+ }
26372
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
26373
+ readFile(scope, sessionId) {
26374
+ const file = this.filePath(scope, sessionId);
26061
26375
  try {
26062
- const raw = import_node_fs13.default.readFileSync(this.file, "utf8");
26376
+ const raw = import_node_fs13.default.readFileSync(file, "utf8");
26063
26377
  const parsed = JSON.parse(raw);
26064
- return parsed;
26065
- } catch {
26066
- return null;
26378
+ if (!Array.isArray(parsed)) {
26379
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
26380
+ file
26381
+ });
26382
+ return [];
26383
+ }
26384
+ const out = [];
26385
+ for (const entry of parsed) {
26386
+ const r = GroupFileEntrySchema.safeParse(entry);
26387
+ if (r.success) out.push(r.data);
26388
+ }
26389
+ return out;
26390
+ } catch (err) {
26391
+ const code = err?.code;
26392
+ if (code === "ENOENT") return [];
26393
+ this.logger?.warn("GroupFileStore.readFile failed", {
26394
+ file,
26395
+ err: err.message
26396
+ });
26397
+ return [];
26067
26398
  }
26068
26399
  }
26069
- preflight() {
26070
- const existing = this.read();
26071
- if (!existing) return { status: "ok" };
26072
- if (isPidAlive(existing.pid)) return { status: "active", existing };
26073
- return { status: "stale", existing };
26400
+ writeFile(scope, sessionId, entries) {
26401
+ const file = this.filePath(scope, sessionId);
26402
+ import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
26403
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
26404
+ import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
26405
+ import_node_fs13.default.renameSync(tmp, file);
26074
26406
  }
26075
- write(state) {
26076
- import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(this.file), { recursive: true });
26077
- const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
26078
- import_node_fs13.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
26079
- import_node_fs13.default.renameSync(tmp, this.file);
26080
- if (process.platform !== "win32") {
26081
- try {
26082
- import_node_fs13.default.chmodSync(this.file, 384);
26083
- } catch {
26084
- }
26085
- }
26407
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
26408
+ list(scope, sessionId) {
26409
+ const key = this.cacheKey(scope, sessionId);
26410
+ const cached = this.cache.get(key);
26411
+ if (cached) return cached.entries;
26412
+ const entries = this.readFile(scope, sessionId);
26413
+ this.cache.set(key, { entries, scope });
26414
+ return entries;
26086
26415
  }
26087
- delete() {
26088
- try {
26089
- import_node_fs13.default.unlinkSync(this.file);
26090
- } catch {
26416
+ /**
26417
+ * upsert:
26418
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
26419
+ * (保留首次入群人 / 入群时刻)
26420
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
26421
+ *
26422
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
26423
+ */
26424
+ upsert(scope, sessionId, input, now = Date.now()) {
26425
+ const entries = this.list(scope, sessionId).slice();
26426
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
26427
+ let next;
26428
+ if (idx >= 0) {
26429
+ const prev = entries[idx];
26430
+ next = {
26431
+ ...prev,
26432
+ size: input.size,
26433
+ mime: input.mime,
26434
+ lastEditedAt: now,
26435
+ stale: false
26436
+ // addedBy / label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
26437
+ };
26438
+ entries[idx] = next;
26439
+ } else {
26440
+ next = {
26441
+ id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
26442
+ relPath: input.relPath,
26443
+ from: input.from,
26444
+ addedBy: input.addedBy,
26445
+ label: input.label,
26446
+ size: input.size,
26447
+ mime: input.mime,
26448
+ addedAt: now
26449
+ // agent / inbox 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
26450
+ };
26451
+ entries.push(next);
26091
26452
  }
26453
+ this.writeFile(scope, sessionId, entries);
26454
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26455
+ return next;
26092
26456
  }
26093
- };
26094
-
26095
- // src/tunnel/tunnel-manager.ts
26096
- var import_node_fs17 = __toESM(require("fs"), 1);
26097
- var import_node_path18 = __toESM(require("path"), 1);
26098
- var import_node_crypto4 = __toESM(require("crypto"), 1);
26099
- var import_node_child_process5 = require("child_process");
26100
-
26101
- // src/tunnel/tunnel-store.ts
26102
- var import_node_fs14 = __toESM(require("fs"), 1);
26103
- var import_node_path15 = __toESM(require("path"), 1);
26104
- var TunnelStore = class {
26105
- constructor(filePath) {
26106
- this.filePath = filePath;
26457
+ /**
26458
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
26459
+ * - 命中 → stale=true,UI 灰显
26460
+ * - 未命中 noop(不需要为不在群里的文件创建 stale 条目)
26461
+ *
26462
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26463
+ */
26464
+ markStale(scope, sessionId, relPath) {
26465
+ const entries = this.list(scope, sessionId).slice();
26466
+ const idx = entries.findIndex((e) => e.relPath === relPath);
26467
+ if (idx < 0) return;
26468
+ if (entries[idx].stale) return;
26469
+ entries[idx] = { ...entries[idx], stale: true };
26470
+ this.writeFile(scope, sessionId, entries);
26471
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26107
26472
  }
26108
- filePath;
26109
- async get() {
26110
- try {
26111
- const raw = await import_node_fs14.default.promises.readFile(this.filePath, "utf8");
26112
- const obj = JSON.parse(raw);
26113
- if (!isPersistedTunnel(obj)) return null;
26114
- return obj;
26115
- } catch (err) {
26116
- const code = err?.code;
26117
- if (code === "ENOENT") return null;
26118
- return null;
26119
- }
26473
+ /**
26474
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
26475
+ * agent / inbox 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
26476
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
26477
+ *
26478
+ * 返回值:true=命中并删除;false=relPath 不在群里。
26479
+ */
26480
+ remove(scope, sessionId, relPath) {
26481
+ const entries = this.list(scope, sessionId).slice();
26482
+ const idx = entries.findIndex((e) => e.relPath === relPath);
26483
+ if (idx < 0) return false;
26484
+ entries.splice(idx, 1);
26485
+ this.writeFile(scope, sessionId, entries);
26486
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26487
+ return true;
26120
26488
  }
26121
- async set(v2) {
26122
- const dir = import_node_path15.default.dirname(this.filePath);
26123
- await import_node_fs14.default.promises.mkdir(dir, { recursive: true });
26124
- const data = JSON.stringify(v2, null, 2);
26125
- const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
26126
- await import_node_fs14.default.promises.writeFile(tmp, data, { mode: 384 });
26127
- if (process.platform !== "win32") {
26489
+ /**
26490
+ * session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
26491
+ *
26492
+ * <dataDir>/sessions/<personaId>/owner/*.group-files.json
26493
+ * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
26494
+ *
26495
+ * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26496
+ */
26497
+ listByPersona(personaId) {
26498
+ const out = [];
26499
+ for (const mode of ["owner", "listener"]) {
26500
+ const scope = { kind: "persona", personaId, mode };
26501
+ const root = this.rootForScope(scope);
26502
+ let names;
26128
26503
  try {
26129
- await import_node_fs14.default.promises.chmod(tmp, 384);
26130
- } catch {
26504
+ names = import_node_fs13.default.readdirSync(root);
26505
+ } catch (err) {
26506
+ const code = err?.code;
26507
+ if (code === "ENOENT") continue;
26508
+ continue;
26509
+ }
26510
+ for (const name of names) {
26511
+ if (!name.endsWith(".group-files.json")) continue;
26512
+ const sessionId = name.slice(0, -".group-files.json".length);
26513
+ if (!sessionId) continue;
26514
+ const entries = this.list(scope, sessionId);
26515
+ out.push({ sessionId, entries });
26131
26516
  }
26132
26517
  }
26133
- await import_node_fs14.default.promises.rename(tmp, this.filePath);
26518
+ return out;
26134
26519
  }
26135
- async clear() {
26136
- try {
26137
- await import_node_fs14.default.promises.unlink(this.filePath);
26138
- } catch (err) {
26139
- const code = err?.code;
26140
- if (code !== "ENOENT") throw err;
26520
+ };
26521
+ function personalViewable(groupStore, personaDir, personaId, absPath) {
26522
+ const realTarget = safeRealpath(absPath);
26523
+ if (!realTarget) {
26524
+ return false;
26525
+ }
26526
+ const personasUnion = groupStore.listByPersona(personaId);
26527
+ for (const { entries } of personasUnion) {
26528
+ for (const e of entries) {
26529
+ if (e.stale) continue;
26530
+ const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
26531
+ if (realEntry && realEntry === realTarget) return true;
26141
26532
  }
26142
26533
  }
26143
- };
26144
- function isPersistedTunnel(o) {
26145
- if (!o || typeof o !== "object") return false;
26146
- const r = o;
26147
- return typeof r.subdomain === "string" && typeof r.frpsHost === "string" && typeof r.frpsPort === "number" && typeof r.frpsToken === "string" && typeof r.url === "string" && typeof r.registeredAt === "string";
26534
+ return false;
26148
26535
  }
26149
-
26150
- // src/tunnel/register-client.ts
26151
- var TunnelRegisterError = class extends Error {
26152
- constructor(message, cause) {
26153
- super(message);
26154
- this.cause = cause;
26155
- this.name = "TunnelRegisterError";
26536
+ function safeRealpath(p2) {
26537
+ try {
26538
+ return import_node_fs13.default.realpathSync(p2);
26539
+ } catch {
26540
+ return null;
26156
26541
  }
26157
- cause;
26542
+ }
26543
+
26544
+ // src/attachment/mime.ts
26545
+ var import_node_path15 = __toESM(require("path"), 1);
26546
+ var EXT_TO_MIME = {
26547
+ ".md": "text/markdown",
26548
+ ".markdown": "text/markdown",
26549
+ ".txt": "text/plain",
26550
+ ".log": "text/plain",
26551
+ ".json": "application/json",
26552
+ ".html": "text/html",
26553
+ ".htm": "text/html",
26554
+ ".css": "text/css",
26555
+ ".js": "application/javascript",
26556
+ ".ts": "application/typescript",
26557
+ ".tsx": "application/typescript",
26558
+ ".png": "image/png",
26559
+ ".jpg": "image/jpeg",
26560
+ ".jpeg": "image/jpeg",
26561
+ ".gif": "image/gif",
26562
+ ".webp": "image/webp",
26563
+ ".svg": "image/svg+xml",
26564
+ ".pdf": "application/pdf",
26565
+ ".mp4": "video/mp4",
26566
+ ".webm": "video/webm",
26567
+ ".mp3": "audio/mpeg",
26568
+ ".wav": "audio/wav",
26569
+ ".zip": "application/zip",
26570
+ ".gz": "application/gzip",
26571
+ ".tar": "application/x-tar",
26572
+ ".csv": "text/csv",
26573
+ ".xml": "application/xml",
26574
+ ".yaml": "application/yaml",
26575
+ ".yml": "application/yaml"
26576
+ };
26577
+ function lookupMime(filePathOrName) {
26578
+ const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
26579
+ return EXT_TO_MIME[ext] ?? "application/octet-stream";
26580
+ }
26581
+
26582
+ // src/attachment/inbox.ts
26583
+ var import_node_fs14 = __toESM(require("fs"), 1);
26584
+ var import_node_path16 = __toESM(require("path"), 1);
26585
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
26586
+ var DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
26587
+ async function handlePersonaInbox(req, args, deps) {
26588
+ const filename = readFilenameFromUrl(req.url);
26589
+ if (!filename) throw httpErr(400, "INVALID_PARAM", "missing ?name= query");
26590
+ const safeName = sanitizeFilename(filename);
26591
+ const mime = lookupMime(safeName);
26592
+ if (deps.isAllowedMime && !deps.isAllowedMime(mime)) {
26593
+ throw httpErr(415, "UNSUPPORTED_MEDIA", `mime ${mime} not allowed`);
26594
+ }
26595
+ const inboxDir = import_node_path16.default.join(args.personaDir, "_inbox");
26596
+ import_node_fs14.default.mkdirSync(inboxDir, { recursive: true });
26597
+ const sha = import_node_crypto5.default.randomBytes(6).toString("hex");
26598
+ const fileName = `${sha}-${safeName}`;
26599
+ const absPath = import_node_path16.default.join(inboxDir, fileName);
26600
+ const size = await streamBodyToFile(req, absPath, deps.maxBytes ?? DEFAULT_MAX_BYTES);
26601
+ const relPath = `_inbox/${fileName}`;
26602
+ deps.groupFileStore.upsert(args.scope, args.sessionId, {
26603
+ relPath,
26604
+ from: args.from,
26605
+ addedBy: args.addedBy,
26606
+ size,
26607
+ mime
26608
+ });
26609
+ return { relPath, size, mime };
26610
+ }
26611
+ async function handleSessionInbox(req, args, deps) {
26612
+ const filename = readFilenameFromUrl(req.url);
26613
+ if (!filename) throw httpErr(400, "INVALID_PARAM", "missing ?name= query");
26614
+ const safeName = sanitizeFilename(filename);
26615
+ const mime = lookupMime(safeName);
26616
+ if (deps.isAllowedMime && !deps.isAllowedMime(mime)) {
26617
+ throw httpErr(415, "UNSUPPORTED_MEDIA", `mime ${mime} not allowed`);
26618
+ }
26619
+ const inboxDir = import_node_path16.default.join(args.sessionCwd, "_inbox");
26620
+ import_node_fs14.default.mkdirSync(inboxDir, { recursive: true });
26621
+ const sha = import_node_crypto5.default.randomBytes(6).toString("hex");
26622
+ const fileName = `${sha}-${safeName}`;
26623
+ const absPath = import_node_path16.default.join(inboxDir, fileName);
26624
+ const size = await streamBodyToFile(req, absPath, deps.maxBytes ?? DEFAULT_MAX_BYTES);
26625
+ const relPath = `_inbox/${fileName}`;
26626
+ deps.groupFileStore.upsert(args.scope, args.sessionId, {
26627
+ relPath,
26628
+ from: "owner",
26629
+ size,
26630
+ mime
26631
+ });
26632
+ return { relPath, size, mime };
26633
+ }
26634
+ function streamBodyToFile(req, absPath, maxBytes) {
26635
+ return new Promise((resolve2, reject) => {
26636
+ let received = 0;
26637
+ const tmp = `${absPath}.tmp-${process.pid}-${Date.now()}`;
26638
+ const stream = import_node_fs14.default.createWriteStream(tmp, { mode: 384 });
26639
+ req.on("data", (chunk) => {
26640
+ received += chunk.length;
26641
+ if (received > maxBytes) {
26642
+ stream.destroy();
26643
+ try {
26644
+ import_node_fs14.default.unlinkSync(tmp);
26645
+ } catch {
26646
+ }
26647
+ reject(httpErr(413, "TOO_LARGE", `upload exceeds ${maxBytes} bytes`));
26648
+ req.destroy();
26649
+ return;
26650
+ }
26651
+ stream.write(chunk);
26652
+ });
26653
+ req.on("end", () => {
26654
+ stream.end();
26655
+ stream.on("finish", () => {
26656
+ try {
26657
+ import_node_fs14.default.renameSync(tmp, absPath);
26658
+ resolve2(received);
26659
+ } catch (err) {
26660
+ reject(err);
26661
+ }
26662
+ });
26663
+ });
26664
+ req.on("error", reject);
26665
+ stream.on("error", reject);
26666
+ });
26667
+ }
26668
+ function readFilenameFromUrl(rawUrl) {
26669
+ if (!rawUrl) return null;
26670
+ try {
26671
+ const u = new URL(rawUrl, "http://placeholder");
26672
+ return u.searchParams.get("name");
26673
+ } catch {
26674
+ return null;
26675
+ }
26676
+ }
26677
+ function sanitizeFilename(name) {
26678
+ const trimmed = name.replace(/[\x00-\x1f]/g, "").replace(/\//g, "_").replace(/\\/g, "_");
26679
+ const dotsOnly = trimmed.replace(/^\.+/, (m2) => "_".repeat(m2.length));
26680
+ if (!dotsOnly) return "unnamed";
26681
+ return dotsOnly.slice(0, 200);
26682
+ }
26683
+ function httpErr(status, code, message) {
26684
+ const err = new Error(message);
26685
+ err.httpStatus = status;
26686
+ err.code = code;
26687
+ return err;
26688
+ }
26689
+ function isHttpError(err) {
26690
+ return err instanceof Error && typeof err.httpStatus === "number" && typeof err.code === "string";
26691
+ }
26692
+ async function handleInboxRequest(req, res, ctxRouter, deps) {
26693
+ const personaMatch = ctxRouter.pathname.match(/^\/persona\/([^/]+)\/inbox$/);
26694
+ if (personaMatch && req.method === "POST") {
26695
+ if (!deps.getPersonaDir || !deps.getPersonaScope) {
26696
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "inbox not wired" });
26697
+ return true;
26698
+ }
26699
+ const pid = personaMatch[1];
26700
+ const personaDir = deps.getPersonaDir(pid);
26701
+ if (!personaDir) {
26702
+ sendJson(res, 404, { code: "NOT_FOUND", message: `persona ${pid} not found` });
26703
+ return true;
26704
+ }
26705
+ const sessionId = new URL(req.url ?? "", "http://placeholder").searchParams.get("session") || pid;
26706
+ try {
26707
+ const result = await handlePersonaInbox(
26708
+ req,
26709
+ {
26710
+ personaId: pid,
26711
+ personaDir,
26712
+ sessionId,
26713
+ scope: deps.getPersonaScope(pid),
26714
+ addedBy: deps.addedBy,
26715
+ from: ctxRouter.isOwner ? "owner" : "user"
26716
+ },
26717
+ deps.inboxOpts ?? { groupFileStore: deps.groupFileStore }
26718
+ );
26719
+ sendJson(res, 200, result);
26720
+ } catch (err) {
26721
+ handleInboxError(err, res);
26722
+ }
26723
+ return true;
26724
+ }
26725
+ const sessionMatch = ctxRouter.pathname.match(/^\/session\/([^/]+)\/inbox$/);
26726
+ if (sessionMatch && req.method === "POST") {
26727
+ if (!ctxRouter.isOwner) {
26728
+ sendJson(res, 403, { code: "FORBIDDEN", message: "direct session inbox is owner-only" });
26729
+ return true;
26730
+ }
26731
+ if (!deps.getSessionCwd) {
26732
+ sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "inbox not wired" });
26733
+ return true;
26734
+ }
26735
+ const sid = sessionMatch[1];
26736
+ const sessionCwd = deps.getSessionCwd(sid);
26737
+ if (!sessionCwd) {
26738
+ sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
26739
+ return true;
26740
+ }
26741
+ try {
26742
+ const result = await handleSessionInbox(
26743
+ req,
26744
+ {
26745
+ sessionId: sid,
26746
+ sessionCwd: sessionCwd.cwd,
26747
+ scope: sessionCwd.scope,
26748
+ from: "owner"
26749
+ },
26750
+ deps.inboxOpts ?? { groupFileStore: deps.groupFileStore }
26751
+ );
26752
+ sendJson(res, 200, result);
26753
+ } catch (err) {
26754
+ handleInboxError(err, res);
26755
+ }
26756
+ return true;
26757
+ }
26758
+ return false;
26759
+ }
26760
+ function handleInboxError(err, res) {
26761
+ if (isHttpError(err)) {
26762
+ sendJson(res, err.httpStatus, { code: err.code, message: err.message });
26763
+ return;
26764
+ }
26765
+ sendJson(res, 500, { code: "INTERNAL", message: err.message });
26766
+ }
26767
+ function sendJson(res, status, body) {
26768
+ if (res.headersSent) return;
26769
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
26770
+ res.end(JSON.stringify(body));
26771
+ }
26772
+
26773
+ // src/transport/http-router.ts
26774
+ function createHttpRouter(deps) {
26775
+ return async (req, res) => {
26776
+ const url = parseUrl(req.url);
26777
+ if (!url) {
26778
+ sendJson2(res, 400, { code: "INVALID_URL", message: "malformed request URL" });
26779
+ return true;
26780
+ }
26781
+ if (url.pathname === "/healthz" && req.method === "GET") {
26782
+ sendJson2(res, 200, { ok: true, version: deps.daemonVersion });
26783
+ return true;
26784
+ }
26785
+ if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && url.pathname !== "/outbox" && !url.pathname.startsWith("/outbox/")) {
26786
+ return false;
26787
+ }
26788
+ {
26789
+ const directMatch = url.pathname.match(/^\/outbox\/([^/]+)$/);
26790
+ const personaMatch = url.pathname.match(/^\/persona\/([^/]+)\/outbox\/([^/]+)$/);
26791
+ if (directMatch || personaMatch) {
26792
+ if (!deps.outboxStore) {
26793
+ sendJson2(res, 501, { code: "NOT_IMPLEMENTED", message: "outbox store not wired" });
26794
+ return true;
26795
+ }
26796
+ const capToken = directMatch ? directMatch[1] : personaMatch[2];
26797
+ const lookup = deps.outboxStore.lookup(capToken);
26798
+ if (!lookup.ok) {
26799
+ const statusByCode = {
26800
+ NOT_FOUND: 404,
26801
+ EXPIRED: 410,
26802
+ REVOKED: 410
26803
+ };
26804
+ sendJson2(res, statusByCode[lookup.code], { code: lookup.code, message: "outbox cap not usable" });
26805
+ return true;
26806
+ }
26807
+ const { entry } = lookup;
26808
+ if (entry.scope.kind === "personal") {
26809
+ const ctx2 = deps.authResolver.resolveFromHeader(
26810
+ req.headers.authorization,
26811
+ req.socket.remoteAddress ?? void 0
26812
+ );
26813
+ if (!ctx2 || ctx2.role !== "personal" || ctx2.personaId !== entry.scope.personalId) {
26814
+ sendJson2(res, 403, { code: "FORBIDDEN", message: "personal-scoped cap requires matching token" });
26815
+ return true;
26816
+ }
26817
+ }
26818
+ const outboxStore = deps.outboxStore;
26819
+ streamFile(res, entry.absPath, deps.logger, () => outboxStore.consume(capToken));
26820
+ return true;
26821
+ }
26822
+ }
26823
+ const ctx = deps.authResolver.resolveFromHeader(
26824
+ req.headers.authorization,
26825
+ req.socket.remoteAddress ?? void 0
26826
+ );
26827
+ if (!ctx) {
26828
+ sendJson2(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
26829
+ return true;
26830
+ }
26831
+ const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
26832
+ if (personaFilesMatch && req.method === "GET") {
26833
+ const pid = personaFilesMatch[1];
26834
+ const pathParam = url.searchParams.get("path");
26835
+ if (!pathParam) {
26836
+ sendJson2(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26837
+ return true;
26838
+ }
26839
+ if (!deps.personaStore || !deps.groupFileStore) {
26840
+ sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
26841
+ return true;
26842
+ }
26843
+ const personaDir = deps.personaStore.personaDirPath(pid);
26844
+ const absPath = import_node_path17.default.isAbsolute(pathParam) ? pathParam : import_node_path17.default.join(personaDir, pathParam);
26845
+ if (!import_node_path17.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
26846
+ sendJson2(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
26847
+ return true;
26848
+ }
26849
+ if (ctx.role === "personal") {
26850
+ if (ctx.personaId !== pid) {
26851
+ sendJson2(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
26852
+ return true;
26853
+ }
26854
+ if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
26855
+ sendJson2(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
26856
+ return true;
26857
+ }
26858
+ }
26859
+ streamFile(res, absPath, deps.logger);
26860
+ return true;
26861
+ }
26862
+ const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
26863
+ if (sessionFilesMatch && req.method === "GET") {
26864
+ if (ctx.role !== "owner") {
26865
+ sendJson2(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
26866
+ return true;
26867
+ }
26868
+ const sid = sessionFilesMatch[1];
26869
+ const pathParam = url.searchParams.get("path");
26870
+ if (!pathParam) {
26871
+ sendJson2(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26872
+ return true;
26873
+ }
26874
+ let absPath;
26875
+ if (import_node_path17.default.isAbsolute(pathParam)) {
26876
+ absPath = pathParam;
26877
+ } else if (deps.sessionStore) {
26878
+ const file = deps.sessionStore.read(sid);
26879
+ if (!file) {
26880
+ sendJson2(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
26881
+ return true;
26882
+ }
26883
+ absPath = import_node_path17.default.join(file.cwd, pathParam);
26884
+ } else {
26885
+ sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
26886
+ return true;
26887
+ }
26888
+ streamFile(res, absPath, deps.logger);
26889
+ return true;
26890
+ }
26891
+ if (/^\/persona\/[^/]+\/inbox$/.test(url.pathname) && req.method === "POST") {
26892
+ if (!deps.groupFileStore || !deps.personaStore) {
26893
+ sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "inbox not wired" }));
26894
+ return true;
26895
+ }
26896
+ const handled = await handleInboxRequest(
26897
+ req,
26898
+ res,
26899
+ {
26900
+ pathname: url.pathname,
26901
+ isOwner: ctx.role === "owner",
26902
+ personaId: ctx.personaId
26903
+ },
26904
+ {
26905
+ groupFileStore: deps.groupFileStore,
26906
+ getPersonaDir: (pid) => deps.personaStore.personaDirPath(pid),
26907
+ // PR 7:persona inbox 默认走 owner-mode scope;UI 端如果用 personal listener
26908
+ // 也归到 owner scope(session 维度)—— PR 9 完善时再走 listener scope。
26909
+ getPersonaScope: (pid) => ({
26910
+ kind: "persona",
26911
+ personaId: pid,
26912
+ mode: "owner"
26913
+ }),
26914
+ addedBy: ctx.label ?? (ctx.role === "owner" ? "owner" : "unknown")
26915
+ }
26916
+ );
26917
+ if (handled) return true;
26918
+ }
26919
+ if (/^\/session\/[^/]+\/inbox$/.test(url.pathname) && req.method === "POST") {
26920
+ if (!deps.groupFileStore || !deps.sessionStore) {
26921
+ sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "inbox not wired" }));
26922
+ return true;
26923
+ }
26924
+ const handled = await handleInboxRequest(
26925
+ req,
26926
+ res,
26927
+ {
26928
+ pathname: url.pathname,
26929
+ isOwner: ctx.role === "owner"
26930
+ },
26931
+ {
26932
+ groupFileStore: deps.groupFileStore,
26933
+ getSessionCwd: (sid) => {
26934
+ const f = deps.sessionStore.read(sid);
26935
+ if (!f) return null;
26936
+ const scope = f.ownerPersonaId ? { kind: "persona", personaId: f.ownerPersonaId, mode: "owner" } : { kind: "default" };
26937
+ return { cwd: f.cwd, scope };
26938
+ },
26939
+ addedBy: ctx.role === "owner" ? "owner" : "unknown"
26940
+ }
26941
+ );
26942
+ if (handled) return true;
26943
+ }
26944
+ if (/^\/persona\/[^/]+\/attachment-meta$/.test(url.pathname) && req.method === "GET") {
26945
+ sendJson2(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "attachment-meta \u2014 PR 6" }));
26946
+ return true;
26947
+ }
26948
+ sendJson2(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
26949
+ return true;
26950
+ };
26951
+ }
26952
+ function parseUrl(rawUrl) {
26953
+ if (!rawUrl) return null;
26954
+ try {
26955
+ return new URL(rawUrl, "http://placeholder");
26956
+ } catch {
26957
+ return null;
26958
+ }
26959
+ }
26960
+ function sendJson2(res, status, body) {
26961
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
26962
+ res.end(JSON.stringify(body));
26963
+ }
26964
+ function withCtx(ctx, body) {
26965
+ return { ...body, role: ctx.role, personaId: ctx.personaId };
26966
+ }
26967
+ function isContainedIn(abs, root) {
26968
+ const normalized = import_node_path17.default.resolve(abs);
26969
+ const normalizedRoot = import_node_path17.default.resolve(root);
26970
+ if (normalized === normalizedRoot) return true;
26971
+ return normalized.startsWith(normalizedRoot + import_node_path17.default.sep);
26972
+ }
26973
+ function streamFile(res, absPath, logger, onComplete) {
26974
+ let stat;
26975
+ try {
26976
+ stat = import_node_fs15.default.statSync(absPath);
26977
+ } catch (err) {
26978
+ const code = err?.code;
26979
+ if (code === "ENOENT") {
26980
+ sendJson2(res, 404, { code: "NOT_FOUND", message: "file not found" });
26981
+ } else {
26982
+ sendJson2(res, 500, { code: "STAT_FAILED", message: err.message });
26983
+ }
26984
+ return;
26985
+ }
26986
+ if (!stat.isFile()) {
26987
+ sendJson2(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
26988
+ return;
26989
+ }
26990
+ const mime = lookupMime(absPath);
26991
+ res.writeHead(200, {
26992
+ "Content-Type": mime,
26993
+ "Content-Length": String(stat.size),
26994
+ // 防止浏览器把任意 mime 当 html 渲染(spec §11 #8 安全心智)
26995
+ "X-Content-Type-Options": "nosniff"
26996
+ });
26997
+ const stream = import_node_fs15.default.createReadStream(absPath);
26998
+ stream.on("error", (err) => {
26999
+ logger?.warn("streamFile read error", { absPath, err: err.message });
27000
+ res.destroy();
27001
+ });
27002
+ if (onComplete) {
27003
+ res.once("finish", onComplete);
27004
+ }
27005
+ stream.pipe(res);
27006
+ }
27007
+
27008
+ // src/attachment/outbox.ts
27009
+ var import_node_fs16 = __toESM(require("fs"), 1);
27010
+ var import_node_path18 = __toESM(require("path"), 1);
27011
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
27012
+ init_protocol();
27013
+ var FILE_NAME = "outbox-caps.json";
27014
+ var OutboxStore = class {
27015
+ file;
27016
+ now;
27017
+ logger;
27018
+ cache = [];
27019
+ constructor(opts) {
27020
+ this.file = import_node_path18.default.join(opts.dataDir, FILE_NAME);
27021
+ this.now = opts.now ?? Date.now;
27022
+ this.logger = opts.logger;
27023
+ this.reload();
27024
+ }
27025
+ reload() {
27026
+ try {
27027
+ const raw = import_node_fs16.default.readFileSync(this.file, "utf8");
27028
+ const parsed = JSON.parse(raw);
27029
+ if (!Array.isArray(parsed)) {
27030
+ this.logger?.warn("OutboxStore.reload: outbox-caps.json is not an array; resetting", {
27031
+ file: this.file
27032
+ });
27033
+ this.cache = [];
27034
+ return;
27035
+ }
27036
+ const out = [];
27037
+ for (const item of parsed) {
27038
+ const r = OutboxCapEntrySchema.safeParse(item);
27039
+ if (r.success) out.push(r.data);
27040
+ }
27041
+ this.cache = out;
27042
+ } catch (err) {
27043
+ const code = err?.code;
27044
+ if (code !== "ENOENT") {
27045
+ this.logger?.warn("OutboxStore.reload failed; outbox-caps.json may be corrupt", {
27046
+ file: this.file,
27047
+ err: err.message
27048
+ });
27049
+ }
27050
+ this.cache = [];
27051
+ }
27052
+ }
27053
+ persist() {
27054
+ import_node_fs16.default.mkdirSync(import_node_path18.default.dirname(this.file), { recursive: true });
27055
+ const tmp = `${this.file}.tmp-${process.pid}-${Date.now()}`;
27056
+ import_node_fs16.default.writeFileSync(tmp, JSON.stringify(this.cache, null, 2), { mode: 384 });
27057
+ import_node_fs16.default.renameSync(tmp, this.file);
27058
+ }
27059
+ /** 列出当前缓存(含 revoked / 过期,UI Drawer 显灰;过滤靠调用方) */
27060
+ list() {
27061
+ return this.cache.slice();
27062
+ }
27063
+ /** 按 personaId / sessionId 过滤;为 outbox Drawer 服务 */
27064
+ listByPersona(personaId) {
27065
+ return this.cache.filter(
27066
+ (c) => c.scopeRef.kind === "persona" && c.scopeRef.personaId === personaId
27067
+ );
27068
+ }
27069
+ /**
27070
+ * Mint a new cap. capToken = 32B 随机 base64url(256-bit entropy),spec §11 #8 防剪贴板/微信
27071
+ * 截图泄露通过短码的暴力面。返回完整 entry 让 caller 拼 URL。
27072
+ *
27073
+ * @param ttlSeconds null = 永久;undefined = 默认 24h(spec §12)
27074
+ */
27075
+ mint(input) {
27076
+ const ts = this.now();
27077
+ const ttl = input.ttlSeconds === null ? null : input.ttlSeconds ?? 24 * 3600;
27078
+ const entry = {
27079
+ capToken: import_node_crypto6.default.randomBytes(32).toString("base64url"),
27080
+ scopeRef: input.scopeRef,
27081
+ absPath: input.absPath,
27082
+ name: input.name,
27083
+ expiresAt: ttl === null ? null : ts + ttl * 1e3,
27084
+ oneShot: input.oneShot ?? false,
27085
+ scope: input.scope ?? { kind: "public" },
27086
+ hits: 0,
27087
+ createdAt: ts
27088
+ };
27089
+ this.cache.push(entry);
27090
+ this.persist();
27091
+ return entry;
27092
+ }
27093
+ /** Mark revoked;不删除条目(便于审计),上层 lookup 后返回 410 */
27094
+ revoke(capToken) {
27095
+ const idx = this.cache.findIndex((c) => c.capToken === capToken);
27096
+ if (idx < 0) return false;
27097
+ if (this.cache[idx].revoked) return false;
27098
+ this.cache[idx] = { ...this.cache[idx], revoked: true };
27099
+ this.persist();
27100
+ return true;
27101
+ }
27102
+ /**
27103
+ * HTTP 层用:根据 capToken 找到 entry 并判定能不能用。
27104
+ * 三种结果:
27105
+ * - { ok: true, entry } 可用,调用方流文件
27106
+ * - { ok: false, code: 'EXPIRED' | 'REVOKED' | 'NOT_FOUND' }
27107
+ *
27108
+ * 命中 + oneShot=true 时由调用方调 consume() 把 cache 中 hits 自增并立即 revoke
27109
+ * (consume 应当在 response 'finish' 后才触发,避免中途断流误算消费)。
27110
+ */
27111
+ lookup(capToken) {
27112
+ const entry = this.cache.find((c) => c.capToken === capToken);
27113
+ if (!entry) return { ok: false, code: "NOT_FOUND" };
27114
+ if (entry.revoked) return { ok: false, code: "REVOKED" };
27115
+ if (entry.expiresAt !== null && this.now() > entry.expiresAt) {
27116
+ return { ok: false, code: "EXPIRED" };
27117
+ }
27118
+ return { ok: true, entry };
27119
+ }
27120
+ /** 命中后真正"消费"一次:hits++ + oneShot 时 revoke。daemon HTTP 层 stream 成功后再调 */
27121
+ consume(capToken) {
27122
+ const idx = this.cache.findIndex((c) => c.capToken === capToken);
27123
+ if (idx < 0) return;
27124
+ const prev = this.cache[idx];
27125
+ this.cache[idx] = {
27126
+ ...prev,
27127
+ hits: prev.hits + 1,
27128
+ revoked: prev.oneShot ? true : prev.revoked
27129
+ };
27130
+ this.persist();
27131
+ }
27132
+ };
27133
+
27134
+ // src/attachment/mount.ts
27135
+ var import_node_fs17 = __toESM(require("fs"), 1);
27136
+ var import_node_path19 = __toESM(require("path"), 1);
27137
+ init_protocol();
27138
+ var MountStore = class {
27139
+ constructor(opts) {
27140
+ this.opts = opts;
27141
+ }
27142
+ opts;
27143
+ filePath(personaId) {
27144
+ return import_node_path19.default.join(this.opts.personaDirPath(personaId), ".clawd", "shared-files.json");
27145
+ }
27146
+ list(personaId) {
27147
+ try {
27148
+ const raw = import_node_fs17.default.readFileSync(this.filePath(personaId), "utf8");
27149
+ const parsed = JSON.parse(raw);
27150
+ if (!Array.isArray(parsed)) return [];
27151
+ const out = [];
27152
+ for (const item of parsed) {
27153
+ const r = MountEntrySchema.safeParse(item);
27154
+ if (r.success) out.push(r.data);
27155
+ }
27156
+ return out;
27157
+ } catch {
27158
+ return [];
27159
+ }
27160
+ }
27161
+ /**
27162
+ * Add a mount。在 personaDir 创建 link/copy + 写 manifest。冲突 basename 抛错(caller
27163
+ * 决定是覆盖还是改名)。
27164
+ */
27165
+ add(personaId, input) {
27166
+ const personaDir = this.opts.personaDirPath(personaId);
27167
+ if (!import_node_fs17.default.existsSync(personaDir)) {
27168
+ throw new Error(`personaDir not found: ${personaDir}`);
27169
+ }
27170
+ const source = import_node_path19.default.resolve(input.absPath);
27171
+ if (!import_node_fs17.default.existsSync(source)) {
27172
+ throw new Error(`source path not found: ${source}`);
27173
+ }
27174
+ const basename = import_node_path19.default.basename(source);
27175
+ const dest = import_node_path19.default.join(personaDir, basename);
27176
+ if (import_node_fs17.default.existsSync(dest)) {
27177
+ throw new Error(`destination already exists: ${dest}`);
27178
+ }
27179
+ if (input.mode === "link") {
27180
+ import_node_fs17.default.symlinkSync(source, dest);
27181
+ } else {
27182
+ import_node_fs17.default.copyFileSync(source, dest);
27183
+ }
27184
+ const entry = {
27185
+ basename,
27186
+ absPath: source,
27187
+ mode: input.mode
27188
+ };
27189
+ const next = this.list(personaId).concat([entry]);
27190
+ this.writeManifest(personaId, next);
27191
+ return entry;
27192
+ }
27193
+ remove(personaId, basename) {
27194
+ const entries = this.list(personaId);
27195
+ const idx = entries.findIndex((e) => e.basename === basename);
27196
+ if (idx < 0) return false;
27197
+ const entry = entries[idx];
27198
+ const personaDir = this.opts.personaDirPath(personaId);
27199
+ const dest = import_node_path19.default.join(personaDir, basename);
27200
+ try {
27201
+ import_node_fs17.default.unlinkSync(dest);
27202
+ } catch {
27203
+ }
27204
+ const next = entries.slice();
27205
+ next.splice(idx, 1);
27206
+ this.writeManifest(personaId, next);
27207
+ void entry;
27208
+ return true;
27209
+ }
27210
+ writeManifest(personaId, entries) {
27211
+ const file = this.filePath(personaId);
27212
+ import_node_fs17.default.mkdirSync(import_node_path19.default.dirname(file), { recursive: true });
27213
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27214
+ import_node_fs17.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27215
+ import_node_fs17.default.renameSync(tmp, file);
27216
+ }
27217
+ };
27218
+
27219
+ // src/discovery/state-file.ts
27220
+ var import_node_fs18 = __toESM(require("fs"), 1);
27221
+ var import_node_path20 = __toESM(require("path"), 1);
27222
+ function defaultStateFilePath(dataDir) {
27223
+ return import_node_path20.default.join(dataDir, "state.json");
27224
+ }
27225
+ function isPidAlive(pid) {
27226
+ if (!Number.isFinite(pid) || pid <= 0) return false;
27227
+ try {
27228
+ process.kill(pid, 0);
27229
+ return true;
27230
+ } catch (err) {
27231
+ const code = err.code;
27232
+ return code === "EPERM";
27233
+ }
27234
+ }
27235
+ var StateFileManager = class {
27236
+ file;
27237
+ constructor(opts) {
27238
+ this.file = defaultStateFilePath(opts.dataDir);
27239
+ }
27240
+ filePath() {
27241
+ return this.file;
27242
+ }
27243
+ read() {
27244
+ try {
27245
+ const raw = import_node_fs18.default.readFileSync(this.file, "utf8");
27246
+ const parsed = JSON.parse(raw);
27247
+ return parsed;
27248
+ } catch {
27249
+ return null;
27250
+ }
27251
+ }
27252
+ preflight() {
27253
+ const existing = this.read();
27254
+ if (!existing) return { status: "ok" };
27255
+ if (isPidAlive(existing.pid)) return { status: "active", existing };
27256
+ return { status: "stale", existing };
27257
+ }
27258
+ write(state) {
27259
+ import_node_fs18.default.mkdirSync(import_node_path20.default.dirname(this.file), { recursive: true });
27260
+ const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
27261
+ import_node_fs18.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
27262
+ import_node_fs18.default.renameSync(tmp, this.file);
27263
+ if (process.platform !== "win32") {
27264
+ try {
27265
+ import_node_fs18.default.chmodSync(this.file, 384);
27266
+ } catch {
27267
+ }
27268
+ }
27269
+ }
27270
+ delete() {
27271
+ try {
27272
+ import_node_fs18.default.unlinkSync(this.file);
27273
+ } catch {
27274
+ }
27275
+ }
27276
+ };
27277
+
27278
+ // src/tunnel/tunnel-manager.ts
27279
+ var import_node_fs22 = __toESM(require("fs"), 1);
27280
+ var import_node_path24 = __toESM(require("path"), 1);
27281
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
27282
+ var import_node_child_process5 = require("child_process");
27283
+
27284
+ // src/tunnel/tunnel-store.ts
27285
+ var import_node_fs19 = __toESM(require("fs"), 1);
27286
+ var import_node_path21 = __toESM(require("path"), 1);
27287
+ var TunnelStore = class {
27288
+ constructor(filePath) {
27289
+ this.filePath = filePath;
27290
+ }
27291
+ filePath;
27292
+ async get() {
27293
+ try {
27294
+ const raw = await import_node_fs19.default.promises.readFile(this.filePath, "utf8");
27295
+ const obj = JSON.parse(raw);
27296
+ if (!isPersistedTunnel(obj)) return null;
27297
+ return obj;
27298
+ } catch (err) {
27299
+ const code = err?.code;
27300
+ if (code === "ENOENT") return null;
27301
+ return null;
27302
+ }
27303
+ }
27304
+ async set(v2) {
27305
+ const dir = import_node_path21.default.dirname(this.filePath);
27306
+ await import_node_fs19.default.promises.mkdir(dir, { recursive: true });
27307
+ const data = JSON.stringify(v2, null, 2);
27308
+ const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
27309
+ await import_node_fs19.default.promises.writeFile(tmp, data, { mode: 384 });
27310
+ if (process.platform !== "win32") {
27311
+ try {
27312
+ await import_node_fs19.default.promises.chmod(tmp, 384);
27313
+ } catch {
27314
+ }
27315
+ }
27316
+ await import_node_fs19.default.promises.rename(tmp, this.filePath);
27317
+ }
27318
+ async clear() {
27319
+ try {
27320
+ await import_node_fs19.default.promises.unlink(this.filePath);
27321
+ } catch (err) {
27322
+ const code = err?.code;
27323
+ if (code !== "ENOENT") throw err;
27324
+ }
27325
+ }
27326
+ };
27327
+ function isPersistedTunnel(o) {
27328
+ if (!o || typeof o !== "object") return false;
27329
+ const r = o;
27330
+ return typeof r.subdomain === "string" && typeof r.frpsHost === "string" && typeof r.frpsPort === "number" && typeof r.frpsToken === "string" && typeof r.url === "string" && typeof r.registeredAt === "string";
27331
+ }
27332
+
27333
+ // src/tunnel/register-client.ts
27334
+ var TunnelRegisterError = class extends Error {
27335
+ constructor(message, cause) {
27336
+ super(message);
27337
+ this.cause = cause;
27338
+ this.name = "TunnelRegisterError";
27339
+ }
27340
+ cause;
26158
27341
  };
26159
27342
  async function registerTunnel(opts) {
26160
27343
  const f = opts.fetchImpl ?? globalThis.fetch;
@@ -26229,9 +27412,9 @@ function escape(v2) {
26229
27412
  }
26230
27413
 
26231
27414
  // src/tunnel/frpc-binary.ts
26232
- var import_node_fs15 = __toESM(require("fs"), 1);
27415
+ var import_node_fs20 = __toESM(require("fs"), 1);
26233
27416
  var import_node_os9 = __toESM(require("os"), 1);
26234
- var import_node_path16 = __toESM(require("path"), 1);
27417
+ var import_node_path22 = __toESM(require("path"), 1);
26235
27418
  var import_node_child_process3 = require("child_process");
26236
27419
  var import_node_stream2 = require("stream");
26237
27420
  var import_promises = require("stream/promises");
@@ -26263,20 +27446,20 @@ function frpcDownloadUrl(version2, p2) {
26263
27446
  }
26264
27447
  async function ensureFrpcBinary(opts) {
26265
27448
  if (opts.override) {
26266
- if (!import_node_fs15.default.existsSync(opts.override)) {
27449
+ if (!import_node_fs20.default.existsSync(opts.override)) {
26267
27450
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
26268
27451
  }
26269
27452
  return opts.override;
26270
27453
  }
26271
27454
  const version2 = opts.version ?? FRPC_VERSION;
26272
27455
  const platform = opts.platform ?? detectPlatform();
26273
- const binDir = import_node_path16.default.join(opts.dataDir, "bin");
26274
- import_node_fs15.default.mkdirSync(binDir, { recursive: true });
27456
+ const binDir = import_node_path22.default.join(opts.dataDir, "bin");
27457
+ import_node_fs20.default.mkdirSync(binDir, { recursive: true });
26275
27458
  cleanupStaleArtifacts(binDir);
26276
- const stableBin = import_node_path16.default.join(binDir, "frpc");
26277
- if (import_node_fs15.default.existsSync(stableBin)) return stableBin;
27459
+ const stableBin = import_node_path22.default.join(binDir, "frpc");
27460
+ if (import_node_fs20.default.existsSync(stableBin)) return stableBin;
26278
27461
  const partialBin = `${stableBin}.partial`;
26279
- const tarballPath = import_node_path16.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
27462
+ const tarballPath = import_node_path22.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
26280
27463
  try {
26281
27464
  const url = frpcDownloadUrl(version2, platform);
26282
27465
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -26285,8 +27468,8 @@ async function ensureFrpcBinary(opts) {
26285
27468
  } else {
26286
27469
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
26287
27470
  }
26288
- import_node_fs15.default.chmodSync(partialBin, 493);
26289
- import_node_fs15.default.renameSync(partialBin, stableBin);
27471
+ import_node_fs20.default.chmodSync(partialBin, 493);
27472
+ import_node_fs20.default.renameSync(partialBin, stableBin);
26290
27473
  } finally {
26291
27474
  safeUnlink(tarballPath);
26292
27475
  safeUnlink(partialBin);
@@ -26296,15 +27479,15 @@ async function ensureFrpcBinary(opts) {
26296
27479
  function cleanupStaleArtifacts(binDir) {
26297
27480
  let entries;
26298
27481
  try {
26299
- entries = import_node_fs15.default.readdirSync(binDir);
27482
+ entries = import_node_fs20.default.readdirSync(binDir);
26300
27483
  } catch {
26301
27484
  return;
26302
27485
  }
26303
27486
  for (const name of entries) {
26304
27487
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
26305
- const full = import_node_path16.default.join(binDir, name);
27488
+ const full = import_node_path22.default.join(binDir, name);
26306
27489
  try {
26307
- import_node_fs15.default.rmSync(full, { recursive: true, force: true });
27490
+ import_node_fs20.default.rmSync(full, { recursive: true, force: true });
26308
27491
  } catch {
26309
27492
  }
26310
27493
  }
@@ -26312,7 +27495,7 @@ function cleanupStaleArtifacts(binDir) {
26312
27495
  }
26313
27496
  function safeUnlink(p2) {
26314
27497
  try {
26315
- import_node_fs15.default.unlinkSync(p2);
27498
+ import_node_fs20.default.unlinkSync(p2);
26316
27499
  } catch {
26317
27500
  }
26318
27501
  }
@@ -26323,13 +27506,13 @@ async function downloadToFile(url, dest, fetchImpl) {
26323
27506
  if (!res.ok || !res.body) {
26324
27507
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
26325
27508
  }
26326
- const out = import_node_fs15.default.createWriteStream(dest);
27509
+ const out = import_node_fs20.default.createWriteStream(dest);
26327
27510
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
26328
27511
  await (0, import_promises.pipeline)(nodeStream, out);
26329
27512
  }
26330
27513
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
26331
- const work = import_node_path16.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
26332
- import_node_fs15.default.mkdirSync(work, { recursive: true });
27514
+ const work = import_node_path22.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
27515
+ import_node_fs20.default.mkdirSync(work, { recursive: true });
26333
27516
  try {
26334
27517
  await new Promise((resolve2, reject) => {
26335
27518
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -26337,32 +27520,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
26337
27520
  proc.on("exit", (code) => code === 0 ? resolve2() : reject(new Error(`tar exited ${code}`)));
26338
27521
  });
26339
27522
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
26340
- const src = import_node_path16.default.join(work, dirName, "frpc");
26341
- if (!import_node_fs15.default.existsSync(src)) {
27523
+ const src = import_node_path22.default.join(work, dirName, "frpc");
27524
+ if (!import_node_fs20.default.existsSync(src)) {
26342
27525
  throw new Error(`frpc not found inside tarball at ${src}`);
26343
27526
  }
26344
- import_node_fs15.default.copyFileSync(src, destBin);
27527
+ import_node_fs20.default.copyFileSync(src, destBin);
26345
27528
  } finally {
26346
- import_node_fs15.default.rmSync(work, { recursive: true, force: true });
27529
+ import_node_fs20.default.rmSync(work, { recursive: true, force: true });
26347
27530
  }
26348
27531
  }
26349
27532
 
26350
27533
  // src/tunnel/frpc-process.ts
26351
- var import_node_fs16 = __toESM(require("fs"), 1);
26352
- var import_node_path17 = __toESM(require("path"), 1);
27534
+ var import_node_fs21 = __toESM(require("fs"), 1);
27535
+ var import_node_path23 = __toESM(require("path"), 1);
26353
27536
  var import_node_child_process4 = require("child_process");
26354
27537
  function frpcPidFilePath(dataDir) {
26355
- return import_node_path17.default.join(dataDir, "frpc.pid");
27538
+ return import_node_path23.default.join(dataDir, "frpc.pid");
26356
27539
  }
26357
27540
  function writeFrpcPid(dataDir, pid) {
26358
27541
  try {
26359
- import_node_fs16.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
27542
+ import_node_fs21.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
26360
27543
  } catch {
26361
27544
  }
26362
27545
  }
26363
27546
  function clearFrpcPid(dataDir) {
26364
27547
  try {
26365
- import_node_fs16.default.unlinkSync(frpcPidFilePath(dataDir));
27548
+ import_node_fs21.default.unlinkSync(frpcPidFilePath(dataDir));
26366
27549
  } catch {
26367
27550
  }
26368
27551
  }
@@ -26378,7 +27561,7 @@ function defaultIsPidAlive(pid) {
26378
27561
  }
26379
27562
  function defaultReadPidFile(file) {
26380
27563
  try {
26381
- return import_node_fs16.default.readFileSync(file, "utf8");
27564
+ return import_node_fs21.default.readFileSync(file, "utf8");
26382
27565
  } catch {
26383
27566
  return null;
26384
27567
  }
@@ -26394,7 +27577,7 @@ function defaultSleep(ms) {
26394
27577
  }
26395
27578
  async function killStaleFrpc(deps) {
26396
27579
  const pidFile = frpcPidFilePath(deps.dataDir);
26397
- const tomlPath = import_node_path17.default.join(deps.dataDir, "frpc.toml");
27580
+ const tomlPath = import_node_path23.default.join(deps.dataDir, "frpc.toml");
26398
27581
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
26399
27582
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
26400
27583
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -26418,7 +27601,7 @@ async function killStaleFrpc(deps) {
26418
27601
  }
26419
27602
  if (victims.size === 0) {
26420
27603
  try {
26421
- import_node_fs16.default.unlinkSync(pidFile);
27604
+ import_node_fs21.default.unlinkSync(pidFile);
26422
27605
  } catch {
26423
27606
  }
26424
27607
  return;
@@ -26429,7 +27612,7 @@ async function killStaleFrpc(deps) {
26429
27612
  }
26430
27613
  await sleep(deps.reapWaitMs ?? 300);
26431
27614
  try {
26432
- import_node_fs16.default.unlinkSync(pidFile);
27615
+ import_node_fs21.default.unlinkSync(pidFile);
26433
27616
  } catch {
26434
27617
  }
26435
27618
  }
@@ -26466,7 +27649,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
26466
27649
  var TunnelManager = class {
26467
27650
  constructor(deps) {
26468
27651
  this.deps = deps;
26469
- this.store = deps.store ?? new TunnelStore(import_node_path18.default.join(deps.dataDir, "tunnel.json"));
27652
+ this.store = deps.store ?? new TunnelStore(import_node_path24.default.join(deps.dataDir, "tunnel.json"));
26470
27653
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
26471
27654
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
26472
27655
  }
@@ -26593,8 +27776,8 @@ var TunnelManager = class {
26593
27776
  dataDir: this.deps.dataDir,
26594
27777
  override: this.deps.frpcBinaryOverride ?? void 0
26595
27778
  });
26596
- const tomlPath = import_node_path18.default.join(this.deps.dataDir, "frpc.toml");
26597
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto4.default.randomBytes(3).toString("hex")}`;
27779
+ const tomlPath = import_node_path24.default.join(this.deps.dataDir, "frpc.toml");
27780
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
26598
27781
  const toml = buildFrpcToml({
26599
27782
  serverAddr: t.frpsHost,
26600
27783
  serverPort: t.frpsPort,
@@ -26604,12 +27787,12 @@ var TunnelManager = class {
26604
27787
  localPort,
26605
27788
  logLevel: "info"
26606
27789
  });
26607
- await import_node_fs17.default.promises.writeFile(tomlPath, toml, { mode: 384 });
27790
+ await import_node_fs22.default.promises.writeFile(tomlPath, toml, { mode: 384 });
26608
27791
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
26609
27792
  stdio: ["ignore", "pipe", "pipe"]
26610
27793
  });
26611
- const logFilePath = import_node_path18.default.join(this.deps.dataDir, "frpc.log");
26612
- const logStream = import_node_fs17.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
27794
+ const logFilePath = import_node_path24.default.join(this.deps.dataDir, "frpc.log");
27795
+ const logStream = import_node_fs22.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
26613
27796
  logStream.on("error", () => {
26614
27797
  });
26615
27798
  const tee = (chunk) => {
@@ -26692,22 +27875,22 @@ async function waitForFrpcReady(proc, timeoutMs) {
26692
27875
 
26693
27876
  // src/tunnel/device-key.ts
26694
27877
  var import_node_os10 = __toESM(require("os"), 1);
26695
- var import_node_crypto5 = __toESM(require("crypto"), 1);
27878
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
26696
27879
  var DERIVE_SALT = "clawd-tunnel-device-v1";
26697
27880
  function deriveStableDeviceKey(opts = {}) {
26698
27881
  const hostname = opts.hostname ?? import_node_os10.default.hostname();
26699
27882
  const uid = opts.uid ?? (typeof import_node_os10.default.userInfo === "function" ? import_node_os10.default.userInfo().uid : 0);
26700
27883
  const input = `${hostname}::${uid}`;
26701
- return import_node_crypto5.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
27884
+ return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
26702
27885
  }
26703
27886
 
26704
27887
  // src/auth-store.ts
26705
- var import_node_fs18 = __toESM(require("fs"), 1);
26706
- var import_node_path19 = __toESM(require("path"), 1);
26707
- var import_node_crypto6 = __toESM(require("crypto"), 1);
27888
+ var import_node_fs23 = __toESM(require("fs"), 1);
27889
+ var import_node_path25 = __toESM(require("path"), 1);
27890
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
26708
27891
  var AUTH_FILE_NAME = "auth.json";
26709
27892
  function authFilePath(dataDir) {
26710
- return import_node_path19.default.join(dataDir, AUTH_FILE_NAME);
27893
+ return import_node_path25.default.join(dataDir, AUTH_FILE_NAME);
26711
27894
  }
26712
27895
  function loadOrCreateAuthToken(opts) {
26713
27896
  const file = authFilePath(opts.dataDir);
@@ -26719,11 +27902,11 @@ function loadOrCreateAuthToken(opts) {
26719
27902
  return token;
26720
27903
  }
26721
27904
  function defaultGenerate() {
26722
- return import_node_crypto6.default.randomBytes(32).toString("base64url");
27905
+ return import_node_crypto9.default.randomBytes(32).toString("base64url");
26723
27906
  }
26724
27907
  function readAuthFile(file) {
26725
27908
  try {
26726
- const raw = import_node_fs18.default.readFileSync(file, "utf8");
27909
+ const raw = import_node_fs23.default.readFileSync(file, "utf8");
26727
27910
  const parsed = JSON.parse(raw);
26728
27911
  if (typeof parsed?.token === "string" && parsed.token.length > 0) {
26729
27912
  return {
@@ -26739,25 +27922,25 @@ function readAuthFile(file) {
26739
27922
  }
26740
27923
  }
26741
27924
  function writeAuthFile(file, content) {
26742
- import_node_fs18.default.mkdirSync(import_node_path19.default.dirname(file), { recursive: true });
26743
- import_node_fs18.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
27925
+ import_node_fs23.default.mkdirSync(import_node_path25.default.dirname(file), { recursive: true });
27926
+ import_node_fs23.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
26744
27927
  try {
26745
- import_node_fs18.default.chmodSync(file, 384);
27928
+ import_node_fs23.default.chmodSync(file, 384);
26746
27929
  } catch {
26747
27930
  }
26748
27931
  }
26749
27932
 
26750
27933
  // src/owner-profile.ts
26751
- var import_node_fs19 = __toESM(require("fs"), 1);
27934
+ var import_node_fs24 = __toESM(require("fs"), 1);
26752
27935
  var import_node_os11 = __toESM(require("os"), 1);
26753
- var import_node_path20 = __toESM(require("path"), 1);
27936
+ var import_node_path26 = __toESM(require("path"), 1);
26754
27937
  var PROFILE_FILENAME = "profile.json";
26755
27938
  function loadOwnerDisplayName(dataDir) {
26756
27939
  const fallback = import_node_os11.default.userInfo().username;
26757
- const profilePath = import_node_path20.default.join(dataDir, PROFILE_FILENAME);
27940
+ const profilePath = import_node_path26.default.join(dataDir, PROFILE_FILENAME);
26758
27941
  let raw;
26759
27942
  try {
26760
- raw = import_node_fs19.default.readFileSync(profilePath, "utf8");
27943
+ raw = import_node_fs24.default.readFileSync(profilePath, "utf8");
26761
27944
  } catch {
26762
27945
  return fallback;
26763
27946
  }
@@ -26786,12 +27969,12 @@ init_protocol();
26786
27969
  init_protocol();
26787
27970
 
26788
27971
  // src/session/fork.ts
26789
- var import_node_fs20 = __toESM(require("fs"), 1);
27972
+ var import_node_fs25 = __toESM(require("fs"), 1);
26790
27973
  var import_node_os12 = __toESM(require("os"), 1);
26791
- var import_node_path21 = __toESM(require("path"), 1);
27974
+ var import_node_path27 = __toESM(require("path"), 1);
26792
27975
  init_claude_history();
26793
27976
  function readJsonlEntries(file) {
26794
- const raw = import_node_fs20.default.readFileSync(file, "utf8");
27977
+ const raw = import_node_fs25.default.readFileSync(file, "utf8");
26795
27978
  const out = [];
26796
27979
  for (const line of raw.split("\n")) {
26797
27980
  const t = line.trim();
@@ -26804,10 +27987,10 @@ function readJsonlEntries(file) {
26804
27987
  return out;
26805
27988
  }
26806
27989
  function forkSession(input) {
26807
- const baseDir = input.baseDir ?? import_node_path21.default.join(import_node_os12.default.homedir(), ".claude");
26808
- const projectDir = import_node_path21.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
26809
- const sourceFile = import_node_path21.default.join(projectDir, `${input.toolSessionId}.jsonl`);
26810
- if (!import_node_fs20.default.existsSync(sourceFile)) {
27990
+ const baseDir = input.baseDir ?? import_node_path27.default.join(import_node_os12.default.homedir(), ".claude");
27991
+ const projectDir = import_node_path27.default.join(baseDir, "projects", cwdToHashDir(input.cwd));
27992
+ const sourceFile = import_node_path27.default.join(projectDir, `${input.toolSessionId}.jsonl`);
27993
+ if (!import_node_fs25.default.existsSync(sourceFile)) {
26811
27994
  throw new Error(`fork: source transcript not found: ${sourceFile}`);
26812
27995
  }
26813
27996
  const entries = readJsonlEntries(sourceFile);
@@ -26837,9 +28020,9 @@ function forkSession(input) {
26837
28020
  }
26838
28021
  forkedLines.push(JSON.stringify(forked));
26839
28022
  }
26840
- const forkedFilePath = import_node_path21.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
26841
- import_node_fs20.default.mkdirSync(projectDir, { recursive: true });
26842
- import_node_fs20.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
28023
+ const forkedFilePath = import_node_path27.default.join(projectDir, `${forkedToolSessionId}.jsonl`);
28024
+ import_node_fs25.default.mkdirSync(projectDir, { recursive: true });
28025
+ import_node_fs25.default.writeFileSync(forkedFilePath, forkedLines.join("\n") + "\n", { mode: 384 });
26843
28026
  return { forkedToolSessionId, forkedFilePath };
26844
28027
  }
26845
28028
 
@@ -27160,9 +28343,9 @@ init_protocol();
27160
28343
 
27161
28344
  // src/workspace/git.ts
27162
28345
  var import_node_child_process6 = require("child_process");
27163
- var import_node_fs21 = __toESM(require("fs"), 1);
28346
+ var import_node_fs26 = __toESM(require("fs"), 1);
27164
28347
  var import_node_os13 = __toESM(require("os"), 1);
27165
- var import_node_path22 = __toESM(require("path"), 1);
28348
+ var import_node_path28 = __toESM(require("path"), 1);
27166
28349
  var import_node_util = require("util");
27167
28350
  var pexec = (0, import_node_util.promisify)(import_node_child_process6.execFile);
27168
28351
  function formatChildProcessError(err) {
@@ -27177,9 +28360,9 @@ function formatChildProcessError(err) {
27177
28360
  return e.message ?? "unknown error";
27178
28361
  }
27179
28362
  function normalizePath(p2) {
27180
- const resolved = import_node_path22.default.resolve(p2);
28363
+ const resolved = import_node_path28.default.resolve(p2);
27181
28364
  try {
27182
- return import_node_fs21.default.realpathSync(resolved);
28365
+ return import_node_fs26.default.realpathSync(resolved);
27183
28366
  } catch {
27184
28367
  return resolved;
27185
28368
  }
@@ -27280,13 +28463,13 @@ function flattenToDirName(branch) {
27280
28463
  }
27281
28464
  function encodeClaudeProjectDir(absPath) {
27282
28465
  if (!absPath || typeof absPath !== "string") return "";
27283
- let canonical = import_node_path22.default.resolve(absPath);
28466
+ let canonical = import_node_path28.default.resolve(absPath);
27284
28467
  try {
27285
- canonical = import_node_fs21.default.realpathSync(canonical);
28468
+ canonical = import_node_fs26.default.realpathSync(canonical);
27286
28469
  } catch {
27287
28470
  try {
27288
- const parent = import_node_fs21.default.realpathSync(import_node_path22.default.dirname(canonical));
27289
- canonical = import_node_path22.default.join(parent, import_node_path22.default.basename(canonical));
28471
+ const parent = import_node_fs26.default.realpathSync(import_node_path28.default.dirname(canonical));
28472
+ canonical = import_node_path28.default.join(parent, import_node_path28.default.basename(canonical));
27290
28473
  } catch {
27291
28474
  }
27292
28475
  }
@@ -27310,11 +28493,11 @@ async function createWorktree(input) {
27310
28493
  if (!isGitRoot) {
27311
28494
  throw new Error(`\u76EE\u5F55 ${cwd} \u4E0D\u662F git repo \u6839`);
27312
28495
  }
27313
- const parent = import_node_path22.default.dirname(import_node_path22.default.resolve(cwd));
27314
- if (parent === "/" || parent === import_node_path22.default.resolve(cwd)) {
28496
+ const parent = import_node_path28.default.dirname(import_node_path28.default.resolve(cwd));
28497
+ if (parent === "/" || parent === import_node_path28.default.resolve(cwd)) {
27315
28498
  throw new Error("repo \u5728\u78C1\u76D8\u6839\u76EE\u5F55\uFF0C\u65E0\u6CD5\u5728\u540C\u7EA7\u521B\u5EFA worktree");
27316
28499
  }
27317
- const worktreeRoot = import_node_path22.default.join(parent, dirName);
28500
+ const worktreeRoot = import_node_path28.default.join(parent, dirName);
27318
28501
  try {
27319
28502
  await pexec("git", ["-C", cwd, "fetch", "origin", baseBranch, "--no-tags"], {
27320
28503
  timeout: 3e4
@@ -27333,7 +28516,7 @@ async function createWorktree(input) {
27333
28516
  const msg = err.message;
27334
28517
  if (msg.startsWith("\u5206\u652F ")) throw err;
27335
28518
  }
27336
- if (import_node_fs21.default.existsSync(worktreeRoot)) {
28519
+ if (import_node_fs26.default.existsSync(worktreeRoot)) {
27337
28520
  throw new Error(`\u76EE\u5F55 ${worktreeRoot} \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E00\u4E2A label \u6216\u6E05\u7406\u540E\u91CD\u8BD5`);
27338
28521
  }
27339
28522
  try {
@@ -27361,8 +28544,8 @@ async function removeWorktree(input) {
27361
28544
  );
27362
28545
  const gitCommonDir = stdout.trim();
27363
28546
  if (!gitCommonDir) throw new Error("empty git-common-dir");
27364
- const absGitCommon = import_node_path22.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path22.default.resolve(worktreeRoot, gitCommonDir);
27365
- repoRoot = import_node_path22.default.dirname(absGitCommon);
28547
+ const absGitCommon = import_node_path28.default.isAbsolute(gitCommonDir) ? gitCommonDir : import_node_path28.default.resolve(worktreeRoot, gitCommonDir);
28548
+ repoRoot = import_node_path28.default.dirname(absGitCommon);
27366
28549
  } catch {
27367
28550
  repoRoot = null;
27368
28551
  }
@@ -27374,7 +28557,7 @@ async function removeWorktree(input) {
27374
28557
  } catch (err) {
27375
28558
  const stderr = err.stderr ?? "";
27376
28559
  const lower = stderr.toLowerCase();
27377
- const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs21.default.existsSync(worktreeRoot);
28560
+ const vanished = lower.includes("not a working tree") || lower.includes("is not a working tree") || !import_node_fs26.default.existsSync(worktreeRoot);
27378
28561
  if (!vanished) {
27379
28562
  throw new Error(`\u6E05\u7406 worktree \u5931\u8D25\uFF1A${formatChildProcessError(err)}`);
27380
28563
  }
@@ -27393,10 +28576,10 @@ async function removeWorktree(input) {
27393
28576
  try {
27394
28577
  const encoded = encodeClaudeProjectDir(worktreeRoot);
27395
28578
  if (encoded) {
27396
- const projectsRoot = import_node_path22.default.join(import_node_os13.default.homedir(), ".claude", "projects");
27397
- const target = import_node_path22.default.resolve(projectsRoot, encoded);
27398
- if (target.startsWith(projectsRoot + import_node_path22.default.sep) && target !== projectsRoot) {
27399
- import_node_fs21.default.rmSync(target, { recursive: true, force: true });
28579
+ const projectsRoot = import_node_path28.default.join(import_node_os13.default.homedir(), ".claude", "projects");
28580
+ const target = import_node_path28.default.resolve(projectsRoot, encoded);
28581
+ if (target.startsWith(projectsRoot + import_node_path28.default.sep) && target !== projectsRoot) {
28582
+ import_node_fs26.default.rmSync(target, { recursive: true, force: true });
27400
28583
  }
27401
28584
  }
27402
28585
  } catch {
@@ -27475,7 +28658,7 @@ init_protocol();
27475
28658
  var version = "0.2.6".length > 0 ? "0.2.6" : "dev";
27476
28659
 
27477
28660
  // src/handlers/meta.ts
27478
- function buildReadyFrame(deps) {
28661
+ function buildReadyFrame(deps, client) {
27479
28662
  const info = deps.manager.info();
27480
28663
  const tools = [];
27481
28664
  for (const id of listRegistered()) {
@@ -27487,6 +28670,14 @@ function buildReadyFrame(deps) {
27487
28670
  }
27488
28671
  }
27489
28672
  const tunnelUrl = deps.getTunnelUrl ? deps.getTunnelUrl() : null;
28673
+ const fileSharing = {};
28674
+ const httpBaseUrl = deps.getHttpBaseUrl ? deps.getHttpBaseUrl() : null;
28675
+ if (httpBaseUrl) {
28676
+ fileSharing.tokenRole = "owner";
28677
+ fileSharing.isLoopback = isLoopbackAddr(client?.remoteAddress);
28678
+ fileSharing.httpBaseUrl = httpBaseUrl;
28679
+ if (deps.httpToken) fileSharing.httpToken = deps.httpToken;
28680
+ }
27490
28681
  return {
27491
28682
  version,
27492
28683
  protocolVersion: PROTOCOL_VERSION,
@@ -27495,7 +28686,8 @@ function buildReadyFrame(deps) {
27495
28686
  tools,
27496
28687
  runningSessions: info.runningSessions,
27497
28688
  tunnelUrl,
27498
- mode: deps.mode
28689
+ mode: deps.mode,
28690
+ ...fileSharing
27499
28691
  };
27500
28692
  }
27501
28693
  function buildMetaHandlers(deps) {
@@ -27606,6 +28798,200 @@ function buildPersonaHandlers(deps) {
27606
28798
  };
27607
28799
  }
27608
28800
 
28801
+ // src/handlers/attachment.ts
28802
+ init_protocol();
28803
+ init_protocol();
28804
+ function buildAttachmentHandlers(deps) {
28805
+ const outboxCreate = async (frame) => {
28806
+ const parsed = AttachmentOutboxCreateArgs.safeParse(frame);
28807
+ if (!parsed.success) {
28808
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28809
+ }
28810
+ const args = parsed.data;
28811
+ if (args.personaId && args.sessionId || !args.personaId && !args.sessionId) {
28812
+ throw new ClawdError(
28813
+ ERROR_CODES.VALIDATION_ERROR,
28814
+ "outboxCreate requires exactly one of personaId / sessionId"
28815
+ );
28816
+ }
28817
+ const scopeRef = deps.getPersonaScopeForRequest(args);
28818
+ if (!scopeRef) {
28819
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "invalid scope");
28820
+ }
28821
+ const name = args.absPath.split("/").pop() || args.absPath;
28822
+ const entry = deps.outboxStore.mint({
28823
+ scopeRef,
28824
+ absPath: args.absPath,
28825
+ name,
28826
+ ttlSeconds: args.ttlSeconds,
28827
+ oneShot: args.oneShot,
28828
+ scope: args.scope
28829
+ });
28830
+ const httpBaseUrl = deps.getHttpBaseUrl();
28831
+ const urlPath = scopeRef.kind === "persona" ? `/persona/${encodeURIComponent(scopeRef.personaId)}/outbox/${entry.capToken}` : `/outbox/${entry.capToken}`;
28832
+ const url = httpBaseUrl ? `${httpBaseUrl}${urlPath}` : urlPath;
28833
+ return {
28834
+ response: {
28835
+ type: "attachment.outboxCreate",
28836
+ capToken: entry.capToken,
28837
+ url,
28838
+ expiresAt: entry.expiresAt
28839
+ }
28840
+ };
28841
+ };
28842
+ const outboxRevoke = async (frame) => {
28843
+ const parsed = AttachmentOutboxRevokeArgs.safeParse(frame);
28844
+ if (!parsed.success) {
28845
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28846
+ }
28847
+ const ok = deps.outboxStore.revoke(parsed.data.capToken);
28848
+ if (!ok) {
28849
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `capToken not found / already revoked`);
28850
+ }
28851
+ return { response: { type: "attachment.outboxRevoke", revoked: true } };
28852
+ };
28853
+ const outboxList = async (frame) => {
28854
+ const parsed = AttachmentOutboxListArgs.safeParse(frame);
28855
+ if (!parsed.success) {
28856
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28857
+ }
28858
+ const entries = parsed.data.personaId ? deps.outboxStore.listByPersona(parsed.data.personaId) : deps.outboxStore.list();
28859
+ return {
28860
+ response: { type: "attachment.outboxList", entries }
28861
+ };
28862
+ };
28863
+ const mountAdd = async (frame) => {
28864
+ if (!deps.mountStore) {
28865
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
28866
+ }
28867
+ const parsed = AttachmentMountAddArgs.safeParse(frame);
28868
+ if (!parsed.success) {
28869
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28870
+ }
28871
+ const args = parsed.data;
28872
+ try {
28873
+ const entry = deps.mountStore.add(args.personaId, {
28874
+ absPath: args.absPath,
28875
+ mode: args.mode
28876
+ });
28877
+ return {
28878
+ response: {
28879
+ type: "attachment.mountAdd",
28880
+ entry,
28881
+ sandboxRestartNeeded: true
28882
+ }
28883
+ };
28884
+ } catch (err) {
28885
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, err.message);
28886
+ }
28887
+ };
28888
+ const mountRemove = async (frame) => {
28889
+ if (!deps.mountStore) {
28890
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
28891
+ }
28892
+ const parsed = AttachmentMountRemoveArgs.safeParse(frame);
28893
+ if (!parsed.success) {
28894
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28895
+ }
28896
+ const ok = deps.mountStore.remove(parsed.data.personaId, parsed.data.basename);
28897
+ if (!ok) {
28898
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "mount not found");
28899
+ }
28900
+ return { response: { type: "attachment.mountRemove", removed: true } };
28901
+ };
28902
+ const mountList = async (frame) => {
28903
+ if (!deps.mountStore) {
28904
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "mountStore not wired");
28905
+ }
28906
+ const parsed = AttachmentMountListArgs.safeParse(frame);
28907
+ if (!parsed.success) {
28908
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28909
+ }
28910
+ const entries = deps.mountStore.list(parsed.data.personaId);
28911
+ return { response: { type: "attachment.mountList", entries } };
28912
+ };
28913
+ const groupAdd = async (frame) => {
28914
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28915
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28916
+ }
28917
+ const parsed = AttachmentGroupAddArgs.safeParse(frame);
28918
+ if (!parsed.success) {
28919
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28920
+ }
28921
+ const args = parsed.data;
28922
+ const scope = deps.getSessionScope(args.sessionId);
28923
+ if (!scope) {
28924
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `session ${args.sessionId} not found`);
28925
+ }
28926
+ const size = 0;
28927
+ const entry = deps.groupFileStore.upsert(scope, args.sessionId, {
28928
+ relPath: args.relPath,
28929
+ from: "owner",
28930
+ label: args.label,
28931
+ size,
28932
+ mime: lookupMime(args.relPath)
28933
+ });
28934
+ return { response: { type: "attachment.groupAdd", entry } };
28935
+ };
28936
+ const groupRemove = async (frame) => {
28937
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28938
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28939
+ }
28940
+ const parsed = AttachmentGroupRemoveArgs.safeParse(frame);
28941
+ if (!parsed.success) {
28942
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28943
+ }
28944
+ const scope = deps.getSessionScope(parsed.data.sessionId);
28945
+ if (!scope) {
28946
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, "session not found");
28947
+ }
28948
+ const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
28949
+ const target = entries.find((e) => e.relPath === parsed.data.relPath);
28950
+ if (target?.from === "owner") {
28951
+ deps.groupFileStore.remove(scope, parsed.data.sessionId, parsed.data.relPath);
28952
+ } else {
28953
+ deps.groupFileStore.markStale(scope, parsed.data.sessionId, parsed.data.relPath);
28954
+ }
28955
+ return { response: { type: "attachment.groupRemove", removed: true } };
28956
+ };
28957
+ const groupList = async (frame) => {
28958
+ if (!deps.groupFileStore || !deps.getSessionScope) {
28959
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28960
+ }
28961
+ const parsed = AttachmentGroupListArgs.safeParse(frame);
28962
+ if (!parsed.success) {
28963
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28964
+ }
28965
+ const scope = deps.getSessionScope(parsed.data.sessionId);
28966
+ if (!scope) return { response: { type: "attachment.groupList", entries: [] } };
28967
+ const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
28968
+ return { response: { type: "attachment.groupList", entries } };
28969
+ };
28970
+ const groupListPersona = async (frame) => {
28971
+ if (!deps.groupFileStore) {
28972
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28973
+ }
28974
+ const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
28975
+ if (!parsed.success) {
28976
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28977
+ }
28978
+ const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
28979
+ return { response: { type: "attachment.groupListPersona", perSession } };
28980
+ };
28981
+ return {
28982
+ "attachment.outboxCreate": outboxCreate,
28983
+ "attachment.outboxRevoke": outboxRevoke,
28984
+ "attachment.outboxList": outboxList,
28985
+ "attachment.mountAdd": mountAdd,
28986
+ "attachment.mountRemove": mountRemove,
28987
+ "attachment.mountList": mountList,
28988
+ "attachment.groupAdd": groupAdd,
28989
+ "attachment.groupRemove": groupRemove,
28990
+ "attachment.groupList": groupList,
28991
+ "attachment.groupListPersona": groupListPersona
28992
+ };
28993
+ }
28994
+
27609
28995
  // src/handlers/index.ts
27610
28996
  function buildMethodHandlers(deps) {
27611
28997
  return {
@@ -27621,7 +29007,8 @@ function buildMethodHandlers(deps) {
27621
29007
  personaRegistry: deps.personaRegistry,
27622
29008
  sessionManager: deps.manager,
27623
29009
  personaBoundHandler: deps.personaBoundHandler
27624
- })
29010
+ }),
29011
+ ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27625
29012
  };
27626
29013
  }
27627
29014
 
@@ -27629,7 +29016,7 @@ function buildMethodHandlers(deps) {
27629
29016
  async function startDaemon(config) {
27630
29017
  const logger = createLogger({
27631
29018
  level: config.logLevel,
27632
- file: import_node_path23.default.join(config.dataDir, "clawd.log")
29019
+ file: import_node_path29.default.join(config.dataDir, "clawd.log")
27633
29020
  });
27634
29021
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
27635
29022
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -27661,7 +29048,7 @@ async function startDaemon(config) {
27661
29048
  const agents = new AgentsScanner();
27662
29049
  const history = new ClaudeHistoryReader();
27663
29050
  let transport = null;
27664
- const personaStore = new PersonaStore(import_node_path23.default.join(config.dataDir, "personas"));
29051
+ const personaStore = new PersonaStore(import_node_path29.default.join(config.dataDir, "personas"));
27665
29052
  const defaultsRoot = findDefaultsRoot();
27666
29053
  if (defaultsRoot) {
27667
29054
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
@@ -27669,13 +29056,18 @@ async function startDaemon(config) {
27669
29056
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
27670
29057
  }
27671
29058
  const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
29059
+ const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
29060
+ const outboxStore = new OutboxStore({ dataDir: config.dataDir, logger });
29061
+ const mountStore = new MountStore({
29062
+ personaDirPath: (pid) => personaStore.personaDirPath(pid)
29063
+ });
27672
29064
  const manager = new SessionManager({
27673
29065
  store,
27674
29066
  logger,
27675
29067
  getAdapter,
27676
29068
  historyReader: history,
27677
29069
  dataDir: config.dataDir,
27678
- personaRoot: import_node_path23.default.join(config.dataDir, "personas"),
29070
+ personaRoot: import_node_path29.default.join(config.dataDir, "personas"),
27679
29071
  personaStore,
27680
29072
  ownerDisplayName,
27681
29073
  mode: config.mode,
@@ -27692,6 +29084,38 @@ async function startDaemon(config) {
27692
29084
  return;
27693
29085
  }
27694
29086
  transport?.broadcastToSession(sid, frame);
29087
+ },
29088
+ // file-sharing (spec §6 PR 3):runner 检测到成功 file-edit tool_result 时,
29089
+ // 闭包 stat + mime 写入群清单。stat 失败不阻塞主流程(log warn + 跳过本条),
29090
+ // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
29091
+ attachmentGroup: {
29092
+ onFileEdit: (input) => {
29093
+ const absPath = import_node_path29.default.isAbsolute(input.relPath) ? input.relPath : import_node_path29.default.join(input.cwd, input.relPath);
29094
+ let size = 0;
29095
+ try {
29096
+ size = import_node_fs27.default.statSync(absPath).size;
29097
+ } catch (err) {
29098
+ logger.warn("attachment.onFileEdit stat failed", {
29099
+ sessionId: input.sessionId,
29100
+ absPath,
29101
+ err: err.message
29102
+ });
29103
+ }
29104
+ try {
29105
+ groupFileStore.upsert(input.scope, input.sessionId, {
29106
+ relPath: input.relPath,
29107
+ from: "agent",
29108
+ size,
29109
+ mime: lookupMime(input.relPath)
29110
+ });
29111
+ } catch (err) {
29112
+ logger.warn("attachment.onFileEdit upsert failed", {
29113
+ sessionId: input.sessionId,
29114
+ relPath: input.relPath,
29115
+ err: err.message
29116
+ });
29117
+ }
29118
+ }
27695
29119
  }
27696
29120
  });
27697
29121
  const observer = new SessionObserver({
@@ -27737,6 +29161,12 @@ async function startDaemon(config) {
27737
29161
  sessionManager: manager
27738
29162
  });
27739
29163
  let currentTunnelUrl = null;
29164
+ const getHttpBaseUrl = () => {
29165
+ if (currentTunnelUrl) {
29166
+ return currentTunnelUrl.replace(/^wss:/i, "https:").replace(/^ws:/i, "http:");
29167
+ }
29168
+ return `http://${config.host}:${config.port}`;
29169
+ };
27740
29170
  const personaBoundHandler = new PersonaBoundHandler({
27741
29171
  registry: personaRegistry,
27742
29172
  personaManager,
@@ -27762,22 +29192,73 @@ async function startDaemon(config) {
27762
29192
  getTunnelUrl: () => currentTunnelUrl,
27763
29193
  // ready / info 帧的 mode = daemon CC spawn 模式('sdk' | 'tui')。UI 据此挂 XtermPanel +
27764
29194
  // 订阅 session:pty / session:control;业务帧名两种 mode 完全一致,UI 业务订阅代码不变
27765
- mode: config.mode
29195
+ mode: config.mode,
29196
+ // file-sharing (spec §8):ready / info 帧把 httpBaseUrl + httpToken 下发给 UI。
29197
+ // PR 2 阶段 httpToken 复用 owner WS token;noAuth 模式下为 null(UI 看到无 httpToken
29198
+ // 时禁用文件 GET/POST,保持 1.0 行为兼容)。
29199
+ getHttpBaseUrl,
29200
+ httpToken: resolvedAuthToken,
29201
+ // file-sharing attachment.* RPC(spec §5 PR 6+)。getPersonaScopeForRequest 把 RPC
29202
+ // args 里的 personaId/sessionId 翻译成 scopeRef 给 OutboxStore;mountAdd/groupAdd
29203
+ // 后续 PR 复用同一份装配。
29204
+ attachment: {
29205
+ outboxStore,
29206
+ mountStore,
29207
+ groupFileStore,
29208
+ personaDirPath: (pid) => personaStore.personaDirPath(pid),
29209
+ getHttpBaseUrl,
29210
+ getPersonaScopeForRequest: (args) => {
29211
+ if (args.personaId) return { kind: "persona", personaId: args.personaId };
29212
+ if (args.sessionId) return { kind: "session", sessionId: args.sessionId };
29213
+ return null;
29214
+ },
29215
+ // group RPC:根据 sessionId 反查 scope;owner-mode persona session 走
29216
+ // 'persona/<pid>/owner',default 走 'default'。PR 9 UI Drawer 用。
29217
+ getSessionScope: (sessionId) => {
29218
+ const file = store.read(sessionId);
29219
+ if (!file) return null;
29220
+ if (file.ownerPersonaId) {
29221
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
29222
+ }
29223
+ return { kind: "default" };
29224
+ }
29225
+ }
29226
+ });
29227
+ const authResolver = new AuthContextResolver({
29228
+ ownerToken: resolvedAuthToken,
29229
+ personaRegistry
29230
+ });
29231
+ const httpRouter = createHttpRouter({
29232
+ authResolver,
29233
+ daemonVersion: version,
29234
+ logger,
29235
+ personaStore,
29236
+ groupFileStore,
29237
+ sessionStore: store,
29238
+ outboxStore
27766
29239
  });
27767
29240
  wsServer = new LocalWsServer({
27768
29241
  host: config.host,
27769
29242
  port: config.port,
27770
29243
  logger,
27771
- readyFrameBuilder: () => buildReadyFrame({
27772
- manager,
27773
- getAdapter,
27774
- getTunnelUrl: () => currentTunnelUrl,
27775
- // ready mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
27776
- mode: config.mode
27777
- }),
29244
+ readyFrameBuilder: (ctx) => buildReadyFrame(
29245
+ {
29246
+ manager,
29247
+ getAdapter,
29248
+ getTunnelUrl: () => currentTunnelUrl,
29249
+ // ready 帧 mode = daemon CC spawn 模式('sdk' | 'tui');UI 用它挂 XtermPanel
29250
+ mode: config.mode,
29251
+ // file-sharing 字段:httpBaseUrl 跟 tunnel 状态走;httpToken 复用 owner WS token
29252
+ getHttpBaseUrl,
29253
+ httpToken: resolvedAuthToken
29254
+ },
29255
+ ctx
29256
+ ),
27778
29257
  protocolVersion: PROTOCOL_VERSION,
27779
29258
  authGate: authGate ?? void 0,
27780
29259
  personaBoundHandler,
29260
+ // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
29261
+ httpRequestHandler: httpRouter,
27781
29262
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
27782
29263
  // daemon 是 pendingQuestions 的唯一 source of truth;新 client 接入 / 刷新页面时
27783
29264
  // 把当前所有未决 question 以 session:question 帧定向回放,让 UI 不再误显示 Ended。
@@ -27893,8 +29374,8 @@ async function startDaemon(config) {
27893
29374
  const lines = [
27894
29375
  `Tunnel: ${r.url}`,
27895
29376
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
27896
- `Frpc config: ${import_node_path23.default.join(config.dataDir, "frpc.toml")}`,
27897
- `Frpc log: ${import_node_path23.default.join(config.dataDir, "frpc.log")}`
29377
+ `Frpc config: ${import_node_path29.default.join(config.dataDir, "frpc.toml")}`,
29378
+ `Frpc log: ${import_node_path29.default.join(config.dataDir, "frpc.log")}`
27898
29379
  ];
27899
29380
  const width = Math.max(...lines.map((l) => l.length));
27900
29381
  const bar = "\u2550".repeat(width + 4);
@@ -27907,8 +29388,8 @@ ${bar}
27907
29388
 
27908
29389
  `);
27909
29390
  try {
27910
- const connectPath = import_node_path23.default.join(config.dataDir, "connect.txt");
27911
- import_node_fs22.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
29391
+ const connectPath = import_node_path29.default.join(config.dataDir, "connect.txt");
29392
+ import_node_fs27.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
27912
29393
  } catch {
27913
29394
  }
27914
29395
  } catch (err) {