@clawos-dev/clawd 0.2.71 → 0.2.72-beta.139.d7294dd

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 +2385 -763
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -87,15 +87,16 @@ var init_methods = __esm({
87
87
  "git:branches",
88
88
  "capabilities:get",
89
89
  // ---- persona:* (persona 管理) ----
90
+ // persona:list 响应 shape 不分流(统一返 PersonaFile[]),但 daemon handler 按
91
+ // ConnectionContext 投影不同子集:
92
+ // owner ctx → 返全量 personas(含 private)
93
+ // guest ctx → persona.public === true ∧ assertGrant(ctx.grants, {persona,p.id}, 'read')
94
+ // method-grants.ts: persona:list 是 CAPABILITY_SCOPED;其它 persona:* 仍 ADMIN_ANY。
90
95
  "persona:create",
91
96
  "persona:list",
92
97
  "persona:get",
93
98
  "persona:update",
94
99
  "persona:delete",
95
- // token 颁发 / 吊销:file-sharing HTTP Bearer 鉴权用(PersonaRegistry.findByToken
96
- // 反查 personaId / label)。owner UI 在 PersonaSettingsDrawer "分享 Token" section 管理。
97
- "persona:issueToken",
98
- "persona:revokeToken",
99
100
  // ---- session:pty 双向透传(仅 CLAWD_CC_MODE=tui 才生效,对应 session:pty push 帧的上行) ----
100
101
  // session:pty:input — UI 把用户键盘字节(base64 UTF-8)发给 daemon,daemon 直写 pty stdin。
101
102
  // 高频帧(每次按键 1 帧),不入 reducer / 不广播。response 是 fire-and-forget 风格 ack。
@@ -106,14 +107,45 @@ var init_methods = __esm({
106
107
  // ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
107
108
  // 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
108
109
  // (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
109
- // 全部管理类 RPC handler 入口 requireOwnerpersonal token 调任意一个都 403);
110
- // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,签名验证),不在白名单内。
110
+ // 全部管理类 RPC dispatcher grant 限 ownercapability admin-only);
111
+ // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,HMAC 签名自包含,不在白名单内)。
111
112
  "attachment.signUrl",
112
113
  "attachment.groupAdd",
113
114
  "attachment.groupRemove",
114
115
  "attachment.groupList",
115
- // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
116
- "attachment.groupListPersona",
116
+ // ---- capability:* (capability platform 鉴权底座) ----
117
+ // owner 颁发 / 列出 / 删除给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
118
+ // 在 daemon 端固定为 `{ resource: '*', action: 'admin' }`,owner 自动满足)。
119
+ // 颁发后 daemon 推 'capability:tokenIssued' 帧;删除推 'capability:tokenDeleted' 帧。
120
+ // 删除是 hard delete:CapabilityStore 物理移除 + 关该 cap 的所有活跃 ws + rm
121
+ // personas/<pid>/.clawd/sessions/guests/<capId>/ guest sessions 目录。
122
+ "capability:issue",
123
+ "capability:list",
124
+ "capability:delete",
125
+ // bidirectional cap 视角 B 2026-05-23: owner 主动告诉 daemon "我颁的某 cap 给了谁"
126
+ // (UI 在 accept dialog 4 步流程里调; 等价于 recordPeerHello 但不经 ws auth path).
127
+ // 否则 B 端颁回的 cap 因为 A 不连 B → peerOwnerId 永不回写 → useContacts 派生 pending.
128
+ "capability:bindPeer",
129
+ // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
130
+ // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
131
+ // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
132
+ // 任何 principal (owner/guest) 可调 (Phase 4).
133
+ "inbox:list",
134
+ "inbox:markRead",
135
+ "inbox:postMessage",
136
+ // ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
137
+ // owner 入口: list / add / remove
138
+ // guest 端: autoAttach (B 接 A 的 token 后通过 cap channel ws 推 cap 给 A)
139
+ // 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
140
+ "received-capability:list",
141
+ "received-capability:add",
142
+ "received-capability:remove",
143
+ "received-capability:autoAttach",
144
+ // ---- whoami (v2 capability platform) ----
145
+ // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
146
+ // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
147
+ // 的 preview 一次性临时 client 用它判定身份和可用 persona。
148
+ "whoami",
117
149
  "info",
118
150
  "ping"
119
151
  ];
@@ -605,8 +637,8 @@ var init_parseUtil = __esm({
605
637
  init_errors2();
606
638
  init_en();
607
639
  makeIssue = (params) => {
608
- const { data, path: path32, errorMaps, issueData } = params;
609
- const fullPath = [...path32, ...issueData.path || []];
640
+ const { data, path: path38, errorMaps, issueData } = params;
641
+ const fullPath = [...path38, ...issueData.path || []];
610
642
  const fullIssue = {
611
643
  ...issueData,
612
644
  path: fullPath
@@ -917,11 +949,11 @@ var init_types = __esm({
917
949
  init_parseUtil();
918
950
  init_util();
919
951
  ParseInputLazyPath = class {
920
- constructor(parent, value, path32, key) {
952
+ constructor(parent, value, path38, key) {
921
953
  this._cachedPath = [];
922
954
  this.parent = parent;
923
955
  this.data = value;
924
- this._path = path32;
956
+ this._path = path38;
925
957
  this._key = key;
926
958
  }
927
959
  get path() {
@@ -4305,12 +4337,12 @@ var init_zod = __esm({
4305
4337
  });
4306
4338
 
4307
4339
  // ../protocol/src/attachment-schemas.ts
4308
- var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4340
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
4309
4341
  var init_attachment_schemas = __esm({
4310
4342
  "../protocol/src/attachment-schemas.ts"() {
4311
4343
  "use strict";
4312
4344
  init_zod();
4313
- TOKEN_ROLES = ["owner", "personal"];
4345
+ TOKEN_ROLES = ["owner"];
4314
4346
  GROUP_FILE_SOURCES = ["agent", "owner"];
4315
4347
  GroupFileEntrySchema = external_exports.object({
4316
4348
  /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
@@ -4331,11 +4363,9 @@ var init_attachment_schemas = __esm({
4331
4363
  stale: external_exports.boolean().optional()
4332
4364
  });
4333
4365
  AttachmentSignUrlArgs = external_exports.object({
4334
- /** 群文件所属的 session */
4335
- sessionId: external_exports.string().min(1),
4336
- /** 相对 session.cwd 的路径;允许传绝对路径,daemon 端会归一化(必须在 cwd 内) */
4337
- relPath: external_exports.string().min(1),
4338
- /** TTL 秒数;缺省 24h;null 走永久 URL(不带 exp 字段) */
4366
+ /** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
4367
+ absPath: external_exports.string().min(1),
4368
+ /** TTL 秒数;缺省 24h;'never' null 不带 exp 字段(永久有效) */
4339
4369
  ttlSeconds: external_exports.number().int().positive().nullable().optional()
4340
4370
  });
4341
4371
  AttachmentSignUrlResponseSchema = external_exports.object({
@@ -4366,31 +4396,15 @@ var init_attachment_schemas = __esm({
4366
4396
  AttachmentGroupListResponseSchema = external_exports.object({
4367
4397
  entries: external_exports.array(GroupFileEntrySchema)
4368
4398
  });
4369
- AttachmentGroupListPersonaArgs = external_exports.object({
4370
- personaId: external_exports.string().min(1)
4371
- });
4372
- AttachmentGroupListPersonaResponseSchema = external_exports.object({
4373
- perSession: external_exports.array(
4374
- external_exports.object({
4375
- sessionId: external_exports.string().min(1),
4376
- entries: external_exports.array(GroupFileEntrySchema)
4377
- })
4378
- )
4379
- });
4380
4399
  }
4381
4400
  });
4382
4401
 
4383
4402
  // ../protocol/src/persona-schemas.ts
4384
- var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema;
4403
+ var PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
4385
4404
  var init_persona_schemas = __esm({
4386
4405
  "../protocol/src/persona-schemas.ts"() {
4387
4406
  "use strict";
4388
4407
  init_zod();
4389
- PersonaTokenEntrySchema = external_exports.object({
4390
- label: external_exports.string(),
4391
- issuedAt: external_exports.number(),
4392
- revoked: external_exports.boolean()
4393
- });
4394
4408
  PersonaFileSchema = external_exports.object({
4395
4409
  personaId: external_exports.string(),
4396
4410
  label: external_exports.string(),
@@ -4399,13 +4413,6 @@ var init_persona_schemas = __esm({
4399
4413
  // 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
4400
4414
  // for forward compatibility, consumer fallbacks to default when key not found.
4401
4415
  iconKey: external_exports.string().optional(),
4402
- /**
4403
- * Persona token 列表:file-sharing HTTP Bearer 鉴权用。
4404
- * - 写:`PersonaManager.issueToken` / `revokeToken`
4405
- * - 读:`PersonaRegistry.findByToken` 反向查表(需 persona.public === true)
4406
- * - optional:兼容历史 persona.json 不含此字段的情形(旧 daemon 写文件时省略)
4407
- */
4408
- tokenMap: external_exports.record(external_exports.string(), PersonaTokenEntrySchema).optional(),
4409
4416
  createdAt: external_exports.number(),
4410
4417
  updatedAt: external_exports.number()
4411
4418
  }).strict();
@@ -4415,10 +4422,6 @@ var init_persona_schemas = __esm({
4415
4422
  // SKILL.md frontmatter `description:`;缺省时省略字段
4416
4423
  description: external_exports.string().optional()
4417
4424
  });
4418
- PersonaPluginSummarySchema = external_exports.object({
4419
- // settings.json 中 enabledPlugins 的 key,例如 'superpowers@claude-plugins-official'
4420
- id: external_exports.string().min(1)
4421
- });
4422
4425
  PersonaSandboxSettingsSchema = external_exports.object({
4423
4426
  permissions: external_exports.object({
4424
4427
  defaultMode: external_exports.string().optional()
@@ -4439,8 +4442,6 @@ var init_persona_schemas = __esm({
4439
4442
  personality: external_exports.string().optional(),
4440
4443
  // 仅 'persona:get' 携带;null = 文件不存在 / parse 失败;undefined = 不在该响应类型上
4441
4444
  skills: external_exports.array(PersonaSkillSummarySchema).optional(),
4442
- // 仅 'persona:get' 携带;空数组 = 没启用任何插件 / settings.json 不存在;undefined = 不在该响应类型上
4443
- plugins: external_exports.array(PersonaPluginSummarySchema).optional(),
4444
4445
  sandboxSettings: PersonaSandboxSettingsSchema.nullable().optional()
4445
4446
  });
4446
4447
  PersonaCreateArgsSchema = external_exports.object({
@@ -4464,19 +4465,11 @@ var init_persona_schemas = __esm({
4464
4465
  iconKey: external_exports.string().nullable().optional()
4465
4466
  }).strict()
4466
4467
  }).strict();
4467
- PersonaIssueTokenArgsSchema = external_exports.object({
4468
- personaId: external_exports.string().min(1),
4469
- label: external_exports.string().min(1)
4470
- });
4471
- PersonaRevokeTokenArgsSchema = external_exports.object({
4472
- personaId: external_exports.string().min(1),
4473
- token: external_exports.string().min(1)
4474
- });
4475
4468
  }
4476
4469
  });
4477
4470
 
4478
4471
  // ../protocol/src/schemas.ts
4479
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4472
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4480
4473
  var init_schemas = __esm({
4481
4474
  "../protocol/src/schemas.ts"() {
4482
4475
  "use strict";
@@ -4578,6 +4571,15 @@ var init_schemas = __esm({
4578
4571
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
4579
4572
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4580
4573
  chatId: external_exports.string().min(1).optional(),
4574
+ /**
4575
+ * 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
4576
+ * 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4577
+ * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4578
+ * 后者再通过 PersonAlias 表反查 personId 二级分组。
4579
+ *
4580
+ * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4581
+ */
4582
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4581
4583
  createdAt: external_exports.string().min(1),
4582
4584
  updatedAt: external_exports.string().min(1)
4583
4585
  });
@@ -4987,7 +4989,20 @@ var init_schemas = __esm({
4987
4989
  AuthRequestFrameSchema = external_exports.object({
4988
4990
  type: external_exports.literal("auth"),
4989
4991
  token: external_exports.string().min(1),
4990
- scheme: external_exports.literal("bearer").optional()
4992
+ scheme: external_exports.literal("bearer").optional(),
4993
+ /**
4994
+ * guest 端 ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
4995
+ * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
4996
+ * 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
4997
+ * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
4998
+ */
4999
+ selfPrincipalId: external_exports.string().min(1).optional(),
5000
+ /**
5001
+ * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5002
+ * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5003
+ * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5004
+ */
5005
+ selfDisplayName: external_exports.string().optional()
4991
5006
  });
4992
5007
  AuthOkFrameSchema = external_exports.object({
4993
5008
  type: external_exports.literal("auth:ok"),
@@ -5000,11 +5015,6 @@ var init_schemas = __esm({
5000
5015
  subdomain: external_exports.string().nullable(),
5001
5016
  url: external_exports.string().nullable()
5002
5017
  });
5003
- TunnelUnavailableEventSchema = external_exports.object({
5004
- type: external_exports.literal("tunnel:unavailable"),
5005
- reason: external_exports.string().min(1),
5006
- failedAt: external_exports.string().min(1)
5007
- });
5008
5018
  InfoRunningSessionSchema = external_exports.object({
5009
5019
  sessionId: external_exports.string().min(1),
5010
5020
  status: SessionStatusSchema,
@@ -5025,10 +5035,9 @@ var init_schemas = __esm({
5025
5035
  // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5026
5036
  // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5027
5037
  // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5028
- /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5038
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
5039
+ * 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
5029
5040
  tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5030
- /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5031
- tokenPersonaId: external_exports.string().min(1).optional(),
5032
5041
  /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5033
5042
  isLoopback: external_exports.boolean().optional(),
5034
5043
  /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
@@ -5055,6 +5064,194 @@ var init_persona_mode = __esm({
5055
5064
  }
5056
5065
  });
5057
5066
 
5067
+ // ../protocol/src/principal.ts
5068
+ function makeOwnerPrincipal(id, displayName) {
5069
+ return PrincipalSchema.parse({ id, kind: "owner", displayName });
5070
+ }
5071
+ var PrincipalKindSchema, PrincipalSchema;
5072
+ var init_principal = __esm({
5073
+ "../protocol/src/principal.ts"() {
5074
+ "use strict";
5075
+ init_zod();
5076
+ PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
5077
+ PrincipalSchema = external_exports.object({
5078
+ id: external_exports.string().min(1),
5079
+ kind: PrincipalKindSchema,
5080
+ displayName: external_exports.string()
5081
+ }).strict();
5082
+ }
5083
+ });
5084
+
5085
+ // ../protocol/src/capability.ts
5086
+ function stripSecretHash(cap) {
5087
+ const { secretHash: _hash, ...wire } = cap;
5088
+ return wire;
5089
+ }
5090
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema, CapabilityBindPeerArgsSchema;
5091
+ var init_capability = __esm({
5092
+ "../protocol/src/capability.ts"() {
5093
+ "use strict";
5094
+ init_zod();
5095
+ ResourceSchema = external_exports.discriminatedUnion("type", [
5096
+ external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
5097
+ external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
5098
+ external_exports.object({ type: external_exports.literal("*") }).strict()
5099
+ ]);
5100
+ ActionSchema = external_exports.enum(["read", "send", "admin"]);
5101
+ GrantSchema = external_exports.object({
5102
+ resource: ResourceSchema,
5103
+ actions: external_exports.array(ActionSchema).min(1)
5104
+ }).strict();
5105
+ CapabilitySchema = external_exports.object({
5106
+ id: external_exports.string().min(1),
5107
+ // sha256(token) hex 64
5108
+ secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
5109
+ displayName: external_exports.string(),
5110
+ // 空数组合法 = 纯访客(只能 DM)
5111
+ grants: external_exports.array(GrantSchema),
5112
+ issuedAt: external_exports.number().int().nonnegative(),
5113
+ expiresAt: external_exports.number().int().positive().optional(),
5114
+ maxUses: external_exports.number().int().positive().optional(),
5115
+ usedCount: external_exports.number().int().nonnegative(),
5116
+ /**
5117
+ * guest 在 auth 帧捎带 selfPrincipalId 时由 AuthGate.authenticate 回写;
5118
+ * 未被消费的 cap 此字段缺失。
5119
+ */
5120
+ peerOwnerId: external_exports.string().min(1).optional(),
5121
+ /** 首次 client hello 时间戳(与 peerOwnerId 同步写入) */
5122
+ firstUsedByPeerAt: external_exports.number().int().positive().optional(),
5123
+ /**
5124
+ * 对方 daemon owner 的 displayName,guest auth 帧 selfDisplayName 触发
5125
+ * recordPeerHello 时同步回写。视角 B (2026-05-23) UI 派生联系人列表用此字段
5126
+ * 显示对方名字(避免 fallback 到我颁 cap 时填的 displayName)。
5127
+ */
5128
+ peerDisplayName: external_exports.string().optional()
5129
+ }).strict();
5130
+ CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
5131
+ CapabilityErrorCodeSchema = external_exports.enum([
5132
+ "TOKEN_INVALID",
5133
+ "TOKEN_REVOKED",
5134
+ "TOKEN_EXPIRED",
5135
+ "TOKEN_EXHAUSTED"
5136
+ ]);
5137
+ WhoamiResponseSchema = external_exports.object({
5138
+ type: external_exports.literal("whoami:ok"),
5139
+ owner: external_exports.object({
5140
+ id: external_exports.string(),
5141
+ kind: external_exports.enum(["owner", "guest"]),
5142
+ displayName: external_exports.string()
5143
+ }).strict(),
5144
+ capability: CapabilityWireSchema,
5145
+ grantedPersonas: external_exports.array(
5146
+ external_exports.object({
5147
+ id: external_exports.string().min(1),
5148
+ displayName: external_exports.string()
5149
+ }).strict()
5150
+ )
5151
+ }).strict();
5152
+ CapabilityIssueArgsSchema = external_exports.object({
5153
+ displayName: external_exports.string().min(1),
5154
+ grants: external_exports.array(GrantSchema),
5155
+ expiresAt: external_exports.number().int().positive().optional(),
5156
+ maxUses: external_exports.number().int().positive().optional()
5157
+ }).strict();
5158
+ CapabilityBindPeerArgsSchema = external_exports.object({
5159
+ capabilityId: external_exports.string().min(1),
5160
+ peerOwnerId: external_exports.string().min(1),
5161
+ peerDisplayName: external_exports.string()
5162
+ }).strict();
5163
+ }
5164
+ });
5165
+
5166
+ // ../protocol/src/inbox.ts
5167
+ var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
5168
+ var init_inbox = __esm({
5169
+ "../protocol/src/inbox.ts"() {
5170
+ "use strict";
5171
+ init_zod();
5172
+ InboxMessageSchema = external_exports.object({
5173
+ id: external_exports.string().min(1),
5174
+ peerOwnerId: external_exports.string().min(1),
5175
+ senderPrincipalId: external_exports.string().min(1),
5176
+ text: external_exports.string().min(1),
5177
+ createdAt: external_exports.number().int().nonnegative(),
5178
+ readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5179
+ }).strict();
5180
+ InboxPostMessageArgsSchema = external_exports.object({
5181
+ peerOwnerId: external_exports.string().min(1).optional(),
5182
+ id: external_exports.string().min(1),
5183
+ text: external_exports.string().min(1),
5184
+ createdAt: external_exports.number().int().nonnegative()
5185
+ }).strict();
5186
+ InboxListArgsSchema = external_exports.object({
5187
+ peerOwnerId: external_exports.string().min(1),
5188
+ sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5189
+ }).strict();
5190
+ InboxMarkReadArgsSchema = external_exports.object({
5191
+ peerOwnerId: external_exports.string().min(1),
5192
+ upToCreatedAt: external_exports.number().int().nonnegative()
5193
+ }).strict();
5194
+ }
5195
+ });
5196
+
5197
+ // ../protocol/src/received-capability.ts
5198
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5199
+ var init_received_capability = __esm({
5200
+ "../protocol/src/received-capability.ts"() {
5201
+ "use strict";
5202
+ init_zod();
5203
+ init_capability();
5204
+ ReceivedCapabilitySchema = external_exports.object({
5205
+ ownerPrincipalId: external_exports.string().min(1),
5206
+ ownerDisplayName: external_exports.string(),
5207
+ remoteUrl: external_exports.string().min(1),
5208
+ capabilityId: external_exports.string().min(1),
5209
+ capabilityToken: external_exports.string().min(1),
5210
+ grants: external_exports.array(GrantSchema),
5211
+ receivedAt: external_exports.number().int().nonnegative()
5212
+ }).strict();
5213
+ ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5214
+ ReceivedCapabilityAddArgsSchema = external_exports.object({
5215
+ remoteUrl: external_exports.string().min(1),
5216
+ token: external_exports.string().min(1)
5217
+ }).strict();
5218
+ ReceivedCapabilityAddOkSchema = external_exports.object({
5219
+ type: external_exports.literal("received-capability:add:ok"),
5220
+ receivedCap: ReceivedCapabilityWireSchema
5221
+ }).strict();
5222
+ ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5223
+ ownerPrincipalId: external_exports.string().min(1)
5224
+ }).strict();
5225
+ ReceivedCapabilityRemoveOkSchema = external_exports.object({
5226
+ type: external_exports.literal("received-capability:remove:ok"),
5227
+ ownerPrincipalId: external_exports.string().min(1)
5228
+ }).strict();
5229
+ ReceivedCapabilityListOkSchema = external_exports.object({
5230
+ type: external_exports.literal("received-capability:list:ok"),
5231
+ receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5232
+ }).strict();
5233
+ ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5234
+ ownerPrincipalId: external_exports.string().min(1),
5235
+ ownerDisplayName: external_exports.string(),
5236
+ remoteUrl: external_exports.string().min(1),
5237
+ capabilityId: external_exports.string().min(1),
5238
+ token: external_exports.string().min(1),
5239
+ grants: external_exports.array(GrantSchema)
5240
+ }).strict();
5241
+ ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5242
+ type: external_exports.literal("received-capability:autoAttach:ok")
5243
+ }).strict();
5244
+ ReceivedCapabilityAddedFrameSchema = external_exports.object({
5245
+ type: external_exports.literal("received-capability:added"),
5246
+ receivedCap: ReceivedCapabilityWireSchema
5247
+ }).strict();
5248
+ ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5249
+ type: external_exports.literal("received-capability:removed"),
5250
+ ownerPrincipalId: external_exports.string().min(1)
5251
+ }).strict();
5252
+ }
5253
+ });
5254
+
5058
5255
  // ../protocol/src/runtime.ts
5059
5256
  var init_runtime = __esm({
5060
5257
  "../protocol/src/runtime.ts"() {
@@ -5067,6 +5264,10 @@ var init_runtime = __esm({
5067
5264
  init_persona_schemas();
5068
5265
  init_persona_mode();
5069
5266
  init_attachment_schemas();
5267
+ init_principal();
5268
+ init_capability();
5269
+ init_inbox();
5270
+ init_received_capability();
5070
5271
  }
5071
5272
  });
5072
5273
 
@@ -5341,8 +5542,8 @@ var require_req = __commonJS({
5341
5542
  if (req.originalUrl) {
5342
5543
  _req.url = req.originalUrl;
5343
5544
  } else {
5344
- const path32 = req.path;
5345
- _req.url = typeof path32 === "string" ? path32 : req.url ? req.url.path || req.url : void 0;
5545
+ const path38 = req.path;
5546
+ _req.url = typeof path38 === "string" ? path38 : req.url ? req.url.path || req.url : void 0;
5346
5547
  }
5347
5548
  if (req.query) {
5348
5549
  _req.query = req.query;
@@ -5507,14 +5708,14 @@ var require_redact = __commonJS({
5507
5708
  }
5508
5709
  return obj;
5509
5710
  }
5510
- function parsePath(path32) {
5711
+ function parsePath(path38) {
5511
5712
  const parts = [];
5512
5713
  let current = "";
5513
5714
  let inBrackets = false;
5514
5715
  let inQuotes = false;
5515
5716
  let quoteChar = "";
5516
- for (let i = 0; i < path32.length; i++) {
5517
- const char = path32[i];
5717
+ for (let i = 0; i < path38.length; i++) {
5718
+ const char = path38[i];
5518
5719
  if (!inBrackets && char === ".") {
5519
5720
  if (current) {
5520
5721
  parts.push(current);
@@ -5645,10 +5846,10 @@ var require_redact = __commonJS({
5645
5846
  return current;
5646
5847
  }
5647
5848
  function redactPaths(obj, paths, censor, remove = false) {
5648
- for (const path32 of paths) {
5649
- const parts = parsePath(path32);
5849
+ for (const path38 of paths) {
5850
+ const parts = parsePath(path38);
5650
5851
  if (parts.includes("*")) {
5651
- redactWildcardPath(obj, parts, censor, path32, remove);
5852
+ redactWildcardPath(obj, parts, censor, path38, remove);
5652
5853
  } else {
5653
5854
  if (remove) {
5654
5855
  removeKey(obj, parts);
@@ -5733,8 +5934,8 @@ var require_redact = __commonJS({
5733
5934
  }
5734
5935
  } else {
5735
5936
  if (afterWildcard.includes("*")) {
5736
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
5737
- const fullPath = [...pathArray.slice(0, pathLength), ...path32];
5937
+ const wrappedCensor = typeof censor === "function" ? (value, path38) => {
5938
+ const fullPath = [...pathArray.slice(0, pathLength), ...path38];
5738
5939
  return censor(value, fullPath);
5739
5940
  } : censor;
5740
5941
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5769,8 +5970,8 @@ var require_redact = __commonJS({
5769
5970
  return null;
5770
5971
  }
5771
5972
  const pathStructure = /* @__PURE__ */ new Map();
5772
- for (const path32 of pathsToClone) {
5773
- const parts = parsePath(path32);
5973
+ for (const path38 of pathsToClone) {
5974
+ const parts = parsePath(path38);
5774
5975
  let current = pathStructure;
5775
5976
  for (let i = 0; i < parts.length; i++) {
5776
5977
  const part = parts[i];
@@ -5822,24 +6023,24 @@ var require_redact = __commonJS({
5822
6023
  }
5823
6024
  return cloneSelectively(obj, pathStructure);
5824
6025
  }
5825
- function validatePath(path32) {
5826
- if (typeof path32 !== "string") {
6026
+ function validatePath(path38) {
6027
+ if (typeof path38 !== "string") {
5827
6028
  throw new Error("Paths must be (non-empty) strings");
5828
6029
  }
5829
- if (path32 === "") {
6030
+ if (path38 === "") {
5830
6031
  throw new Error("Invalid redaction path ()");
5831
6032
  }
5832
- if (path32.includes("..")) {
5833
- throw new Error(`Invalid redaction path (${path32})`);
6033
+ if (path38.includes("..")) {
6034
+ throw new Error(`Invalid redaction path (${path38})`);
5834
6035
  }
5835
- if (path32.includes(",")) {
5836
- throw new Error(`Invalid redaction path (${path32})`);
6036
+ if (path38.includes(",")) {
6037
+ throw new Error(`Invalid redaction path (${path38})`);
5837
6038
  }
5838
6039
  let bracketCount = 0;
5839
6040
  let inQuotes = false;
5840
6041
  let quoteChar = "";
5841
- for (let i = 0; i < path32.length; i++) {
5842
- const char = path32[i];
6042
+ for (let i = 0; i < path38.length; i++) {
6043
+ const char = path38[i];
5843
6044
  if ((char === '"' || char === "'") && bracketCount > 0) {
5844
6045
  if (!inQuotes) {
5845
6046
  inQuotes = true;
@@ -5853,20 +6054,20 @@ var require_redact = __commonJS({
5853
6054
  } else if (char === "]" && !inQuotes) {
5854
6055
  bracketCount--;
5855
6056
  if (bracketCount < 0) {
5856
- throw new Error(`Invalid redaction path (${path32})`);
6057
+ throw new Error(`Invalid redaction path (${path38})`);
5857
6058
  }
5858
6059
  }
5859
6060
  }
5860
6061
  if (bracketCount !== 0) {
5861
- throw new Error(`Invalid redaction path (${path32})`);
6062
+ throw new Error(`Invalid redaction path (${path38})`);
5862
6063
  }
5863
6064
  }
5864
6065
  function validatePaths(paths) {
5865
6066
  if (!Array.isArray(paths)) {
5866
6067
  throw new TypeError("paths must be an array");
5867
6068
  }
5868
- for (const path32 of paths) {
5869
- validatePath(path32);
6069
+ for (const path38 of paths) {
6070
+ validatePath(path38);
5870
6071
  }
5871
6072
  }
5872
6073
  function slowRedact(options = {}) {
@@ -6034,8 +6235,8 @@ var require_redaction = __commonJS({
6034
6235
  if (shape[k2] === null) {
6035
6236
  o[k2] = (value) => topCensor(value, [k2]);
6036
6237
  } else {
6037
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
6038
- return censor(value, [k2, ...path32]);
6238
+ const wrappedCensor = typeof censor === "function" ? (value, path38) => {
6239
+ return censor(value, [k2, ...path38]);
6039
6240
  } : censor;
6040
6241
  o[k2] = Redact({
6041
6242
  paths: shape[k2],
@@ -6253,10 +6454,10 @@ var require_atomic_sleep = __commonJS({
6253
6454
  var require_sonic_boom = __commonJS({
6254
6455
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6255
6456
  "use strict";
6256
- var fs28 = require("fs");
6457
+ var fs34 = require("fs");
6257
6458
  var EventEmitter2 = require("events");
6258
6459
  var inherits = require("util").inherits;
6259
- var path32 = require("path");
6460
+ var path38 = require("path");
6260
6461
  var sleep = require_atomic_sleep();
6261
6462
  var assert = require("assert");
6262
6463
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6310,20 +6511,20 @@ var require_sonic_boom = __commonJS({
6310
6511
  const mode = sonic.mode;
6311
6512
  if (sonic.sync) {
6312
6513
  try {
6313
- if (sonic.mkdir) fs28.mkdirSync(path32.dirname(file), { recursive: true });
6314
- const fd = fs28.openSync(file, flags, mode);
6514
+ if (sonic.mkdir) fs34.mkdirSync(path38.dirname(file), { recursive: true });
6515
+ const fd = fs34.openSync(file, flags, mode);
6315
6516
  fileOpened(null, fd);
6316
6517
  } catch (err) {
6317
6518
  fileOpened(err);
6318
6519
  throw err;
6319
6520
  }
6320
6521
  } else if (sonic.mkdir) {
6321
- fs28.mkdir(path32.dirname(file), { recursive: true }, (err) => {
6522
+ fs34.mkdir(path38.dirname(file), { recursive: true }, (err) => {
6322
6523
  if (err) return fileOpened(err);
6323
- fs28.open(file, flags, mode, fileOpened);
6524
+ fs34.open(file, flags, mode, fileOpened);
6324
6525
  });
6325
6526
  } else {
6326
- fs28.open(file, flags, mode, fileOpened);
6527
+ fs34.open(file, flags, mode, fileOpened);
6327
6528
  }
6328
6529
  }
6329
6530
  function SonicBoom(opts) {
@@ -6364,8 +6565,8 @@ var require_sonic_boom = __commonJS({
6364
6565
  this.flush = flushBuffer;
6365
6566
  this.flushSync = flushBufferSync;
6366
6567
  this._actualWrite = actualWriteBuffer;
6367
- fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
6368
- fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
6568
+ fsWriteSync = () => fs34.writeSync(this.fd, this._writingBuf);
6569
+ fsWrite = () => fs34.write(this.fd, this._writingBuf, this.release);
6369
6570
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6370
6571
  this._writingBuf = "";
6371
6572
  this.write = write;
@@ -6374,15 +6575,15 @@ var require_sonic_boom = __commonJS({
6374
6575
  this._actualWrite = actualWrite;
6375
6576
  fsWriteSync = () => {
6376
6577
  if (Buffer.isBuffer(this._writingBuf)) {
6377
- return fs28.writeSync(this.fd, this._writingBuf);
6578
+ return fs34.writeSync(this.fd, this._writingBuf);
6378
6579
  }
6379
- return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6580
+ return fs34.writeSync(this.fd, this._writingBuf, "utf8");
6380
6581
  };
6381
6582
  fsWrite = () => {
6382
6583
  if (Buffer.isBuffer(this._writingBuf)) {
6383
- return fs28.write(this.fd, this._writingBuf, this.release);
6584
+ return fs34.write(this.fd, this._writingBuf, this.release);
6384
6585
  }
6385
- return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6586
+ return fs34.write(this.fd, this._writingBuf, "utf8", this.release);
6386
6587
  };
6387
6588
  } else {
6388
6589
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6439,7 +6640,7 @@ var require_sonic_boom = __commonJS({
6439
6640
  }
6440
6641
  }
6441
6642
  if (this._fsync) {
6442
- fs28.fsyncSync(this.fd);
6643
+ fs34.fsyncSync(this.fd);
6443
6644
  }
6444
6645
  const len = this._len;
6445
6646
  if (this._reopening) {
@@ -6553,7 +6754,7 @@ var require_sonic_boom = __commonJS({
6553
6754
  const onDrain = () => {
6554
6755
  if (!this._fsync) {
6555
6756
  try {
6556
- fs28.fsync(this.fd, (err) => {
6757
+ fs34.fsync(this.fd, (err) => {
6557
6758
  this._flushPending = false;
6558
6759
  cb(err);
6559
6760
  });
@@ -6655,7 +6856,7 @@ var require_sonic_boom = __commonJS({
6655
6856
  const fd = this.fd;
6656
6857
  this.once("ready", () => {
6657
6858
  if (fd !== this.fd) {
6658
- fs28.close(fd, (err) => {
6859
+ fs34.close(fd, (err) => {
6659
6860
  if (err) {
6660
6861
  return this.emit("error", err);
6661
6862
  }
@@ -6704,7 +6905,7 @@ var require_sonic_boom = __commonJS({
6704
6905
  buf = this._bufs[0];
6705
6906
  }
6706
6907
  try {
6707
- const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
6908
+ const n = Buffer.isBuffer(buf) ? fs34.writeSync(this.fd, buf) : fs34.writeSync(this.fd, buf, "utf8");
6708
6909
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6709
6910
  buf = releasedBufObj.writingBuf;
6710
6911
  this._len = releasedBufObj.len;
@@ -6720,7 +6921,7 @@ var require_sonic_boom = __commonJS({
6720
6921
  }
6721
6922
  }
6722
6923
  try {
6723
- fs28.fsyncSync(this.fd);
6924
+ fs34.fsyncSync(this.fd);
6724
6925
  } catch {
6725
6926
  }
6726
6927
  }
@@ -6741,7 +6942,7 @@ var require_sonic_boom = __commonJS({
6741
6942
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6742
6943
  }
6743
6944
  try {
6744
- const n = fs28.writeSync(this.fd, buf);
6945
+ const n = fs34.writeSync(this.fd, buf);
6745
6946
  buf = buf.subarray(n);
6746
6947
  this._len = Math.max(this._len - n, 0);
6747
6948
  if (buf.length <= 0) {
@@ -6769,13 +6970,13 @@ var require_sonic_boom = __commonJS({
6769
6970
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6770
6971
  if (this.sync) {
6771
6972
  try {
6772
- const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
6973
+ const written = Buffer.isBuffer(this._writingBuf) ? fs34.writeSync(this.fd, this._writingBuf) : fs34.writeSync(this.fd, this._writingBuf, "utf8");
6773
6974
  release(null, written);
6774
6975
  } catch (err) {
6775
6976
  release(err);
6776
6977
  }
6777
6978
  } else {
6778
- fs28.write(this.fd, this._writingBuf, release);
6979
+ fs34.write(this.fd, this._writingBuf, release);
6779
6980
  }
6780
6981
  }
6781
6982
  function actualWriteBuffer() {
@@ -6784,7 +6985,7 @@ var require_sonic_boom = __commonJS({
6784
6985
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6785
6986
  if (this.sync) {
6786
6987
  try {
6787
- const written = fs28.writeSync(this.fd, this._writingBuf);
6988
+ const written = fs34.writeSync(this.fd, this._writingBuf);
6788
6989
  release(null, written);
6789
6990
  } catch (err) {
6790
6991
  release(err);
@@ -6793,7 +6994,7 @@ var require_sonic_boom = __commonJS({
6793
6994
  if (kCopyBuffer) {
6794
6995
  this._writingBuf = Buffer.from(this._writingBuf);
6795
6996
  }
6796
- fs28.write(this.fd, this._writingBuf, release);
6997
+ fs34.write(this.fd, this._writingBuf, release);
6797
6998
  }
6798
6999
  }
6799
7000
  function actualClose(sonic) {
@@ -6809,12 +7010,12 @@ var require_sonic_boom = __commonJS({
6809
7010
  sonic._lens = [];
6810
7011
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6811
7012
  try {
6812
- fs28.fsync(sonic.fd, closeWrapped);
7013
+ fs34.fsync(sonic.fd, closeWrapped);
6813
7014
  } catch {
6814
7015
  }
6815
7016
  function closeWrapped() {
6816
7017
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6817
- fs28.close(sonic.fd, done);
7018
+ fs34.close(sonic.fd, done);
6818
7019
  } else {
6819
7020
  done();
6820
7021
  }
@@ -7071,7 +7272,7 @@ var require_thread_stream = __commonJS({
7071
7272
  var { version: version2 } = require_package();
7072
7273
  var { EventEmitter: EventEmitter2 } = require("events");
7073
7274
  var { Worker } = require("worker_threads");
7074
- var { join: join4 } = require("path");
7275
+ var { join: join11 } = require("path");
7075
7276
  var { pathToFileURL } = require("url");
7076
7277
  var { wait } = require_wait();
7077
7278
  var {
@@ -7107,7 +7308,7 @@ var require_thread_stream = __commonJS({
7107
7308
  function createWorker(stream, opts) {
7108
7309
  const { filename, workerData } = opts;
7109
7310
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7110
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
7311
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7111
7312
  const worker = new Worker(toExecute, {
7112
7313
  ...opts.workerOpts,
7113
7314
  trackUnmanagedFds: false,
@@ -7493,7 +7694,7 @@ var require_transport = __commonJS({
7493
7694
  "use strict";
7494
7695
  var { createRequire } = require("module");
7495
7696
  var getCallers = require_caller();
7496
- var { join: join4, isAbsolute, sep } = require("path");
7697
+ var { join: join11, isAbsolute, sep: sep2 } = require("path");
7497
7698
  var sleep = require_atomic_sleep();
7498
7699
  var onExit = require_on_exit_leak_free();
7499
7700
  var ThreadStream = require_thread_stream();
@@ -7556,7 +7757,7 @@ var require_transport = __commonJS({
7556
7757
  throw new Error("only one of target or targets can be specified");
7557
7758
  }
7558
7759
  if (targets) {
7559
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7760
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7560
7761
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
7561
7762
  return {
7562
7763
  ...dest,
@@ -7574,7 +7775,7 @@ var require_transport = __commonJS({
7574
7775
  });
7575
7776
  });
7576
7777
  } else if (pipeline2) {
7577
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7778
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7578
7779
  options.pipelines = [pipeline2.map((dest) => {
7579
7780
  return {
7580
7781
  ...dest,
@@ -7596,12 +7797,12 @@ var require_transport = __commonJS({
7596
7797
  return origin;
7597
7798
  }
7598
7799
  if (origin === "pino/file") {
7599
- return join4(__dirname, "..", "file.js");
7800
+ return join11(__dirname, "..", "file.js");
7600
7801
  }
7601
7802
  let fixTarget2;
7602
7803
  for (const filePath of callers) {
7603
7804
  try {
7604
- const context = filePath === "node:repl" ? process.cwd() + sep : filePath;
7805
+ const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
7605
7806
  fixTarget2 = createRequire(context).resolve(origin);
7606
7807
  break;
7607
7808
  } catch (err) {
@@ -8586,7 +8787,7 @@ var require_safe_stable_stringify = __commonJS({
8586
8787
  return circularValue;
8587
8788
  }
8588
8789
  let res = "";
8589
- let join4 = ",";
8790
+ let join11 = ",";
8590
8791
  const originalIndentation = indentation;
8591
8792
  if (Array.isArray(value)) {
8592
8793
  if (value.length === 0) {
@@ -8600,7 +8801,7 @@ var require_safe_stable_stringify = __commonJS({
8600
8801
  indentation += spacer;
8601
8802
  res += `
8602
8803
  ${indentation}`;
8603
- join4 = `,
8804
+ join11 = `,
8604
8805
  ${indentation}`;
8605
8806
  }
8606
8807
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8608,13 +8809,13 @@ ${indentation}`;
8608
8809
  for (; i < maximumValuesToStringify - 1; i++) {
8609
8810
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8610
8811
  res += tmp2 !== void 0 ? tmp2 : "null";
8611
- res += join4;
8812
+ res += join11;
8612
8813
  }
8613
8814
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8614
8815
  res += tmp !== void 0 ? tmp : "null";
8615
8816
  if (value.length - 1 > maximumBreadth) {
8616
8817
  const removedKeys = value.length - maximumBreadth - 1;
8617
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8818
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8618
8819
  }
8619
8820
  if (spacer !== "") {
8620
8821
  res += `
@@ -8635,7 +8836,7 @@ ${originalIndentation}`;
8635
8836
  let separator = "";
8636
8837
  if (spacer !== "") {
8637
8838
  indentation += spacer;
8638
- join4 = `,
8839
+ join11 = `,
8639
8840
  ${indentation}`;
8640
8841
  whitespace = " ";
8641
8842
  }
@@ -8649,13 +8850,13 @@ ${indentation}`;
8649
8850
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
8650
8851
  if (tmp !== void 0) {
8651
8852
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8652
- separator = join4;
8853
+ separator = join11;
8653
8854
  }
8654
8855
  }
8655
8856
  if (keyLength > maximumBreadth) {
8656
8857
  const removedKeys = keyLength - maximumBreadth;
8657
8858
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
8658
- separator = join4;
8859
+ separator = join11;
8659
8860
  }
8660
8861
  if (spacer !== "" && separator.length > 1) {
8661
8862
  res = `
@@ -8696,7 +8897,7 @@ ${originalIndentation}`;
8696
8897
  }
8697
8898
  const originalIndentation = indentation;
8698
8899
  let res = "";
8699
- let join4 = ",";
8900
+ let join11 = ",";
8700
8901
  if (Array.isArray(value)) {
8701
8902
  if (value.length === 0) {
8702
8903
  return "[]";
@@ -8709,7 +8910,7 @@ ${originalIndentation}`;
8709
8910
  indentation += spacer;
8710
8911
  res += `
8711
8912
  ${indentation}`;
8712
- join4 = `,
8913
+ join11 = `,
8713
8914
  ${indentation}`;
8714
8915
  }
8715
8916
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8717,13 +8918,13 @@ ${indentation}`;
8717
8918
  for (; i < maximumValuesToStringify - 1; i++) {
8718
8919
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8719
8920
  res += tmp2 !== void 0 ? tmp2 : "null";
8720
- res += join4;
8921
+ res += join11;
8721
8922
  }
8722
8923
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8723
8924
  res += tmp !== void 0 ? tmp : "null";
8724
8925
  if (value.length - 1 > maximumBreadth) {
8725
8926
  const removedKeys = value.length - maximumBreadth - 1;
8726
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8927
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8727
8928
  }
8728
8929
  if (spacer !== "") {
8729
8930
  res += `
@@ -8736,7 +8937,7 @@ ${originalIndentation}`;
8736
8937
  let whitespace = "";
8737
8938
  if (spacer !== "") {
8738
8939
  indentation += spacer;
8739
- join4 = `,
8940
+ join11 = `,
8740
8941
  ${indentation}`;
8741
8942
  whitespace = " ";
8742
8943
  }
@@ -8745,7 +8946,7 @@ ${indentation}`;
8745
8946
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
8746
8947
  if (tmp !== void 0) {
8747
8948
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8748
- separator = join4;
8949
+ separator = join11;
8749
8950
  }
8750
8951
  }
8751
8952
  if (spacer !== "" && separator.length > 1) {
@@ -8803,20 +9004,20 @@ ${originalIndentation}`;
8803
9004
  indentation += spacer;
8804
9005
  let res2 = `
8805
9006
  ${indentation}`;
8806
- const join5 = `,
9007
+ const join12 = `,
8807
9008
  ${indentation}`;
8808
9009
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
8809
9010
  let i = 0;
8810
9011
  for (; i < maximumValuesToStringify - 1; i++) {
8811
9012
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8812
9013
  res2 += tmp2 !== void 0 ? tmp2 : "null";
8813
- res2 += join5;
9014
+ res2 += join12;
8814
9015
  }
8815
9016
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8816
9017
  res2 += tmp !== void 0 ? tmp : "null";
8817
9018
  if (value.length - 1 > maximumBreadth) {
8818
9019
  const removedKeys = value.length - maximumBreadth - 1;
8819
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
9020
+ res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
8820
9021
  }
8821
9022
  res2 += `
8822
9023
  ${originalIndentation}`;
@@ -8832,16 +9033,16 @@ ${originalIndentation}`;
8832
9033
  return '"[Object]"';
8833
9034
  }
8834
9035
  indentation += spacer;
8835
- const join4 = `,
9036
+ const join11 = `,
8836
9037
  ${indentation}`;
8837
9038
  let res = "";
8838
9039
  let separator = "";
8839
9040
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
8840
9041
  if (isTypedArrayWithEntries(value)) {
8841
- res += stringifyTypedArray(value, join4, maximumBreadth);
9042
+ res += stringifyTypedArray(value, join11, maximumBreadth);
8842
9043
  keys = keys.slice(value.length);
8843
9044
  maximumPropertiesToStringify -= value.length;
8844
- separator = join4;
9045
+ separator = join11;
8845
9046
  }
8846
9047
  if (deterministic) {
8847
9048
  keys = sort(keys, comparator);
@@ -8852,13 +9053,13 @@ ${indentation}`;
8852
9053
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
8853
9054
  if (tmp !== void 0) {
8854
9055
  res += `${separator}${strEscape(key2)}: ${tmp}`;
8855
- separator = join4;
9056
+ separator = join11;
8856
9057
  }
8857
9058
  }
8858
9059
  if (keyLength > maximumBreadth) {
8859
9060
  const removedKeys = keyLength - maximumBreadth;
8860
9061
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
8861
- separator = join4;
9062
+ separator = join11;
8862
9063
  }
8863
9064
  if (separator !== "") {
8864
9065
  res = `
@@ -9949,11 +10150,11 @@ var init_lib = __esm({
9949
10150
  }
9950
10151
  }
9951
10152
  },
9952
- addToPath: function addToPath(path32, added, removed, oldPosInc, options) {
9953
- var last = path32.lastComponent;
10153
+ addToPath: function addToPath(path38, added, removed, oldPosInc, options) {
10154
+ var last = path38.lastComponent;
9954
10155
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
9955
10156
  return {
9956
- oldPos: path32.oldPos + oldPosInc,
10157
+ oldPos: path38.oldPos + oldPosInc,
9957
10158
  lastComponent: {
9958
10159
  count: last.count + 1,
9959
10160
  added,
@@ -9963,7 +10164,7 @@ var init_lib = __esm({
9963
10164
  };
9964
10165
  } else {
9965
10166
  return {
9966
- oldPos: path32.oldPos + oldPosInc,
10167
+ oldPos: path38.oldPos + oldPosInc,
9967
10168
  lastComponent: {
9968
10169
  count: 1,
9969
10170
  added,
@@ -10021,7 +10222,7 @@ var init_lib = __esm({
10021
10222
  tokenize: function tokenize(value) {
10022
10223
  return Array.from(value);
10023
10224
  },
10024
- join: function join3(chars) {
10225
+ join: function join4(chars) {
10025
10226
  return chars.join("");
10026
10227
  },
10027
10228
  postProcess: function postProcess(changeObjects) {
@@ -10394,10 +10595,10 @@ function attachmentToHistoryMessage(o, ts) {
10394
10595
  const memories = raw.map((m2) => {
10395
10596
  if (!m2 || typeof m2 !== "object") return null;
10396
10597
  const rec = m2;
10397
- const path32 = typeof rec.path === "string" ? rec.path : null;
10598
+ const path38 = typeof rec.path === "string" ? rec.path : null;
10398
10599
  const content = typeof rec.content === "string" ? rec.content : null;
10399
- if (!path32 || content == null) return null;
10400
- const entry = { path: path32, content };
10600
+ if (!path38 || content == null) return null;
10601
+ const entry = { path: path38, content };
10401
10602
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10402
10603
  return entry;
10403
10604
  }).filter((m2) => m2 !== null);
@@ -10857,6 +11058,27 @@ var init_claude_history = __esm({
10857
11058
  }
10858
11059
  });
10859
11060
 
11061
+ // src/tools/sandbox.ts
11062
+ function shouldSandbox(cwd, personaRoot) {
11063
+ if (!personaRoot) return false;
11064
+ const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
11065
+ return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
11066
+ }
11067
+ function inferSandboxSettingsPath(cwd, personaRoot) {
11068
+ if (!shouldSandbox(cwd, personaRoot)) return null;
11069
+ const rel = path12.relative(personaRoot, cwd);
11070
+ const personaId = rel.split(path12.sep)[0];
11071
+ if (!personaId) return null;
11072
+ return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
11073
+ }
11074
+ var path12;
11075
+ var init_sandbox = __esm({
11076
+ "src/tools/sandbox.ts"() {
11077
+ "use strict";
11078
+ path12 = __toESM(require("path"), 1);
11079
+ }
11080
+ });
11081
+
10860
11082
  // src/tools/claude.ts
10861
11083
  function macOSDesktopCandidates(home) {
10862
11084
  return [
@@ -10906,7 +11128,7 @@ function buildSpawnArgs(ctx) {
10906
11128
  ];
10907
11129
  if (ctx.model) args.push("--model", ctx.model);
10908
11130
  switch (ctx.personaMode) {
10909
- case "listener":
11131
+ case "guest":
10910
11132
  args.push("--setting-sources", "project,local");
10911
11133
  break;
10912
11134
  case "owner":
@@ -10921,7 +11143,8 @@ function buildSpawnArgs(ctx) {
10921
11143
  throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
10922
11144
  }
10923
11145
  }
10924
- if (ctx.extraSettings) args.push("--settings", ctx.extraSettings);
11146
+ const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
11147
+ if (sandboxSettings) args.push("--settings", sandboxSettings);
10925
11148
  if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
10926
11149
  if (ctx.effort) args.push("--effort", ctx.effort);
10927
11150
  if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
@@ -11201,10 +11424,10 @@ function parseAttachment(obj) {
11201
11424
  const memories = raw.map((m2) => {
11202
11425
  if (!m2 || typeof m2 !== "object") return null;
11203
11426
  const rec = m2;
11204
- const path32 = typeof rec.path === "string" ? rec.path : null;
11427
+ const path38 = typeof rec.path === "string" ? rec.path : null;
11205
11428
  const content = typeof rec.content === "string" ? rec.content : null;
11206
- if (!path32 || content == null) return null;
11207
- const out = { path: path32, content };
11429
+ if (!path38 || content == null) return null;
11430
+ const out = { path: path38, content };
11208
11431
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11209
11432
  return out;
11210
11433
  }).filter((m2) => m2 !== null);
@@ -11320,6 +11543,7 @@ var init_claude = __esm({
11320
11543
  init_protocol();
11321
11544
  init_claude_history();
11322
11545
  init_tool_result_extra();
11546
+ init_sandbox();
11323
11547
  ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
11324
11548
  "hook_additional_context",
11325
11549
  "hook_success",
@@ -18702,7 +18926,7 @@ var require_websocket = __commonJS({
18702
18926
  var http2 = require("http");
18703
18927
  var net = require("net");
18704
18928
  var tls = require("tls");
18705
- var { randomBytes, createHash } = require("crypto");
18929
+ var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
18706
18930
  var { Duplex, Readable: Readable3 } = require("stream");
18707
18931
  var { URL: URL2 } = require("url");
18708
18932
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19232,7 +19456,7 @@ var require_websocket = __commonJS({
19232
19456
  }
19233
19457
  }
19234
19458
  const defaultPort = isSecure ? 443 : 80;
19235
- const key = randomBytes(16).toString("base64");
19459
+ const key = randomBytes2(16).toString("base64");
19236
19460
  const request = isSecure ? https.request : http2.request;
19237
19461
  const protocolSet = /* @__PURE__ */ new Set();
19238
19462
  let perMessageDeflate;
@@ -19362,7 +19586,7 @@ var require_websocket = __commonJS({
19362
19586
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19363
19587
  return;
19364
19588
  }
19365
- const digest = createHash("sha1").update(key + GUID).digest("base64");
19589
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
19366
19590
  if (res.headers["sec-websocket-accept"] !== digest) {
19367
19591
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19368
19592
  return;
@@ -19729,7 +19953,7 @@ var require_websocket_server = __commonJS({
19729
19953
  var EventEmitter2 = require("events");
19730
19954
  var http2 = require("http");
19731
19955
  var { Duplex } = require("stream");
19732
- var { createHash } = require("crypto");
19956
+ var { createHash: createHash3 } = require("crypto");
19733
19957
  var extension2 = require_extension();
19734
19958
  var PerMessageDeflate2 = require_permessage_deflate();
19735
19959
  var subprotocol2 = require_subprotocol();
@@ -20030,7 +20254,7 @@ var require_websocket_server = __commonJS({
20030
20254
  );
20031
20255
  }
20032
20256
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20033
- const digest = createHash("sha1").update(key + GUID).digest("base64");
20257
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
20034
20258
  const headers = [
20035
20259
  "HTTP/1.1 101 Switching Protocols",
20036
20260
  "Upgrade: websocket",
@@ -20118,7 +20342,7 @@ var require_websocket_server = __commonJS({
20118
20342
  // src/run-case/recorder.ts
20119
20343
  function startRunCaseRecorder(opts) {
20120
20344
  const now = opts.now ?? Date.now;
20121
- const dir = import_node_path28.default.dirname(opts.recordPath);
20345
+ const dir = import_node_path27.default.dirname(opts.recordPath);
20122
20346
  let stream = null;
20123
20347
  let closing = false;
20124
20348
  let closedSettled = false;
@@ -20158,12 +20382,12 @@ function startRunCaseRecorder(opts) {
20158
20382
  };
20159
20383
  return { tap, close, closed };
20160
20384
  }
20161
- var import_node_fs25, import_node_path28;
20385
+ var import_node_fs25, import_node_path27;
20162
20386
  var init_recorder = __esm({
20163
20387
  "src/run-case/recorder.ts"() {
20164
20388
  "use strict";
20165
20389
  import_node_fs25 = __toESM(require("fs"), 1);
20166
- import_node_path28 = __toESM(require("path"), 1);
20390
+ import_node_path27 = __toESM(require("path"), 1);
20167
20391
  }
20168
20392
  });
20169
20393
 
@@ -20206,7 +20430,7 @@ var init_wire = __esm({
20206
20430
  // src/run-case/controller.ts
20207
20431
  async function runController(opts) {
20208
20432
  const now = opts.now ?? Date.now;
20209
- const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path29.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20433
+ const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20210
20434
  const ownsCwd = opts.cwd === void 0;
20211
20435
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20212
20436
  const spawnCtx = { cwd };
@@ -20373,13 +20597,13 @@ async function runController(opts) {
20373
20597
  }
20374
20598
  return exitCode ?? 0;
20375
20599
  }
20376
- var import_node_fs26, import_node_os14, import_node_path29;
20600
+ var import_node_fs26, import_node_os14, import_node_path28;
20377
20601
  var init_controller = __esm({
20378
20602
  "src/run-case/controller.ts"() {
20379
20603
  "use strict";
20380
20604
  import_node_fs26 = require("fs");
20381
20605
  import_node_os14 = __toESM(require("os"), 1);
20382
- import_node_path29 = __toESM(require("path"), 1);
20606
+ import_node_path28 = __toESM(require("path"), 1);
20383
20607
  init_claude();
20384
20608
  init_stdout_splitter();
20385
20609
  init_permission_stdio();
@@ -20611,7 +20835,7 @@ Env (advanced):
20611
20835
  `;
20612
20836
 
20613
20837
  // src/index.ts
20614
- var import_node_path27 = __toESM(require("path"), 1);
20838
+ var import_node_path26 = __toESM(require("path"), 1);
20615
20839
  var import_node_fs24 = __toESM(require("fs"), 1);
20616
20840
 
20617
20841
  // src/logger.ts
@@ -20649,6 +20873,9 @@ function createLogger(opts = {}) {
20649
20873
  return wrap(base);
20650
20874
  }
20651
20875
 
20876
+ // src/session/store-factory.ts
20877
+ var path5 = __toESM(require("path"), 1);
20878
+
20652
20879
  // src/session/store.ts
20653
20880
  var import_node_fs3 = __toESM(require("fs"), 1);
20654
20881
  var import_node_path4 = __toESM(require("path"), 1);
@@ -20657,10 +20884,14 @@ init_protocol();
20657
20884
  // src/session/scope.ts
20658
20885
  var import_node_path3 = __toESM(require("path"), 1);
20659
20886
  function scopeKey(scope) {
20660
- return scope.kind === "default" ? "default" : `persona:${scope.personaId}:${scope.mode}`;
20887
+ if (scope.kind === "default") return "default";
20888
+ if (scope.mode === "guest") return `persona:${scope.personaId}:guest:${scope.capId}`;
20889
+ return `persona:${scope.personaId}:${scope.mode}`;
20661
20890
  }
20662
20891
  function scopeSubPath(scope) {
20663
- return scope.kind === "default" ? ["default"] : [scope.personaId, scope.mode];
20892
+ if (scope.kind === "default") return ["default"];
20893
+ if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
20894
+ return [scope.personaId, scope.mode];
20664
20895
  }
20665
20896
  function metaFromScope(scope, personaRoot) {
20666
20897
  if (scope.kind === "default") return void 0;
@@ -20669,10 +20900,18 @@ function metaFromScope(scope, personaRoot) {
20669
20900
  }
20670
20901
  return {
20671
20902
  idleKillEnabled: true,
20672
- personaMode: "listener",
20903
+ personaMode: "guest",
20673
20904
  extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json")
20674
20905
  };
20675
20906
  }
20907
+ function scopeForFile(file, ownerPrincipalId) {
20908
+ if (!file.ownerPersonaId) return { kind: "default" };
20909
+ const creator = file.creatorPrincipalId;
20910
+ if (ownerPrincipalId && creator && creator !== ownerPrincipalId) {
20911
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "guest", capId: creator };
20912
+ }
20913
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
20914
+ }
20676
20915
 
20677
20916
  // src/session/store.ts
20678
20917
  function safeFileName(sessionId) {
@@ -20686,12 +20925,16 @@ function safeFileName(sessionId) {
20686
20925
  var SessionStore = class {
20687
20926
  root;
20688
20927
  constructor(opts) {
20689
- const scope = opts.scope ?? { kind: "default" };
20690
- this.root = import_node_path4.default.join(
20691
- opts.dataDir,
20692
- "sessions",
20693
- ...scopeSubPath(scope).map(safeFileName)
20694
- );
20928
+ if ("root" in opts) {
20929
+ this.root = opts.root;
20930
+ } else {
20931
+ const scope = opts.scope ?? { kind: "default" };
20932
+ this.root = import_node_path4.default.join(
20933
+ opts.dataDir,
20934
+ "sessions",
20935
+ ...scopeSubPath(scope).map(safeFileName)
20936
+ );
20937
+ }
20695
20938
  }
20696
20939
  filePath(sessionId) {
20697
20940
  return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
@@ -20756,6 +20999,66 @@ var SessionStore = class {
20756
20999
  }
20757
21000
  };
20758
21001
 
21002
+ // src/session/store-factory.ts
21003
+ var SessionStoreFactory = class {
21004
+ dataDir;
21005
+ bareStore = null;
21006
+ vmOwnerStores = /* @__PURE__ */ new Map();
21007
+ // vmGuest 索引 key = `${pid}::${capId}`(按 cap.id 隔离,一张 cap 一个目录)
21008
+ vmGuestStores = /* @__PURE__ */ new Map();
21009
+ constructor(opts) {
21010
+ this.dataDir = opts.dataDir;
21011
+ }
21012
+ // ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
21013
+ bareRoot() {
21014
+ return path5.join(this.dataDir, "sessions");
21015
+ }
21016
+ vmOwnerRoot(personaId) {
21017
+ return path5.join(
21018
+ this.dataDir,
21019
+ "personas",
21020
+ safeFileName(personaId),
21021
+ ".clawd",
21022
+ "sessions",
21023
+ "owner"
21024
+ );
21025
+ }
21026
+ vmGuestRoot(personaId, capId) {
21027
+ return path5.join(
21028
+ this.dataDir,
21029
+ "personas",
21030
+ safeFileName(personaId),
21031
+ ".clawd",
21032
+ "sessions",
21033
+ "guests",
21034
+ safeFileName(capId)
21035
+ );
21036
+ }
21037
+ // ---- SessionStore 工厂(缓存) ----
21038
+ forBare() {
21039
+ if (!this.bareStore) {
21040
+ this.bareStore = new SessionStore({ root: this.bareRoot() });
21041
+ }
21042
+ return this.bareStore;
21043
+ }
21044
+ forVmOwner(personaId) {
21045
+ const key = personaId;
21046
+ const cached = this.vmOwnerStores.get(key);
21047
+ if (cached) return cached;
21048
+ const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
21049
+ this.vmOwnerStores.set(key, st);
21050
+ return st;
21051
+ }
21052
+ forVmGuest(personaId, capId) {
21053
+ const key = `${personaId}::${capId}`;
21054
+ const cached = this.vmGuestStores.get(key);
21055
+ if (cached) return cached;
21056
+ const st = new SessionStore({ root: this.vmGuestRoot(personaId, capId) });
21057
+ this.vmGuestStores.set(key, st);
21058
+ return st;
21059
+ }
21060
+ };
21061
+
20759
21062
  // src/session/manager.ts
20760
21063
  var import_node_fs5 = __toESM(require("fs"), 1);
20761
21064
  var import_node_path6 = __toESM(require("path"), 1);
@@ -20875,7 +21178,7 @@ function cloneState(s) {
20875
21178
  subSessionMeta: s.subSessionMeta
20876
21179
  };
20877
21180
  }
20878
- var IDLE_KILL_DELAY_MS = 5e3;
21181
+ var IDLE_KILL_DELAY_MS = 6e4;
20879
21182
  var SEEN_UUID_CAP = 2e3;
20880
21183
  function applyEventBatch(state, events, deps) {
20881
21184
  if (events.length === 0) return { state, effects: [] };
@@ -20915,7 +21218,10 @@ function buildSpawnContext(state, deps) {
20915
21218
  toolSessionId: file.toolSessionId,
20916
21219
  model: file.model,
20917
21220
  permissionMode: file.permissionMode,
20918
- effort: file.effort
21221
+ effort: file.effort,
21222
+ // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
21223
+ // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
21224
+ personaRoot: deps.personaRoot
20919
21225
  };
20920
21226
  const meta = state.subSessionMeta;
20921
21227
  if (meta?.personaMode) {
@@ -21642,7 +21948,8 @@ var SessionRunner = class {
21642
21948
  now: this.hooks.now ?? Date.now,
21643
21949
  resolveContextWindow: this.hooks.resolveContextWindow,
21644
21950
  genUuid: this.hooks.genUuid ?? v4_default,
21645
- ownerDisplayName: this.hooks.ownerDisplayName
21951
+ ownerDisplayName: this.hooks.ownerDisplayName,
21952
+ personaRoot: this.hooks.personaRoot
21646
21953
  };
21647
21954
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
21648
21955
  this.state = state;
@@ -22066,9 +22373,18 @@ var SessionManager = class {
22066
22373
  attachObserver(observer) {
22067
22374
  this.attachedObserver = observer;
22068
22375
  }
22069
- // 按 scope 拿对应的 SessionStore。default scope 复用 deps.store(保证与
22070
- // bootstrap 注入的 store 一致);persona scope 第一次访问时按需创建并缓存。
22376
+ // 按 scope 拿对应的 SessionStore.
22377
+ // Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
22378
+ // default → factory.forBare() <dataDir>/sessions/
22379
+ // persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
22380
+ // persona/<pid>/guest → factory.forVmGuest(pid, capId) <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
22381
+ // 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
22071
22382
  storeFor(scope) {
22383
+ if (this.deps.storeFactory) {
22384
+ if (scope.kind === "default") return this.deps.storeFactory.forBare();
22385
+ if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
22386
+ return this.deps.storeFactory.forVmGuest(scope.personaId, scope.capId);
22387
+ }
22072
22388
  if (scope.kind === "default") return this.deps.store;
22073
22389
  const key = scopeKey(scope);
22074
22390
  const cached = this.storesByScope.get(key);
@@ -22088,19 +22404,18 @@ var SessionManager = class {
22088
22404
  this.capabilitiesCache.set(tool, caps);
22089
22405
  return caps;
22090
22406
  }
22091
- // 由 SessionFile.ownerPersonaId schema 字段推回写入 scope
22092
- // ownerPersonaId create({ownerPersonaId}) schema 落盘时记入,永远只反映
22093
- // session 出身(不可变);此函数只用于 SessionStore 的写入路由,不参与运行时行为
22094
- // 决策(personaMode='owner' 等由路径 → scope → metaFromScope 派生)。
22095
- // 仅覆盖 owner + default 两类——listener sub-session 不会从 default RPC 路径进来。
22407
+ // 由 SessionFile 反推 scope 的 thin wrapper —— 决策矩阵收敛到 scope.scopeForFile
22408
+ // free function(scope.spec.ts transition 表驱动测试),manager 只负责注入 deps.
22096
22409
  scopeForFile(file) {
22097
- return file.ownerPersonaId ? { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" } : { kind: "default" };
22410
+ return scopeForFile(file, this.deps.ownerPrincipalId);
22098
22411
  }
22099
- // <dataDir>/sessions/ 列出所有 persona 命名空间(不含 'default')。
22100
- // 用于 findOwnedSession / listAllOwned scope 查询。
22412
+ // 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned scope 查询.
22413
+ // Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
22414
+ // 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
22415
+ // <dataDir>/sessions/ 列子目录 (排除 'default').
22101
22416
  listPersonaIdsOnDisk() {
22102
22417
  if (!this.deps.dataDir) return [];
22103
- const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
22418
+ const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
22104
22419
  let entries;
22105
22420
  try {
22106
22421
  entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
@@ -22111,60 +22426,79 @@ var SessionManager = class {
22111
22426
  }
22112
22427
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
22113
22428
  }
22114
- // owner / default 两个分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
22115
- // SessionManager 在这里定位。三层快慢路径:
22116
- // 1) active runner(用户当前在用的 session):runner.state.file SessionFile 内存权威副本,
22117
- // 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
22118
- // 绝大多数场景
22119
- // 2) default scope 磁盘:inactive default session 命中
22120
- // 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
22121
- // listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
22122
- //
22123
- // 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
22124
- // (持 default-only store 是 file-sharing v1 的预存 bug 根因——见 fix #703)
22429
+ // 扫某 persona guests/ 子目录, 拿所有 capId.
22430
+ // 路径: <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
22431
+ // 只在 storeFactory 注入 (新布局) 下生效, 老布局无 guest 目录.
22432
+ listGuestCapIdsForPersona(personaId) {
22433
+ if (!this.deps.dataDir || !this.deps.storeFactory) return [];
22434
+ const root = import_node_path6.default.join(
22435
+ this.deps.dataDir,
22436
+ "personas",
22437
+ personaId,
22438
+ ".clawd",
22439
+ "sessions",
22440
+ "guests"
22441
+ );
22442
+ let entries;
22443
+ try {
22444
+ entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
22445
+ } catch (err) {
22446
+ const code = err?.code;
22447
+ if (code === "ENOENT") return [];
22448
+ throw err;
22449
+ }
22450
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
22451
+ }
22452
+ // owned 类目下按 sessionId 找文件——前端只传 sessionId 不带 scope.
22453
+ // 扫盘顺序: default → 每个 persona 的 owner/ → 每个 persona 的 guests/<capId>/.
22125
22454
  findOwnedSession(sessionId) {
22126
- const runner = this.runners.get(sessionId);
22127
- if (runner) return runner.getState().file;
22128
22455
  const dflt = this.deps.store.read(sessionId);
22129
22456
  if (dflt) return dflt;
22130
22457
  for (const personaId of this.listPersonaIdsOnDisk()) {
22131
22458
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22132
- const file = ownerStore.read(sessionId);
22133
- if (file) return file;
22459
+ const found = ownerStore.read(sessionId);
22460
+ if (found) return found;
22461
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22462
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22463
+ const g2 = guestStore.read(sessionId);
22464
+ if (g2) return g2;
22465
+ }
22134
22466
  }
22135
22467
  return null;
22136
22468
  }
22137
- // findOwnedSession + scopeForFile 收口。把"sessionId SessionScope"的派生
22138
- // 集中到 manager(scope 单一来源),调用方拿 scope 不再自己拼 object 也不依赖
22139
- // ownerPersonaId 字段语义。给 attachment handler 通过 deps.getSessionScope 闭包消费。
22140
- findOwnedSessionScope(sessionId) {
22141
- const file = this.findOwnedSession(sessionId);
22142
- return file ? this.scopeForFile(file) : null;
22143
- }
22144
- // 合并 default + 所有 persona owner 的 SessionFile —— 桌面 App 主 session 列表入口。
22145
- // 同样不含 listener sub-session(listener 走 persona:listSubSessions RPC 单独入口)。
22469
+ // 合并 default + 每个 persona owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
22470
+ // guest sessions handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
22471
+ // (creatorPrincipalId === capId), 这里仅负责数据源汇总.
22146
22472
  listAllOwned() {
22147
22473
  const out = [];
22148
22474
  out.push(...this.deps.store.list());
22149
22475
  for (const personaId of this.listPersonaIdsOnDisk()) {
22150
22476
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22151
22477
  out.push(...ownerStore.list());
22478
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22479
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22480
+ out.push(...guestStore.list());
22481
+ }
22152
22482
  }
22153
22483
  return out.sort(
22154
22484
  (a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
22155
22485
  );
22156
22486
  }
22157
- // 写回 SessionFile,自动根据 ownerPersonaId 派生写入 scope
22158
- // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由。
22487
+ // 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
22488
+ // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
22159
22489
  writeOwned(file) {
22160
22490
  return this.storeFor(this.scopeForFile(file)).write(file);
22161
22491
  }
22162
- // 删除 owned SessionFile(default persona/owner),与 findOwnedSession 对称扫盘。
22492
+ // 删除 owned SessionFile(default / persona owner / persona guest),与 findOwnedSession 对称扫盘.
22163
22493
  deleteOwned(sessionId) {
22164
22494
  if (this.deps.store.delete(sessionId)) return true;
22165
22495
  for (const personaId of this.listPersonaIdsOnDisk()) {
22166
22496
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22167
22497
  if (ownerStore.delete(sessionId)) return true;
22498
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22499
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22500
+ if (guestStore.delete(sessionId)) return true;
22501
+ }
22168
22502
  }
22169
22503
  return false;
22170
22504
  }
@@ -22172,8 +22506,8 @@ var SessionManager = class {
22172
22506
  // 经过 compressFrameForWire 后决定是 push collector 还是走 deps.broadcastFrame。
22173
22507
  // scope 决定两件事:SessionStore 路由 + SubSessionMeta 派生(metaFromScope)。
22174
22508
  // - default → 复用 deps.store,无 subSessionMeta(普通 session)
22175
- // - persona/owner → <dataDir>/sessions/<personaId>/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
22176
- // - persona/listener<dataDir>/sessions/<personaId>/listener/,meta={idleKillEnabled:true, personaMode:'listener', extraSettings:<sandbox>}
22509
+ // - persona/owner → personas/<pid>/.clawd/sessions/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
22510
+ // - persona/guestpersonas/<pid>/.clawd/sessions/guests/<capId>/,meta={idleKillEnabled:true, personaMode:'guest', extraSettings:<sandbox>}
22177
22511
  newRunner(file, scope) {
22178
22512
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
22179
22513
  const store = this.storeFor(scope);
@@ -22192,6 +22526,9 @@ var SessionManager = class {
22192
22526
  dataDir: this.deps.dataDir,
22193
22527
  personaStore: this.deps.personaStore,
22194
22528
  ownerDisplayName: this.deps.ownerDisplayName,
22529
+ // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
22530
+ // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
22531
+ personaRoot: this.deps.personaRoot,
22195
22532
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22196
22533
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22197
22534
  scope,
@@ -22286,7 +22623,7 @@ var SessionManager = class {
22286
22623
  }
22287
22624
  }
22288
22625
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
22289
- create(args) {
22626
+ create(args, creatorPrincipalId) {
22290
22627
  let cwd;
22291
22628
  if (args.ownerPersonaId) {
22292
22629
  cwd = derivePersonaSpawnCwd(
@@ -22327,10 +22664,11 @@ var SessionManager = class {
22327
22664
  iconKey: args.iconKey,
22328
22665
  forkedFromSessionId: args.forkedFromSessionId,
22329
22666
  ownerPersonaId: args.ownerPersonaId,
22667
+ creatorPrincipalId,
22330
22668
  createdAt: iso,
22331
22669
  updatedAt: iso
22332
22670
  };
22333
- const scope = args.ownerPersonaId ? { kind: "persona", personaId: args.ownerPersonaId, mode: "owner" } : { kind: "default" };
22671
+ const scope = this.scopeForFile(file);
22334
22672
  const written = this.storeFor(scope).write(file);
22335
22673
  return { response: written, broadcast: [] };
22336
22674
  }
@@ -22755,28 +23093,18 @@ var SessionManager = class {
22755
23093
  this.runners.set(file.sessionId, r);
22756
23094
  return r;
22757
23095
  }
22758
- // ---------------- persona / listener sub-session 专用 API ----------------
23096
+ // ---------------- scope-explicit sub-session API ----------------
22759
23097
  //
22760
- // PersonaManager SessionManager 之上的薄编排层,listener sub-session 的持久化
22761
- // (SessionFile)仍由 SessionManager 持有。区别于普通 / owner session:
22762
- // - SessionFile 落到 <dataDir>/sessions/<personaId>/listener/(拓扑分类)
22763
- // - reducer state.subSessionMeta.idleKillEnabled = true(turn_end 自动 idle-kill)
22764
- // - sessionId 由 PersonaManager 派生(personaId-<tokenHash>),不走自动 newSessionId
23098
+ // 区别于 owner / default sessionId 入口,这些 API 接 SessionScope 参数 ——
23099
+ // 调用方(PersonaManager / observer 等)明确传 scope,由 storeFor 路由到正确目录:
23100
+ // - {kind:'persona', personaId, mode:'guest', capId} → guests/<capId>/,meta={idleKill:true, sandbox}
23101
+ // - {kind:'persona', personaId, mode:'owner'} → owner/,meta={idleKill:false}
22765
23102
  //
22766
- // 这些 API SessionScope 参数(不再是单参 agentId)—— transport 始终明确传
22767
- // {kind:'persona', personaId, mode:'listener'},与 owner / default 分类隔离。
23103
+ // SubSessionMeta scope metaFromScope 派生,不进 SessionFile schema。
22768
23104
  /** 按 scope 读取 SessionFile;不存在返回 null */
22769
23105
  readForScope(sessionId, scope) {
22770
23106
  return this.storeFor(scope).read(sessionId);
22771
23107
  }
22772
- /**
22773
- * 列出指定 persona 在指定 mode 下的所有 SessionFile。
22774
- * persona:listSubSessions RPC 入口(mode='listener');
22775
- * 路径下还未写过任何 session 时 list() 返回空数组(SessionStore.list 内部 ENOENT 处理)
22776
- */
22777
- listPersonaSessions(personaId, mode) {
22778
- return this.storeFor({ kind: "persona", personaId, mode }).list();
22779
- }
22780
23108
  /**
22781
23109
  * 创建 sub-session 的 SessionFile + 准备 runner(不 spawn)。
22782
23110
  * SubSessionMeta 不进 SessionFile schema,运行时由 scope → metaFromScope 派生。
@@ -22921,7 +23249,7 @@ var SessionManager = class {
22921
23249
  return { response: { ok: true }, broadcast };
22922
23250
  }
22923
23251
  /**
22924
- * 改 SessionFile.label(用于 listener-bound chat:rename RPC)。
23252
+ * 改 SessionFile.label(scope-explicit sub-session 重命名入口)。
22925
23253
  * 不存在抛 SESSION_NOT_FOUND。
22926
23254
  */
22927
23255
  renameForScope(args) {
@@ -22936,7 +23264,7 @@ var SessionManager = class {
22936
23264
  store.write({ ...file, label: args.label });
22937
23265
  }
22938
23266
  /**
22939
- * 删除 listener-scope sub-session(用于 chat:delete RPC)。
23267
+ * 删除 scope-explicit sub-session
22940
23268
  * - 若 runner 存在:走 reducer 'delete' command 收尾(SIGKILL + 文件 unlink + broadcast)
22941
23269
  * - 若 runner 不存在:直接删 scope store 文件 + 广播 session:deleted
22942
23270
  * 不存在抛 SESSION_NOT_FOUND。
@@ -23204,7 +23532,7 @@ var SessionManager = class {
23204
23532
 
23205
23533
  // src/persona/store.ts
23206
23534
  var fs6 = __toESM(require("fs"), 1);
23207
- var path7 = __toESM(require("path"), 1);
23535
+ var path8 = __toESM(require("path"), 1);
23208
23536
  init_protocol();
23209
23537
  var DEFAULT_SETTINGS = {
23210
23538
  permissions: {
@@ -23233,13 +23561,13 @@ var PersonaStore = class {
23233
23561
  }
23234
23562
  root;
23235
23563
  personaDir(personaId) {
23236
- return path7.join(this.root, safeFileName(personaId));
23564
+ return path8.join(this.root, safeFileName(personaId));
23237
23565
  }
23238
23566
  metaPath(personaId) {
23239
- return path7.join(this.personaDir(personaId), ".clawd", "persona.json");
23567
+ return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
23240
23568
  }
23241
23569
  claudeMdPath(personaId) {
23242
- return path7.join(this.personaDir(personaId), "CLAUDE.md");
23570
+ return path8.join(this.personaDir(personaId), "CLAUDE.md");
23243
23571
  }
23244
23572
  /**
23245
23573
  * Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
@@ -23249,11 +23577,11 @@ var PersonaStore = class {
23249
23577
  * 加载 persona 人格,只有 listener 多一层 OS sandbox。
23250
23578
  */
23251
23579
  sandboxSettingsPath(personaId) {
23252
- return path7.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23580
+ return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23253
23581
  }
23254
23582
  write(persona, personality) {
23255
23583
  const dir = this.personaDir(persona.personaId);
23256
- fs6.mkdirSync(path7.join(dir, ".clawd"), { recursive: true });
23584
+ fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
23257
23585
  this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
23258
23586
  this.atomicWrite(
23259
23587
  this.sandboxSettingsPath(persona.personaId),
@@ -23271,6 +23599,10 @@ var PersonaStore = class {
23271
23599
  const p2 = this.metaPath(personaId);
23272
23600
  if (!fs6.existsSync(p2)) return null;
23273
23601
  const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23602
+ if (raw && typeof raw === "object" && "tokenMap" in raw) {
23603
+ delete raw.tokenMap;
23604
+ this.atomicWrite(p2, JSON.stringify(raw, null, 2));
23605
+ }
23274
23606
  return PersonaFileSchema.parse(raw);
23275
23607
  }
23276
23608
  has(personaId) {
@@ -23296,48 +23628,12 @@ var PersonaStore = class {
23296
23628
  }
23297
23629
  /** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
23298
23630
  skillsDir(personaId) {
23299
- return path7.join(this.personaDir(personaId), ".claude", "skills");
23300
- }
23301
- /**
23302
- * Claude Code 项目级 settings 路径:`<personaDir>/.claude/settings.json`。
23303
- * 这里只读 `enabledPlugins` 字段,由 owner 通过 CC `/plugin` 之类命令维护,daemon 不写。
23304
- */
23305
- claudeSettingsPath(personaId) {
23306
- return path7.join(this.personaDir(personaId), ".claude", "settings.json");
23307
- }
23308
- /**
23309
- * 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
23310
- * 的 key 转成 `{ id }[]` 给 UI 做只读展示。
23311
- *
23312
- * 兜底范围(来自老板规则:不要无事实依据的兜底):
23313
- * - 文件不存在 → [](plugin 未被启用是合法初始态)
23314
- * - JSON 解析失败 → [](owner 手改文件,schema 漂移交给 UI 兜底)
23315
- * - `enabledPlugins` 字段缺失 / 非 object → []
23316
- * - 单 value 非 boolean → 跳过该 entry
23317
- * 其他错误(fs 权限等)原样抛出,由上层 handler 转 ws error 帧。
23318
- */
23319
- readEnabledPlugins(personaId) {
23320
- const p2 = this.claudeSettingsPath(personaId);
23321
- if (!fs6.existsSync(p2)) return [];
23322
- let raw;
23323
- try {
23324
- raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23325
- } catch {
23326
- return [];
23327
- }
23328
- if (!raw || typeof raw !== "object") return [];
23329
- const enabled = raw.enabledPlugins;
23330
- if (!enabled || typeof enabled !== "object" || Array.isArray(enabled)) return [];
23331
- const out = [];
23332
- for (const [id, value] of Object.entries(enabled)) {
23333
- if (value === true && id.length > 0) out.push({ id });
23334
- }
23335
- return out;
23631
+ return path8.join(this.personaDir(personaId), ".claude", "skills");
23336
23632
  }
23337
23633
  list() {
23338
23634
  if (!fs6.existsSync(this.root)) return [];
23339
23635
  return fs6.readdirSync(this.root).filter((name) => {
23340
- return fs6.existsSync(path7.join(this.root, name, ".clawd", "persona.json"));
23636
+ return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
23341
23637
  });
23342
23638
  }
23343
23639
  remove(personaId) {
@@ -23362,12 +23658,19 @@ var PersonaRegistry = class {
23362
23658
  }
23363
23659
  store;
23364
23660
  cache = /* @__PURE__ */ new Map();
23365
- /** 从 store 全量重建缓存(boot 时 + 测试 setup 时) */
23661
+ /**
23662
+ * 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
23663
+ * 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
23664
+ * try/catch 隔离,让其它 persona 仍能加载。
23665
+ */
23366
23666
  reload() {
23367
23667
  this.cache.clear();
23368
23668
  for (const id of this.store.list()) {
23369
- const p2 = this.store.read(id);
23370
- if (p2) this.cache.set(p2.personaId, p2);
23669
+ try {
23670
+ const p2 = this.store.read(id);
23671
+ if (p2) this.cache.set(p2.personaId, p2);
23672
+ } catch {
23673
+ }
23371
23674
  }
23372
23675
  }
23373
23676
  get(personaId) {
@@ -23383,21 +23686,6 @@ var PersonaRegistry = class {
23383
23686
  list() {
23384
23687
  return Array.from(this.cache.values());
23385
23688
  }
23386
- /**
23387
- * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23388
- * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23389
- * persona 数量通常 < 100,性能可忽略。
23390
- */
23391
- findByToken(token) {
23392
- if (!token) return null;
23393
- for (const persona of this.cache.values()) {
23394
- if (!persona.public) continue;
23395
- const entry = persona.tokenMap?.[token];
23396
- if (!entry || entry.revoked) continue;
23397
- return { personaId: persona.personaId, label: entry.label };
23398
- }
23399
- return null;
23400
- }
23401
23689
  };
23402
23690
 
23403
23691
  // src/persona/manager.ts
@@ -23691,9 +23979,6 @@ var PersonaManager = class {
23691
23979
  model: args.model,
23692
23980
  public: args.public ?? false,
23693
23981
  iconKey: args.iconKey,
23694
- // 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
23695
- // file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
23696
- tokenMap: {},
23697
23982
  createdAt: now,
23698
23983
  updatedAt: now
23699
23984
  };
@@ -23730,65 +24015,28 @@ var PersonaManager = class {
23730
24015
  listSkills(personaId) {
23731
24016
  return listSkillsForDir(this.deps.store.skillsDir(personaId), "project");
23732
24017
  }
23733
- /**
23734
- * 读取 persona `.claude/settings.json` 中已启用的插件列表(value === true 的 key)。
23735
- * 文件不存在 / 解析失败 / 字段缺失 → 空数组。供 persona:get handler 拼响应。
23736
- */
23737
- listEnabledPlugins(personaId) {
23738
- return this.deps.store.readEnabledPlugins(personaId);
23739
- }
23740
24018
  /** 读 sandbox-settings.json;不存在 / 损坏返回 null。供 persona:get handler 拼响应。 */
23741
24019
  readSandboxSettings(personaId) {
23742
24020
  return this.deps.store.readSandboxSettings(personaId);
23743
24021
  }
23744
24022
  /**
23745
24023
  * 删除 persona。
23746
- * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录。
24024
+ * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
24025
+ * Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
24026
+ * .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
24027
+ *
24028
+ * ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
24029
+ * ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
24030
+ * 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
24031
+ * 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
24032
+ * 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
24033
+ * force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
24034
+ * "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
23747
24035
  */
23748
24036
  delete(personaId) {
23749
24037
  this.deps.store.remove(personaId);
23750
24038
  this.deps.registry.remove(personaId);
23751
24039
  }
23752
- /**
23753
- * 生成 32-char base64url token 并写入 tokenMap,给 file-sharing HTTP Bearer
23754
- * 鉴权用(PersonaRegistry.findByToken 反查 personaId/label)。
23755
- */
23756
- issueToken(personaId, label) {
23757
- const existing = this.deps.registry.get(personaId);
23758
- if (!existing) throw new Error(`persona not found: ${personaId}`);
23759
- const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
23760
- const entry = {
23761
- label,
23762
- issuedAt: Date.now(),
23763
- revoked: false
23764
- };
23765
- const updated = {
23766
- ...existing,
23767
- tokenMap: { ...existing.tokenMap ?? {}, [token]: entry },
23768
- updatedAt: Date.now()
23769
- };
23770
- this.deps.store.writeMeta(updated);
23771
- this.deps.registry.set(updated);
23772
- return { token, persona: updated };
23773
- }
23774
- /** 标记 token 为 revoked(保留条目用于审计);不存在的 token 抛错。 */
23775
- revokeToken(personaId, token) {
23776
- const existing = this.deps.registry.get(personaId);
23777
- if (!existing) throw new Error(`persona not found: ${personaId}`);
23778
- const tokenEntry = existing.tokenMap?.[token];
23779
- if (!tokenEntry) throw new Error(`token not found in persona ${personaId}`);
23780
- const updated = {
23781
- ...existing,
23782
- tokenMap: {
23783
- ...existing.tokenMap ?? {},
23784
- [token]: { ...tokenEntry, revoked: true }
23785
- },
23786
- updatedAt: Date.now()
23787
- };
23788
- this.deps.store.writeMeta(updated);
23789
- this.deps.registry.set(updated);
23790
- return updated;
23791
- }
23792
24040
  /**
23793
24041
  * label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
23794
24042
  * base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
@@ -23808,7 +24056,7 @@ var PersonaManager = class {
23808
24056
 
23809
24057
  // src/persona/seed.ts
23810
24058
  var fs8 = __toESM(require("fs"), 1);
23811
- var path9 = __toESM(require("path"), 1);
24059
+ var path10 = __toESM(require("path"), 1);
23812
24060
  var import_node_url = require("url");
23813
24061
  var import_meta = {};
23814
24062
  var DEFAULT_PERSONAS = [
@@ -23844,14 +24092,14 @@ var DEFAULT_PERSONAS = [
23844
24092
  function findDefaultsRoot() {
23845
24093
  const candidates = [];
23846
24094
  try {
23847
- const here = path9.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
23848
- candidates.push(path9.resolve(here, "defaults"));
23849
- candidates.push(path9.resolve(here, "persona-defaults"));
24095
+ const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
24096
+ candidates.push(path10.resolve(here, "defaults"));
24097
+ candidates.push(path10.resolve(here, "persona-defaults"));
23850
24098
  } catch {
23851
24099
  }
23852
24100
  if (process.argv[1]) {
23853
- const argvDir = path9.dirname(process.argv[1]);
23854
- candidates.push(path9.resolve(argvDir, "persona-defaults"));
24101
+ const argvDir = path10.dirname(process.argv[1]);
24102
+ candidates.push(path10.resolve(argvDir, "persona-defaults"));
23855
24103
  }
23856
24104
  for (const c of candidates) {
23857
24105
  try {
@@ -23868,7 +24116,7 @@ function seedDefaultPersonas(args) {
23868
24116
  args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
23869
24117
  continue;
23870
24118
  }
23871
- const bundleDir = path9.join(args.defaultsRoot, entry.personaId);
24119
+ const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
23872
24120
  if (!fs8.existsSync(bundleDir)) {
23873
24121
  args.logger.warn("persona.seed.skip", {
23874
24122
  personaId: entry.personaId,
@@ -23877,7 +24125,7 @@ function seedDefaultPersonas(args) {
23877
24125
  });
23878
24126
  continue;
23879
24127
  }
23880
- const claudeMdPath = path9.join(bundleDir, "CLAUDE.md");
24128
+ const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
23881
24129
  if (!fs8.existsSync(claudeMdPath)) {
23882
24130
  args.logger.warn("persona.seed.skip", {
23883
24131
  personaId: entry.personaId,
@@ -23894,7 +24142,6 @@ function seedDefaultPersonas(args) {
23894
24142
  model: entry.model,
23895
24143
  public: entry.public,
23896
24144
  iconKey: entry.iconKey,
23897
- tokenMap: {},
23898
24145
  createdAt: now,
23899
24146
  updatedAt: now
23900
24147
  };
@@ -23906,8 +24153,8 @@ function seedDefaultPersonas(args) {
23906
24153
  function copyBundleExtras(srcDir, dstDir) {
23907
24154
  for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
23908
24155
  if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
23909
- const srcPath = path9.join(srcDir, entry.name);
23910
- const dstPath = path9.join(dstDir, entry.name);
24156
+ const srcPath = path10.join(srcDir, entry.name);
24157
+ const dstPath = path10.join(dstDir, entry.name);
23911
24158
  if (entry.isDirectory()) {
23912
24159
  fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
23913
24160
  } else if (entry.isFile()) {
@@ -25275,6 +25522,7 @@ var import_sender = __toESM(require_sender(), 1);
25275
25522
  var import_subprotocol = __toESM(require_subprotocol(), 1);
25276
25523
  var import_websocket = __toESM(require_websocket(), 1);
25277
25524
  var import_websocket_server = __toESM(require_websocket_server(), 1);
25525
+ var wrapper_default = import_websocket.default;
25278
25526
 
25279
25527
  // src/transport/local-ws-server.ts
25280
25528
  var import_node_http = __toESM(require("http"), 1);
@@ -25289,6 +25537,9 @@ var LocalWsServer = class {
25289
25537
  httpServer = null;
25290
25538
  frameHandler = null;
25291
25539
  clients = /* @__PURE__ */ new Map();
25540
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
25541
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
25542
+ capabilityIdToClients = /* @__PURE__ */ new Map();
25292
25543
  logger;
25293
25544
  pingIntervalMs;
25294
25545
  async start() {
@@ -25379,6 +25630,49 @@ var LocalWsServer = class {
25379
25630
  this.safeSend(c.ws, frame);
25380
25631
  }
25381
25632
  }
25633
+ // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25634
+ // 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
25635
+ // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25636
+ broadcastToOwners(frame) {
25637
+ const gate = this.opts.authGate;
25638
+ for (const c of this.clients.values()) {
25639
+ if (gate && !gate.isAuthed(c.handle.id)) continue;
25640
+ if (c.ctx && c.ctx.principal.kind !== "owner") continue;
25641
+ this.safeSend(c.ws, frame);
25642
+ }
25643
+ }
25644
+ // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
25645
+ // - 所有 owner ws (owner 看自家所有 channel)
25646
+ // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
25647
+ // 一次调用同时推两端, inbox:event push 帧由此走.
25648
+ broadcastToCapabilityChannel(capabilityId, frame) {
25649
+ this.broadcastToOwners(frame);
25650
+ const set = this.capabilityIdToClients.get(capabilityId);
25651
+ if (!set) return;
25652
+ for (const id of set) {
25653
+ const c = this.clients.get(id);
25654
+ if (!c) continue;
25655
+ this.safeSend(c.ws, frame);
25656
+ }
25657
+ }
25658
+ // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
25659
+ // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
25660
+ // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
25661
+ // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
25662
+ // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
25663
+ broadcastToPrincipal(principalId, frame) {
25664
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
25665
+ this.broadcastToOwners(frame);
25666
+ return;
25667
+ }
25668
+ const set = this.capabilityIdToClients.get(principalId);
25669
+ if (!set) return;
25670
+ for (const id of set) {
25671
+ const c = this.clients.get(id);
25672
+ if (!c) continue;
25673
+ this.safeSend(c.ws, frame);
25674
+ }
25675
+ }
25382
25676
  firstSubscriber(sessionId) {
25383
25677
  for (const c of this.clients.values()) {
25384
25678
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -25400,6 +25694,40 @@ var LocalWsServer = class {
25400
25694
  if (!c) return;
25401
25695
  this.safeSend(c.ws, frame);
25402
25696
  }
25697
+ // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
25698
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
25699
+ attachClientContext(clientId, ctx) {
25700
+ const c = this.clients.get(clientId);
25701
+ if (!c) return;
25702
+ c.ctx = ctx;
25703
+ if (ctx.capabilityId) {
25704
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
25705
+ if (!set) {
25706
+ set = /* @__PURE__ */ new Set();
25707
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
25708
+ }
25709
+ set.add(clientId);
25710
+ }
25711
+ }
25712
+ // 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
25713
+ getClientContext(clientId) {
25714
+ return this.clients.get(clientId)?.ctx ?? null;
25715
+ }
25716
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
25717
+ // 让客户端识别 'capability revoked' 而非普通断连。
25718
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
25719
+ const set = this.capabilityIdToClients.get(capabilityId);
25720
+ if (!set) return;
25721
+ const ids = [...set];
25722
+ for (const id of ids) {
25723
+ const c = this.clients.get(id);
25724
+ if (!c) continue;
25725
+ try {
25726
+ c.ws.close(code, reason);
25727
+ } catch {
25728
+ }
25729
+ }
25730
+ }
25403
25731
  // URL path 路由:'/' → 主连接路径;其他 → close 4404
25404
25732
  routeConnection(socket, req) {
25405
25733
  const remoteAddress = req?.socket?.remoteAddress;
@@ -25434,7 +25762,7 @@ var LocalWsServer = class {
25434
25762
  } catch {
25435
25763
  }
25436
25764
  }, this.pingIntervalMs);
25437
- this.clients.set(id, { handle, ws: socket, pingTimer });
25765
+ this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
25438
25766
  this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
25439
25767
  const authGate = this.opts.authGate;
25440
25768
  let authed = true;
@@ -25458,6 +25786,12 @@ var LocalWsServer = class {
25458
25786
  socket.on("close", () => {
25459
25787
  const c = this.clients.get(id);
25460
25788
  if (c?.pingTimer) clearInterval(c.pingTimer);
25789
+ const capId = c?.ctx?.capabilityId;
25790
+ if (capId) {
25791
+ const set = this.capabilityIdToClients.get(capId);
25792
+ set?.delete(id);
25793
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
25794
+ }
25461
25795
  this.clients.delete(id);
25462
25796
  authGate?.unregister(id);
25463
25797
  this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
@@ -25490,6 +25824,17 @@ var LocalWsServer = class {
25490
25824
  return;
25491
25825
  }
25492
25826
  } else if (frame.type === "auth") {
25827
+ const token = typeof frame.token === "string" ? frame.token ?? "" : "";
25828
+ const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
25829
+ const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
25830
+ if (token && this.opts.tryVerifyCapabilityToken) {
25831
+ const guestCtx = this.opts.tryVerifyCapabilityToken(
25832
+ token,
25833
+ selfPrincipalId || void 0,
25834
+ selfDisplayName || void 0
25835
+ );
25836
+ if (guestCtx) this.attachClientContext(client.id, guestCtx);
25837
+ }
25493
25838
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
25494
25839
  return;
25495
25840
  }
@@ -25606,15 +25951,31 @@ var AuthGate = class {
25606
25951
  this.markFailed(handle.id);
25607
25952
  return frame.type === "auth" ? "consumed" : "reject";
25608
25953
  }
25609
- if (!this.opts.expectedToken) {
25610
- this.opts.closeConnection(handle, 1008, "auth not configured");
25611
- this.markFailed(handle.id);
25612
- return "consumed";
25613
- }
25614
- if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25615
- this.opts.closeConnection(handle, 1008, "auth failed");
25616
- this.markFailed(handle.id);
25617
- return "consumed";
25954
+ let ctx = null;
25955
+ if (this.opts.authenticate) {
25956
+ const r = this.opts.authenticate(
25957
+ parsed.data.token,
25958
+ parsed.data.selfPrincipalId,
25959
+ parsed.data.selfDisplayName
25960
+ );
25961
+ if (!r.ok) {
25962
+ this.opts.closeConnection(handle, 4401, r.code);
25963
+ this.markFailed(handle.id);
25964
+ return "consumed";
25965
+ }
25966
+ ctx = r.context;
25967
+ } else {
25968
+ if (!this.opts.expectedToken) {
25969
+ this.opts.closeConnection(handle, 1008, "auth not configured");
25970
+ this.markFailed(handle.id);
25971
+ return "consumed";
25972
+ }
25973
+ if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25974
+ this.opts.closeConnection(handle, 1008, "auth failed");
25975
+ this.markFailed(handle.id);
25976
+ return "consumed";
25977
+ }
25978
+ ctx = this.opts.buildOwnerContext?.() ?? null;
25618
25979
  }
25619
25980
  if (st.timer != null) {
25620
25981
  const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -25622,6 +25983,7 @@ var AuthGate = class {
25622
25983
  }
25623
25984
  st.authed = true;
25624
25985
  st.timer = null;
25986
+ if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
25625
25987
  this.opts.sendOk(handle, { type: "auth:ok" });
25626
25988
  return "consumed";
25627
25989
  }
@@ -25657,7 +26019,7 @@ var AuthContextResolver = class {
25657
26019
  }
25658
26020
  opts;
25659
26021
  /**
25660
- * 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26022
+ * 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
25661
26023
  * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
25662
26024
  */
25663
26025
  resolveFromHeader(authHeader, remoteAddress) {
@@ -25667,15 +26029,6 @@ var AuthContextResolver = class {
25667
26029
  if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
25668
26030
  return { role: "owner", isLoopback };
25669
26031
  }
25670
- const personalHit = this.opts.personaRegistry.findByToken(token);
25671
- if (personalHit) {
25672
- return {
25673
- role: "personal",
25674
- personaId: personalHit.personaId,
25675
- label: personalHit.label,
25676
- isLoopback
25677
- };
25678
- }
25679
26032
  return null;
25680
26033
  }
25681
26034
  };
@@ -25697,206 +26050,753 @@ function constantTimeEqual2(a, b2) {
25697
26050
  return diff2 === 0;
25698
26051
  }
25699
26052
 
25700
- // src/transport/http-router.ts
25701
- var import_node_fs14 = __toESM(require("fs"), 1);
25702
- var import_node_path16 = __toESM(require("path"), 1);
26053
+ // ../protocol/src/index.ts
26054
+ init_runtime();
25703
26055
 
25704
- // src/attachment/group.ts
25705
- var import_node_fs13 = __toESM(require("fs"), 1);
25706
- var import_node_path14 = __toESM(require("path"), 1);
25707
- var import_node_crypto4 = __toESM(require("crypto"), 1);
25708
- init_protocol();
25709
- var GroupFileStore = class {
25710
- dataDir;
25711
- logger;
25712
- cache = /* @__PURE__ */ new Map();
25713
- constructor(opts) {
25714
- this.dataDir = opts.dataDir;
25715
- this.logger = opts.logger;
25716
- }
25717
- rootForScope(scope) {
25718
- return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26056
+ // src/transport/connection-context.ts
26057
+ function ownerContext(ownerPrincipalId, displayName) {
26058
+ return {
26059
+ principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
26060
+ grants: [{ resource: { type: "*" }, actions: ["admin"] }]
26061
+ };
26062
+ }
26063
+ function guestContext(cap) {
26064
+ return {
26065
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
26066
+ grants: cap.grants,
26067
+ capabilityId: cap.id
26068
+ };
26069
+ }
26070
+ function authenticate(token, deps) {
26071
+ if (!token) return { ok: false, code: "NO_TOKEN" };
26072
+ if (deps.isOwnerToken(token)) {
26073
+ return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
26074
+ }
26075
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
26076
+ const v2 = deps.capabilityRegistry.verifyToken(token);
26077
+ if (!v2.ok) {
26078
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
26079
+ return { ok: false, code: v2.code };
26080
+ }
26081
+ if (deps.selfPrincipalId && deps.selfDisplayName && deps.onGuestHello) {
26082
+ try {
26083
+ deps.onGuestHello(v2.capability.id, deps.selfPrincipalId, deps.selfDisplayName);
26084
+ } catch (e) {
26085
+ if (e.message === "TOKEN_SHARED_LEAK") {
26086
+ return { ok: false, code: "TOKEN_REVOKED" };
26087
+ }
26088
+ throw e;
26089
+ }
25719
26090
  }
25720
- /** SessionStore.filePath 平级,扩展名 .group-files.json */
25721
- filePath(scope, sessionId) {
25722
- return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26091
+ return { ok: true, context: guestContext(v2.capability) };
26092
+ }
26093
+
26094
+ // src/permission/capability-store.ts
26095
+ var fs15 = __toESM(require("fs"), 1);
26096
+ var path18 = __toESM(require("path"), 1);
26097
+ var CAPABILITIES_FILE_NAME = "capabilities.json";
26098
+ var FILE_VERSION = 1;
26099
+ var CapabilityStore = class {
26100
+ constructor(dataDir) {
26101
+ this.dataDir = dataDir;
26102
+ fs15.mkdirSync(dataDir, { recursive: true });
26103
+ this.cache = this.readFromDisk();
25723
26104
  }
25724
- cacheKey(scope, sessionId) {
25725
- return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26105
+ dataDir;
26106
+ cache;
26107
+ list() {
26108
+ return [...this.cache];
25726
26109
  }
25727
- /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
25728
- readFile(scope, sessionId) {
25729
- const file = this.filePath(scope, sessionId);
26110
+ upsert(cap) {
26111
+ const idx = this.cache.findIndex((c) => c.id === cap.id);
26112
+ if (idx >= 0) {
26113
+ this.cache[idx] = cap;
26114
+ } else {
26115
+ this.cache.push(cap);
26116
+ }
26117
+ this.flush();
26118
+ }
26119
+ remove(id) {
26120
+ const next = this.cache.filter((c) => c.id !== id);
26121
+ if (next.length === this.cache.length) return;
26122
+ this.cache = next;
26123
+ this.flush();
26124
+ }
26125
+ filePath() {
26126
+ return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
26127
+ }
26128
+ readFromDisk() {
26129
+ const file = this.filePath();
26130
+ let raw;
25730
26131
  try {
25731
- const raw = import_node_fs13.default.readFileSync(file, "utf8");
25732
- const parsed = JSON.parse(raw);
25733
- if (!Array.isArray(parsed)) {
25734
- this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
25735
- file
25736
- });
25737
- return [];
25738
- }
25739
- const out = [];
25740
- for (const entry of parsed) {
25741
- const r = GroupFileEntrySchema.safeParse(entry);
25742
- if (r.success) out.push(r.data);
25743
- }
25744
- return out;
26132
+ raw = fs15.readFileSync(file, "utf8");
25745
26133
  } catch (err) {
25746
- const code = err?.code;
25747
- if (code === "ENOENT") return [];
25748
- this.logger?.warn("GroupFileStore.readFile failed", {
25749
- file,
25750
- err: err.message
25751
- });
26134
+ if (err?.code === "ENOENT") return [];
26135
+ return [];
26136
+ }
26137
+ if (!raw.trim()) return [];
26138
+ let parsed;
26139
+ try {
26140
+ parsed = JSON.parse(raw);
26141
+ } catch {
25752
26142
  return [];
25753
26143
  }
26144
+ if (!parsed || typeof parsed !== "object") return [];
26145
+ const arr = parsed.capabilities;
26146
+ if (!Array.isArray(arr)) return [];
26147
+ const out = [];
26148
+ for (const item of arr) {
26149
+ const r = CapabilitySchema.safeParse(item);
26150
+ if (r.success) out.push(r.data);
26151
+ }
26152
+ return out;
25754
26153
  }
25755
- writeFile(scope, sessionId, entries) {
25756
- const file = this.filePath(scope, sessionId);
25757
- import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
25758
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
25759
- import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
25760
- import_node_fs13.default.renameSync(tmp, file);
26154
+ flush() {
26155
+ const content = { version: FILE_VERSION, capabilities: this.cache };
26156
+ this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
25761
26157
  }
25762
- /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
25763
- list(scope, sessionId) {
25764
- const key = this.cacheKey(scope, sessionId);
25765
- const cached = this.cache.get(key);
25766
- if (cached) return cached.entries;
25767
- const entries = this.readFile(scope, sessionId);
25768
- this.cache.set(key, { entries, scope });
25769
- return entries;
26158
+ atomicWrite(file, content) {
26159
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26160
+ fs15.writeFileSync(tmp, content, { mode: 384 });
26161
+ fs15.renameSync(tmp, file);
26162
+ try {
26163
+ fs15.chmodSync(file, 384);
26164
+ } catch {
26165
+ }
26166
+ }
26167
+ };
26168
+
26169
+ // src/permission/capability-registry.ts
26170
+ var crypto4 = __toESM(require("crypto"), 1);
26171
+ var CapabilityRegistry = class {
26172
+ constructor(store, now = () => Date.now()) {
26173
+ this.store = store;
26174
+ this.now = now;
26175
+ for (const cap of store.list()) {
26176
+ this.bySecretHash.set(cap.secretHash, cap);
26177
+ }
26178
+ }
26179
+ store;
26180
+ now;
26181
+ // sha256(token) → Capability
26182
+ bySecretHash = /* @__PURE__ */ new Map();
26183
+ list() {
26184
+ return this.store.list();
26185
+ }
26186
+ verifyToken(token) {
26187
+ if (!token) return { ok: false, code: "TOKEN_INVALID" };
26188
+ const hash = sha256Hex(token);
26189
+ const cap = this.bySecretHash.get(hash);
26190
+ if (!cap) return { ok: false, code: "TOKEN_INVALID" };
26191
+ if (cap.expiresAt && this.now() >= cap.expiresAt) {
26192
+ return { ok: false, code: "TOKEN_EXPIRED" };
26193
+ }
26194
+ if (cap.maxUses && cap.usedCount >= cap.maxUses) {
26195
+ return { ok: false, code: "TOKEN_EXHAUSTED" };
26196
+ }
26197
+ const updated = { ...cap, usedCount: cap.usedCount + 1 };
26198
+ this.bySecretHash.set(hash, updated);
26199
+ this.store.upsert(updated);
26200
+ return { ok: true, capability: updated };
26201
+ }
26202
+ upsertCapability(cap) {
26203
+ this.bySecretHash.set(cap.secretHash, cap);
26204
+ this.store.upsert(cap);
25770
26205
  }
25771
26206
  /**
25772
- * upsert:
25773
- * - relPath 已存在 更新 lastEditedAt + clear stale;不动 from / addedAt / id
25774
- * (保留首次入群人 / 入群时刻)
25775
- * - 不存在 → append,id 派生稳定 uuid,addedAt = now
25776
- *
25777
- * 返回最新 entry(caller 可用来 broadcast 通知)。
26207
+ * Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
26208
+ * 后续 verifyToken token 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
26209
+ * idempotent:不存在 → null。
25778
26210
  */
25779
- upsert(scope, sessionId, input, now = Date.now()) {
25780
- const entries = this.list(scope, sessionId).slice();
25781
- const idx = entries.findIndex((e) => e.relPath === input.relPath);
25782
- let next;
25783
- if (idx >= 0) {
25784
- const prev = entries[idx];
25785
- next = {
25786
- ...prev,
25787
- size: input.size,
25788
- mime: input.mime,
25789
- lastEditedAt: now,
25790
- stale: false
25791
- // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
25792
- };
25793
- entries[idx] = next;
25794
- } else {
25795
- next = {
25796
- id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
25797
- relPath: input.relPath,
25798
- from: input.from,
25799
- label: input.label,
25800
- size: input.size,
25801
- mime: input.mime,
25802
- addedAt: now
25803
- // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
25804
- };
25805
- entries.push(next);
26211
+ delete(id) {
26212
+ const current = this.store.list().find((c) => c.id === id);
26213
+ if (!current) return null;
26214
+ this.bySecretHash.delete(current.secretHash);
26215
+ this.store.remove(id);
26216
+ return current;
26217
+ }
26218
+ findById(id) {
26219
+ return this.store.list().find((c) => c.id === id) ?? null;
26220
+ }
26221
+ };
26222
+ function sha256Hex(s) {
26223
+ return crypto4.createHash("sha256").update(s).digest("hex");
26224
+ }
26225
+
26226
+ // src/permission/capability-manager.ts
26227
+ var crypto5 = __toESM(require("crypto"), 1);
26228
+ var CapabilityManager = class {
26229
+ constructor(registry2, hooks = {}) {
26230
+ this.registry = registry2;
26231
+ this.hooks = hooks;
26232
+ }
26233
+ registry;
26234
+ hooks;
26235
+ list() {
26236
+ return this.registry.list();
26237
+ }
26238
+ // whoami handler 用:根据 capabilityId 反查 caller 的 capability 实体。
26239
+ // 返回 Capability 持久化形态;handler 调 stripSecretHash 后再上 wire。
26240
+ findById(id) {
26241
+ return this.registry.findById(id);
26242
+ }
26243
+ issue(args) {
26244
+ if (!args.displayName.trim()) {
26245
+ throw new Error("CapabilityManager.issue: displayName must be non-empty");
25806
26246
  }
25807
- this.writeFile(scope, sessionId, entries);
25808
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25809
- return next;
26247
+ const token = (this.hooks.generateToken ?? defaultGenerateToken)();
26248
+ const id = (this.hooks.generateId ?? defaultGenerateId)();
26249
+ const now = (this.hooks.now ?? Date.now)();
26250
+ const cap = {
26251
+ id,
26252
+ secretHash: sha256Hex2(token),
26253
+ displayName: args.displayName,
26254
+ grants: args.grants,
26255
+ issuedAt: now,
26256
+ usedCount: 0,
26257
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
26258
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
26259
+ };
26260
+ this.registry.upsertCapability(cap);
26261
+ this.hooks.onIssued?.(cap, token);
26262
+ return { token, capability: cap };
25810
26263
  }
25811
26264
  /**
25812
- * 标记一个 relPath stale(agent rm / mv 后文件不在)。
25813
- * - 命中 stale=true,UI 灰显
25814
- * - 未命中 noop(不需要为不在群里的文件创建 stale 条目)
25815
- *
25816
- * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26265
+ * guest auth 帧捎带 selfPrincipalId/selfDisplayName 时,AuthGate.authenticate
26266
+ * 校验 cap token 通过后调本方法把 peer 字段写回 cap:
26267
+ * - 首次(cap.firstUsedByPeerAt 未设)→ 写盘 + 触发 onIssued 复用 push 频道
26268
+ * 让 owner UI 通过 capability:updated 实时拿到 peer 字段
26269
+ * - 重复(同 peerOwnerId)→ noop
26270
+ * - 冲突(不同 peerOwnerId)→ throw TOKEN_SHARED_LEAK,AuthGate 转 close 4401
25817
26271
  */
25818
- markStale(scope, sessionId, relPath) {
25819
- const entries = this.list(scope, sessionId).slice();
25820
- const idx = entries.findIndex((e) => e.relPath === relPath);
25821
- if (idx < 0) return;
25822
- if (entries[idx].stale) return;
25823
- entries[idx] = { ...entries[idx], stale: true };
25824
- this.writeFile(scope, sessionId, entries);
25825
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26272
+ recordPeerHello(capId, args) {
26273
+ const cap = this.registry.findById(capId);
26274
+ if (!cap) return { changed: false };
26275
+ if (cap.firstUsedByPeerAt != null) {
26276
+ if (cap.peerOwnerId !== args.ownerPrincipalId) {
26277
+ throw new Error("TOKEN_SHARED_LEAK");
26278
+ }
26279
+ return { changed: false };
26280
+ }
26281
+ const updated = {
26282
+ ...cap,
26283
+ peerOwnerId: args.ownerPrincipalId,
26284
+ firstUsedByPeerAt: args.now,
26285
+ peerDisplayName: args.displayName
26286
+ };
26287
+ this.registry.upsertCapability(updated);
26288
+ this.hooks.onIssued?.(updated, "__peer_hello__");
26289
+ return { changed: true };
25826
26290
  }
25827
26291
  /**
25828
- * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
25829
- * agent 自动入群的不应走这条 —— markStale 表达"文件不在了"语义;
25830
- * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
25831
- *
25832
- * 返回值:true=命中并删除;false=relPath 不在群里。
26292
+ * Hard delete:从 registry / store 物理移除 capability。
26293
+ * idempotent:不存在 null(handler 翻译成 VALIDATION_ERROR)
26294
+ * onDeleted hook daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
25833
26295
  */
25834
- remove(scope, sessionId, relPath) {
25835
- const entries = this.list(scope, sessionId).slice();
25836
- const idx = entries.findIndex((e) => e.relPath === relPath);
25837
- if (idx < 0) return false;
25838
- entries.splice(idx, 1);
25839
- this.writeFile(scope, sessionId, entries);
25840
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25841
- return true;
26296
+ delete(id) {
26297
+ const removed = this.registry.delete(id);
26298
+ if (!removed) return null;
26299
+ this.hooks.onDeleted?.(removed);
26300
+ return { capability: removed };
26301
+ }
26302
+ };
26303
+ function defaultGenerateToken() {
26304
+ return crypto5.randomBytes(24).toString("base64url");
26305
+ }
26306
+ function defaultGenerateId() {
26307
+ return "cap_" + crypto5.randomBytes(6).toString("base64url");
26308
+ }
26309
+ function sha256Hex2(s) {
26310
+ return crypto5.createHash("sha256").update(s).digest("hex");
26311
+ }
26312
+
26313
+ // src/permission/cleanup.ts
26314
+ var fs16 = __toESM(require("fs"), 1);
26315
+ function cleanupGuestSessionsForCapability(cap, factory) {
26316
+ const removed = [];
26317
+ for (const g2 of cap.grants) {
26318
+ if (g2.resource.type !== "persona") continue;
26319
+ const dir = factory.vmGuestRoot(g2.resource.id, cap.id);
26320
+ try {
26321
+ fs16.rmSync(dir, { recursive: true, force: true });
26322
+ removed.push(dir);
26323
+ } catch {
26324
+ }
26325
+ }
26326
+ return { removed };
26327
+ }
26328
+
26329
+ // src/inbox/inbox-store.ts
26330
+ var fs17 = __toESM(require("fs"), 1);
26331
+ var path19 = __toESM(require("path"), 1);
26332
+ var INBOX_SUBDIR = "inbox";
26333
+ var InboxStore = class {
26334
+ constructor(dataDir) {
26335
+ this.dataDir = dataDir;
26336
+ fs17.mkdirSync(this.dirPath(), { recursive: true });
25842
26337
  }
26338
+ dataDir;
25843
26339
  /**
25844
- * session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
25845
- *
25846
- * <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
25847
- * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
26340
+ * 列出某 peer 的消息, createdAt 升序.
26341
+ * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
26342
+ * 文件不存在时返回 [] (不抛, 不创建).
26343
+ */
26344
+ list(peerOwnerId, sinceCreatedAt) {
26345
+ const file = this.filePath(peerOwnerId);
26346
+ let raw;
26347
+ try {
26348
+ raw = fs17.readFileSync(file, "utf8");
26349
+ } catch (err) {
26350
+ if (err?.code === "ENOENT") return [];
26351
+ return [];
26352
+ }
26353
+ const all = parseAllLines(raw);
26354
+ if (sinceCreatedAt === void 0) return all;
26355
+ return all.filter((m2) => m2.createdAt > sinceCreatedAt);
26356
+ }
26357
+ /**
26358
+ * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
26359
+ */
26360
+ listAllPeerOwnerIds() {
26361
+ const dir = this.dirPath();
26362
+ let entries;
26363
+ try {
26364
+ entries = fs17.readdirSync(dir);
26365
+ } catch (err) {
26366
+ if (err?.code === "ENOENT") return [];
26367
+ return [];
26368
+ }
26369
+ return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
26370
+ }
26371
+ /**
26372
+ * 追加一条消息,按 message.id 幂等去重。
26373
+ * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
26374
+ */
26375
+ append(message) {
26376
+ const existing = this.list(message.peerOwnerId);
26377
+ if (existing.some((m2) => m2.id === message.id)) return;
26378
+ const file = this.filePath(message.peerOwnerId);
26379
+ const line = JSON.stringify(message) + "\n";
26380
+ fs17.appendFileSync(file, line, { mode: 384 });
26381
+ try {
26382
+ fs17.chmodSync(file, 384);
26383
+ } catch {
26384
+ }
26385
+ }
26386
+ /**
26387
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
26388
+ * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
26389
+ * 返写入了几条.
26390
+ */
26391
+ markRead(peerOwnerId, principalId, upToCreatedAt) {
26392
+ const messages = this.list(peerOwnerId);
26393
+ let changed = 0;
26394
+ const next = messages.map((m2) => {
26395
+ if (m2.senderPrincipalId === principalId) return m2;
26396
+ if (m2.createdAt > upToCreatedAt) return m2;
26397
+ const cur = m2.readBy[principalId];
26398
+ if (cur !== void 0 && cur >= upToCreatedAt) return m2;
26399
+ changed++;
26400
+ return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
26401
+ });
26402
+ if (changed === 0) return 0;
26403
+ this.rewriteFile(peerOwnerId, next);
26404
+ return changed;
26405
+ }
26406
+ /**
26407
+ * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
26408
+ */
26409
+ removeByPeerOwnerId(peerOwnerId) {
26410
+ const file = this.filePath(peerOwnerId);
26411
+ try {
26412
+ fs17.unlinkSync(file);
26413
+ } catch (err) {
26414
+ if (err?.code === "ENOENT") return;
26415
+ }
26416
+ }
26417
+ rewriteFile(peerOwnerId, messages) {
26418
+ const file = this.filePath(peerOwnerId);
26419
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26420
+ const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
26421
+ fs17.writeFileSync(tmp, content, { mode: 384 });
26422
+ fs17.renameSync(tmp, file);
26423
+ try {
26424
+ fs17.chmodSync(file, 384);
26425
+ } catch {
26426
+ }
26427
+ }
26428
+ dirPath() {
26429
+ return path19.join(this.dataDir, INBOX_SUBDIR);
26430
+ }
26431
+ filePath(peerOwnerId) {
26432
+ return path19.join(this.dirPath(), `${peerOwnerId}.jsonl`);
26433
+ }
26434
+ };
26435
+ function parseAllLines(raw) {
26436
+ const out = [];
26437
+ for (const line of raw.split("\n")) {
26438
+ const trimmed = line.trim();
26439
+ if (!trimmed) continue;
26440
+ let parsed;
26441
+ try {
26442
+ parsed = JSON.parse(trimmed);
26443
+ } catch {
26444
+ continue;
26445
+ }
26446
+ const r = InboxMessageSchema.safeParse(parsed);
26447
+ if (r.success) out.push(r.data);
26448
+ }
26449
+ return out;
26450
+ }
26451
+
26452
+ // src/inbox/inbox-manager.ts
26453
+ var InboxManager = class {
26454
+ constructor(store, broadcast) {
26455
+ this.store = store;
26456
+ this.broadcast = broadcast;
26457
+ }
26458
+ store;
26459
+ broadcast;
26460
+ /**
26461
+ * 写一条消息到 peer.jsonl + broadcast 给本机所有 owner ws。
26462
+ * 按 id 幂等:同 id 已落盘则 no-op(不广播、不重写)。
25848
26463
  *
25849
- * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26464
+ * caller 必须已通过 handler 鉴权
26465
+ * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
25850
26466
  */
25851
- listByPersona(personaId) {
25852
- const out = [];
25853
- for (const mode of ["owner", "listener"]) {
25854
- const scope = { kind: "persona", personaId, mode };
25855
- const root = this.rootForScope(scope);
25856
- let names;
26467
+ postMessage(args) {
26468
+ const beforeLen = this.store.list(args.peerOwnerId).length;
26469
+ const msg = {
26470
+ id: args.id,
26471
+ peerOwnerId: args.peerOwnerId,
26472
+ senderPrincipalId: args.senderPrincipalId,
26473
+ text: args.text,
26474
+ createdAt: args.createdAt,
26475
+ readBy: {}
26476
+ };
26477
+ this.store.append(msg);
26478
+ const afterLen = this.store.list(args.peerOwnerId).length;
26479
+ if (afterLen === beforeLen) return msg;
26480
+ this.broadcast(args.peerOwnerId, {
26481
+ type: "inbox:event",
26482
+ peerOwnerId: args.peerOwnerId,
26483
+ message: msg
26484
+ });
26485
+ return msg;
26486
+ }
26487
+ list(peerOwnerId, sinceCreatedAt) {
26488
+ return this.store.list(peerOwnerId, sinceCreatedAt);
26489
+ }
26490
+ /**
26491
+ * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
26492
+ * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
26493
+ */
26494
+ markRead(args) {
26495
+ const before = this.store.list(args.peerOwnerId);
26496
+ const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
26497
+ if (changed === 0) return 0;
26498
+ const after = this.store.list(args.peerOwnerId);
26499
+ const beforeById = new Map(before.map((m2) => [m2.id, m2]));
26500
+ for (const m2 of after) {
26501
+ const prev = beforeById.get(m2.id);
26502
+ if (!prev) continue;
26503
+ if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
26504
+ this.broadcast(args.peerOwnerId, {
26505
+ type: "inbox:event",
26506
+ peerOwnerId: args.peerOwnerId,
26507
+ message: m2
26508
+ });
26509
+ }
26510
+ return changed;
26511
+ }
26512
+ /** peer:remove cascade. 删整个 peer 的 jsonl. */
26513
+ removeChannel(peerOwnerId) {
26514
+ this.store.removeByPeerOwnerId(peerOwnerId);
26515
+ }
26516
+ };
26517
+
26518
+ // src/state/received-capability-store.ts
26519
+ var fs18 = __toESM(require("fs"), 1);
26520
+ var path20 = __toESM(require("path"), 1);
26521
+ var FILE_NAME = "received-capabilities.json";
26522
+ var ReceivedCapabilityStore = class {
26523
+ constructor(dataDir) {
26524
+ this.dataDir = dataDir;
26525
+ }
26526
+ dataDir;
26527
+ caps = /* @__PURE__ */ new Map();
26528
+ load() {
26529
+ this.caps.clear();
26530
+ const file = path20.join(this.dataDir, FILE_NAME);
26531
+ let raw;
26532
+ try {
26533
+ raw = fs18.readFileSync(file, "utf8");
26534
+ } catch (err) {
26535
+ if (err?.code !== "ENOENT") this.renameBak(file);
26536
+ return;
26537
+ }
26538
+ let parsed;
26539
+ try {
26540
+ parsed = JSON.parse(raw);
26541
+ } catch {
26542
+ this.renameBak(file);
26543
+ return;
26544
+ }
26545
+ const arr = parsed?.receivedCaps;
26546
+ if (!Array.isArray(arr)) return;
26547
+ for (const entry of arr) {
26548
+ const r = ReceivedCapabilitySchema.safeParse(entry);
26549
+ if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
26550
+ }
26551
+ }
26552
+ list() {
26553
+ return Array.from(this.caps.values());
26554
+ }
26555
+ get(ownerPrincipalId) {
26556
+ return this.caps.get(ownerPrincipalId) ?? null;
26557
+ }
26558
+ hasOwner(ownerPrincipalId) {
26559
+ return this.caps.has(ownerPrincipalId);
26560
+ }
26561
+ upsert(cap) {
26562
+ this.caps.set(cap.ownerPrincipalId, cap);
26563
+ this.flush();
26564
+ }
26565
+ remove(ownerPrincipalId) {
26566
+ if (!this.caps.delete(ownerPrincipalId)) return;
26567
+ this.flush();
26568
+ }
26569
+ flush() {
26570
+ const file = path20.join(this.dataDir, FILE_NAME);
26571
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
26572
+ const content = JSON.stringify(
26573
+ { receivedCaps: Array.from(this.caps.values()) },
26574
+ null,
26575
+ 2
26576
+ );
26577
+ fs18.mkdirSync(this.dataDir, { recursive: true });
26578
+ fs18.writeFileSync(tmp, content, { mode: 384 });
26579
+ fs18.renameSync(tmp, file);
26580
+ }
26581
+ renameBak(file) {
26582
+ try {
26583
+ fs18.renameSync(file, `${file}.bak`);
26584
+ } catch {
26585
+ }
26586
+ }
26587
+ };
26588
+
26589
+ // src/state/legacy-migration.ts
26590
+ var fs19 = __toESM(require("fs"), 1);
26591
+ var path21 = __toESM(require("path"), 1);
26592
+ var TARGETS = [
26593
+ { src: "capabilities.json", bak: "capabilities.legacy.bak" },
26594
+ { src: "remote-personas", bak: "remote-personas.legacy.bak" },
26595
+ { src: "peers.json", bak: "peers.legacy.bak" }
26596
+ ];
26597
+ function migrateLegacyFiles(dataDir) {
26598
+ for (const t of TARGETS) {
26599
+ const srcPath = path21.join(dataDir, t.src);
26600
+ const bakPath = path21.join(dataDir, t.bak);
26601
+ if (!fs19.existsSync(srcPath)) continue;
26602
+ if (fs19.existsSync(bakPath)) continue;
26603
+ try {
26604
+ fs19.renameSync(srcPath, bakPath);
26605
+ } catch {
26606
+ }
26607
+ }
26608
+ }
26609
+
26610
+ // src/received-capability/connect-remote.ts
26611
+ var crypto6 = __toESM(require("crypto"), 1);
26612
+ var HANDSHAKE_TIMEOUT_MS = 5e3;
26613
+ var RPC_TIMEOUT_MS = 5e3;
26614
+ async function connectRemote(args) {
26615
+ const ws = new wrapper_default(args.url);
26616
+ await new Promise((resolve2, reject) => {
26617
+ const onError = (e) => reject(e);
26618
+ ws.once("open", () => {
26619
+ ws.off("error", onError);
26620
+ resolve2();
26621
+ });
26622
+ ws.once("error", onError);
26623
+ });
26624
+ ws.send(
26625
+ JSON.stringify({
26626
+ type: "auth",
26627
+ token: args.token,
26628
+ selfPrincipalId: args.selfPrincipalId,
26629
+ selfDisplayName: args.selfDisplayName
26630
+ })
26631
+ );
26632
+ await new Promise((resolve2, reject) => {
26633
+ const onMessage = (raw) => {
26634
+ let f;
25857
26635
  try {
25858
- names = import_node_fs13.default.readdirSync(root);
25859
- } catch (err) {
25860
- const code = err?.code;
25861
- if (code === "ENOENT") continue;
25862
- continue;
26636
+ f = JSON.parse(raw.toString());
26637
+ } catch {
26638
+ return;
25863
26639
  }
25864
- for (const name of names) {
25865
- if (!name.endsWith(".group-files.json")) continue;
25866
- const sessionId = name.slice(0, -".group-files.json".length);
25867
- if (!sessionId) continue;
25868
- const entries = this.list(scope, sessionId);
25869
- out.push({ sessionId, entries });
26640
+ if (f.type === "auth:ok" || f.type === "ready") {
26641
+ ws.off("message", onMessage);
26642
+ resolve2();
26643
+ return;
26644
+ }
26645
+ if (f.type === "auth:error" || f.code === 4401) {
26646
+ ws.off("message", onMessage);
26647
+ reject(new Error(`auth failed: ${f.message ?? f.type ?? "unknown"}`));
26648
+ }
26649
+ };
26650
+ ws.on("message", onMessage);
26651
+ setTimeout(() => {
26652
+ ws.off("message", onMessage);
26653
+ reject(new Error("auth handshake timeout"));
26654
+ }, HANDSHAKE_TIMEOUT_MS);
26655
+ });
26656
+ function call(method, payload) {
26657
+ const requestId = crypto6.randomUUID();
26658
+ return new Promise((resolve2, reject) => {
26659
+ const onMessage = (raw) => {
26660
+ let f;
26661
+ try {
26662
+ f = JSON.parse(raw.toString());
26663
+ } catch {
26664
+ return;
26665
+ }
26666
+ if (f.requestId !== requestId) return;
26667
+ ws.off("message", onMessage);
26668
+ if (f.error || f.type?.endsWith(":error")) {
26669
+ reject(new Error(f.message ?? f.error ?? "rpc error"));
26670
+ } else {
26671
+ resolve2(f);
26672
+ }
26673
+ };
26674
+ ws.on("message", onMessage);
26675
+ ws.send(JSON.stringify({ type: method, requestId, ...payload }));
26676
+ setTimeout(() => {
26677
+ ws.off("message", onMessage);
26678
+ reject(new Error(`${method} timeout`));
26679
+ }, RPC_TIMEOUT_MS);
26680
+ });
26681
+ }
26682
+ return {
26683
+ whoami: async () => {
26684
+ const f = await call("whoami", {});
26685
+ return {
26686
+ ownerId: f.owner.id,
26687
+ displayName: f.owner.displayName,
26688
+ capabilityId: f.capability.id,
26689
+ grants: f.capability.grants
26690
+ };
26691
+ },
26692
+ close: () => {
26693
+ try {
26694
+ ws.close();
26695
+ } catch {
25870
26696
  }
25871
26697
  }
25872
- return out;
26698
+ };
26699
+ }
26700
+
26701
+ // src/migrations/2026-05-20-flatten-sessions.ts
26702
+ var fs20 = __toESM(require("fs"), 1);
26703
+ var path22 = __toESM(require("path"), 1);
26704
+ var MIGRATION_FLAG_NAME = ".migration.v1.done";
26705
+ function migrateFlattenSessions(opts) {
26706
+ const dataDir = opts.dataDir;
26707
+ const now = opts.now ?? Date.now;
26708
+ const sessionsDir = path22.join(dataDir, "sessions");
26709
+ const flagPath = path22.join(sessionsDir, MIGRATION_FLAG_NAME);
26710
+ if (existsSync4(flagPath)) {
26711
+ return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26712
+ }
26713
+ let movedBare = 0;
26714
+ let movedVmOwner = 0;
26715
+ let archivedListener = 0;
26716
+ const defaultDir = path22.join(sessionsDir, "default");
26717
+ if (existsSync4(defaultDir)) {
26718
+ for (const entry of readdirSafe(defaultDir)) {
26719
+ if (!entry.endsWith(".json")) continue;
26720
+ const src = path22.join(defaultDir, entry);
26721
+ const dst = path22.join(sessionsDir, entry);
26722
+ fs20.renameSync(src, dst);
26723
+ movedBare += 1;
26724
+ }
26725
+ rmdirIfEmpty(defaultDir);
26726
+ }
26727
+ for (const pid of readdirSafe(sessionsDir)) {
26728
+ const personaDir = path22.join(sessionsDir, pid);
26729
+ if (!isDir(personaDir)) continue;
26730
+ if (pid === "default") continue;
26731
+ const ownerSrc = path22.join(personaDir, "owner");
26732
+ if (existsSync4(ownerSrc) && isDir(ownerSrc)) {
26733
+ const ownerDst = path22.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26734
+ fs20.mkdirSync(ownerDst, { recursive: true });
26735
+ for (const file of readdirSafe(ownerSrc)) {
26736
+ if (!file.endsWith(".json")) continue;
26737
+ fs20.renameSync(path22.join(ownerSrc, file), path22.join(ownerDst, file));
26738
+ movedVmOwner += 1;
26739
+ }
26740
+ rmdirIfEmpty(ownerSrc);
26741
+ }
26742
+ const listenerSrc = path22.join(personaDir, "listener");
26743
+ if (existsSync4(listenerSrc) && isDir(listenerSrc)) {
26744
+ const archiveDst = path22.join(dataDir, ".legacy", `listener-${pid}`);
26745
+ fs20.mkdirSync(archiveDst, { recursive: true });
26746
+ for (const file of readdirSafe(listenerSrc)) {
26747
+ if (!file.endsWith(".json")) continue;
26748
+ fs20.renameSync(path22.join(listenerSrc, file), path22.join(archiveDst, file));
26749
+ archivedListener += 1;
26750
+ }
26751
+ rmdirIfEmpty(listenerSrc);
26752
+ }
26753
+ rmdirIfEmpty(personaDir);
26754
+ }
26755
+ fs20.mkdirSync(sessionsDir, { recursive: true });
26756
+ fs20.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26757
+ return {
26758
+ skipped: false,
26759
+ flagWritten: true,
26760
+ movedBare,
26761
+ movedVmOwner,
26762
+ archivedListener
26763
+ };
26764
+ }
26765
+ function existsSync4(p2) {
26766
+ try {
26767
+ fs20.statSync(p2);
26768
+ return true;
26769
+ } catch {
26770
+ return false;
25873
26771
  }
25874
- };
25875
- function personalViewable(groupStore, personaDir, personaId, absPath) {
25876
- const realTarget = safeRealpath(absPath);
25877
- if (!realTarget) {
26772
+ }
26773
+ function isDir(p2) {
26774
+ try {
26775
+ return fs20.statSync(p2).isDirectory();
26776
+ } catch {
25878
26777
  return false;
25879
26778
  }
25880
- const personasUnion = groupStore.listByPersona(personaId);
25881
- for (const { entries } of personasUnion) {
25882
- for (const e of entries) {
25883
- if (e.stale) continue;
25884
- const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
25885
- if (realEntry && realEntry === realTarget) return true;
25886
- }
26779
+ }
26780
+ function readdirSafe(p2) {
26781
+ try {
26782
+ return fs20.readdirSync(p2);
26783
+ } catch {
26784
+ return [];
25887
26785
  }
25888
- return false;
25889
26786
  }
25890
- function safeRealpath(p2) {
26787
+ function rmdirIfEmpty(p2) {
25891
26788
  try {
25892
- return import_node_fs13.default.realpathSync(p2);
26789
+ fs20.rmdirSync(p2);
25893
26790
  } catch {
25894
- return null;
25895
26791
  }
25896
26792
  }
25897
26793
 
25898
- // src/attachment/mime.ts
26794
+ // src/transport/http-router.ts
26795
+ var import_node_fs13 = __toESM(require("fs"), 1);
25899
26796
  var import_node_path15 = __toESM(require("path"), 1);
26797
+
26798
+ // src/attachment/mime.ts
26799
+ var import_node_path14 = __toESM(require("path"), 1);
25900
26800
  var TEXT_PLAIN = "text/plain; charset=utf-8";
25901
26801
  var EXT_TO_NATIVE_MIME = {
25902
26802
  // 图片
@@ -26003,14 +26903,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
26003
26903
  ".mk"
26004
26904
  ]);
26005
26905
  function lookupMime(filePathOrName) {
26006
- const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
26906
+ const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
26007
26907
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
26008
26908
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
26009
26909
  return "application/octet-stream";
26010
26910
  }
26011
26911
 
26012
26912
  // src/attachment/sign-url.ts
26013
- var import_node_crypto5 = __toESM(require("crypto"), 1);
26913
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
26014
26914
  var HMAC_ALGO = "sha256";
26015
26915
  function base64urlEncode(buf) {
26016
26916
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -26027,7 +26927,7 @@ function decodeAbsPathFromUrl(encoded) {
26027
26927
  }
26028
26928
  function computeSig(secret, absPath, e) {
26029
26929
  const msg = e === null ? absPath : `${absPath}|${e}`;
26030
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26930
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26031
26931
  }
26032
26932
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
26033
26933
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -26062,7 +26962,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
26062
26962
  if (provided.length !== expected.length) {
26063
26963
  return { ok: false, code: "BAD_SIG" };
26064
26964
  }
26065
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
26965
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
26066
26966
  return { ok: false, code: "BAD_SIG" };
26067
26967
  }
26068
26968
  if (e !== null && now() / 1e3 > e) {
@@ -26083,7 +26983,7 @@ function createHttpRouter(deps) {
26083
26983
  sendJson(res, 200, { ok: true, version: deps.daemonVersion });
26084
26984
  return true;
26085
26985
  }
26086
- if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26986
+ if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26087
26987
  return false;
26088
26988
  }
26089
26989
  if (url.pathname.startsWith("/files/") && req.method === "GET") {
@@ -26123,43 +27023,8 @@ function createHttpRouter(deps) {
26123
27023
  sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
26124
27024
  return true;
26125
27025
  }
26126
- const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
26127
- if (personaFilesMatch && req.method === "GET") {
26128
- const pid = personaFilesMatch[1];
26129
- const pathParam = url.searchParams.get("path");
26130
- if (!pathParam) {
26131
- sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26132
- return true;
26133
- }
26134
- if (!deps.personaStore || !deps.groupFileStore) {
26135
- sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
26136
- return true;
26137
- }
26138
- const personaDir = deps.personaStore.personaDirPath(pid);
26139
- const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
26140
- if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
26141
- sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
26142
- return true;
26143
- }
26144
- if (ctx.role === "personal") {
26145
- if (ctx.personaId !== pid) {
26146
- sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
26147
- return true;
26148
- }
26149
- if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
26150
- sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
26151
- return true;
26152
- }
26153
- }
26154
- streamFile(res, absPath, deps.logger);
26155
- return true;
26156
- }
26157
27026
  const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
26158
27027
  if (sessionFilesMatch && req.method === "GET") {
26159
- if (ctx.role !== "owner") {
26160
- sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
26161
- return true;
26162
- }
26163
27028
  const sid = sessionFilesMatch[1];
26164
27029
  const pathParam = url.searchParams.get("path");
26165
27030
  if (!pathParam) {
@@ -26167,7 +27032,7 @@ function createHttpRouter(deps) {
26167
27032
  return true;
26168
27033
  }
26169
27034
  let absPath;
26170
- if (import_node_path16.default.isAbsolute(pathParam)) {
27035
+ if (import_node_path15.default.isAbsolute(pathParam)) {
26171
27036
  absPath = pathParam;
26172
27037
  } else if (deps.sessionStore) {
26173
27038
  const file = deps.sessionStore.read(sid);
@@ -26175,7 +27040,7 @@ function createHttpRouter(deps) {
26175
27040
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
26176
27041
  return true;
26177
27042
  }
26178
- absPath = import_node_path16.default.join(file.cwd, pathParam);
27043
+ absPath = import_node_path15.default.join(file.cwd, pathParam);
26179
27044
  } else {
26180
27045
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
26181
27046
  return true;
@@ -26200,18 +27065,12 @@ function sendJson(res, status, body) {
26200
27065
  res.end(JSON.stringify(body));
26201
27066
  }
26202
27067
  function withCtx(ctx, body) {
26203
- return { ...body, role: ctx.role, personaId: ctx.personaId };
26204
- }
26205
- function isContainedIn(abs, root) {
26206
- const normalized = import_node_path16.default.resolve(abs);
26207
- const normalizedRoot = import_node_path16.default.resolve(root);
26208
- if (normalized === normalizedRoot) return true;
26209
- return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
27068
+ return { ...body, role: ctx.role };
26210
27069
  }
26211
27070
  function streamFile(res, absPath, logger) {
26212
27071
  let stat;
26213
27072
  try {
26214
- stat = import_node_fs14.default.statSync(absPath);
27073
+ stat = import_node_fs13.default.statSync(absPath);
26215
27074
  } catch (err) {
26216
27075
  const code = err?.code;
26217
27076
  if (code === "ENOENT") {
@@ -26226,7 +27085,7 @@ function streamFile(res, absPath, logger) {
26226
27085
  return;
26227
27086
  }
26228
27087
  const mime = lookupMime(absPath);
26229
- const basename = import_node_path16.default.basename(absPath);
27088
+ const basename = import_node_path15.default.basename(absPath);
26230
27089
  res.writeHead(200, {
26231
27090
  "Content-Type": mime,
26232
27091
  "Content-Length": String(stat.size),
@@ -26234,7 +27093,7 @@ function streamFile(res, absPath, logger) {
26234
27093
  // 防止浏览器把任意 mime 当 html 渲染
26235
27094
  "X-Content-Type-Options": "nosniff"
26236
27095
  });
26237
- const stream = import_node_fs14.default.createReadStream(absPath);
27096
+ const stream = import_node_fs13.default.createReadStream(absPath);
26238
27097
  stream.on("error", (err) => {
26239
27098
  logger?.warn("streamFile read error", { absPath, err: err.message });
26240
27099
  res.destroy();
@@ -26242,6 +27101,147 @@ function streamFile(res, absPath, logger) {
26242
27101
  stream.pipe(res);
26243
27102
  }
26244
27103
 
27104
+ // src/attachment/group.ts
27105
+ var import_node_fs14 = __toESM(require("fs"), 1);
27106
+ var import_node_path16 = __toESM(require("path"), 1);
27107
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
27108
+ init_protocol();
27109
+ var GroupFileStore = class {
27110
+ dataDir;
27111
+ logger;
27112
+ cache = /* @__PURE__ */ new Map();
27113
+ constructor(opts) {
27114
+ this.dataDir = opts.dataDir;
27115
+ this.logger = opts.logger;
27116
+ }
27117
+ rootForScope(scope) {
27118
+ return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27119
+ }
27120
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
27121
+ filePath(scope, sessionId) {
27122
+ return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27123
+ }
27124
+ cacheKey(scope, sessionId) {
27125
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
27126
+ }
27127
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
27128
+ readFile(scope, sessionId) {
27129
+ const file = this.filePath(scope, sessionId);
27130
+ try {
27131
+ const raw = import_node_fs14.default.readFileSync(file, "utf8");
27132
+ const parsed = JSON.parse(raw);
27133
+ if (!Array.isArray(parsed)) {
27134
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
27135
+ file
27136
+ });
27137
+ return [];
27138
+ }
27139
+ const out = [];
27140
+ for (const entry of parsed) {
27141
+ const r = GroupFileEntrySchema.safeParse(entry);
27142
+ if (r.success) out.push(r.data);
27143
+ }
27144
+ return out;
27145
+ } catch (err) {
27146
+ const code = err?.code;
27147
+ if (code === "ENOENT") return [];
27148
+ this.logger?.warn("GroupFileStore.readFile failed", {
27149
+ file,
27150
+ err: err.message
27151
+ });
27152
+ return [];
27153
+ }
27154
+ }
27155
+ writeFile(scope, sessionId, entries) {
27156
+ const file = this.filePath(scope, sessionId);
27157
+ import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
27158
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27159
+ import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27160
+ import_node_fs14.default.renameSync(tmp, file);
27161
+ }
27162
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
27163
+ list(scope, sessionId) {
27164
+ const key = this.cacheKey(scope, sessionId);
27165
+ const cached = this.cache.get(key);
27166
+ if (cached) return cached.entries;
27167
+ const entries = this.readFile(scope, sessionId);
27168
+ this.cache.set(key, { entries });
27169
+ return entries;
27170
+ }
27171
+ /**
27172
+ * upsert:
27173
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
27174
+ * (保留首次入群人 / 入群时刻)
27175
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
27176
+ *
27177
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
27178
+ */
27179
+ upsert(scope, sessionId, input, now = Date.now()) {
27180
+ const entries = this.list(scope, sessionId).slice();
27181
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
27182
+ let next;
27183
+ if (idx >= 0) {
27184
+ const prev = entries[idx];
27185
+ next = {
27186
+ ...prev,
27187
+ size: input.size,
27188
+ mime: input.mime,
27189
+ lastEditedAt: now,
27190
+ stale: false
27191
+ // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
27192
+ };
27193
+ entries[idx] = next;
27194
+ } else {
27195
+ next = {
27196
+ id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
27197
+ relPath: input.relPath,
27198
+ from: input.from,
27199
+ label: input.label,
27200
+ size: input.size,
27201
+ mime: input.mime,
27202
+ addedAt: now
27203
+ // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
27204
+ };
27205
+ entries.push(next);
27206
+ }
27207
+ this.writeFile(scope, sessionId, entries);
27208
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27209
+ return next;
27210
+ }
27211
+ /**
27212
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
27213
+ * - 命中 → stale=true,UI 灰显
27214
+ * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
27215
+ *
27216
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
27217
+ */
27218
+ markStale(scope, sessionId, relPath) {
27219
+ const entries = this.list(scope, sessionId).slice();
27220
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27221
+ if (idx < 0) return;
27222
+ if (entries[idx].stale) return;
27223
+ entries[idx] = { ...entries[idx], stale: true };
27224
+ this.writeFile(scope, sessionId, entries);
27225
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27226
+ }
27227
+ /**
27228
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
27229
+ * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
27230
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
27231
+ *
27232
+ * 返回值:true=命中并删除;false=relPath 不在群里。
27233
+ */
27234
+ remove(scope, sessionId, relPath) {
27235
+ const entries = this.list(scope, sessionId).slice();
27236
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27237
+ if (idx < 0) return false;
27238
+ entries.splice(idx, 1);
27239
+ this.writeFile(scope, sessionId, entries);
27240
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27241
+ return true;
27242
+ }
27243
+ };
27244
+
26245
27245
  // src/discovery/state-file.ts
26246
27246
  var import_node_fs15 = __toESM(require("fs"), 1);
26247
27247
  var import_node_path17 = __toESM(require("path"), 1);
@@ -26918,39 +27918,44 @@ var AUTH_FILE_NAME = "auth.json";
26918
27918
  function authFilePath(dataDir) {
26919
27919
  return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
26920
27920
  }
26921
- function loadOrCreateAuthFile(opts) {
27921
+ function loadOrCreateAuth(opts) {
26922
27922
  const file = authFilePath(opts.dataDir);
26923
- const generate = opts.generate ?? defaultGenerate;
26924
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
26925
27923
  const existing = readAuthFile(file);
26926
- if (existing && existing.token && existing.signSecret) {
26927
- return {
26928
- token: existing.token,
26929
- signSecret: existing.signSecret,
26930
- createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
26931
- };
27924
+ const genToken = opts.generate ?? defaultGenerateToken2;
27925
+ const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
27926
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
27927
+ if (existing) {
27928
+ if (!existing.ownerPrincipalId) {
27929
+ const ownerPrincipalId2 = genId();
27930
+ writeAuthFile(file, {
27931
+ token: existing.token,
27932
+ ownerPrincipalId: ownerPrincipalId2,
27933
+ createdAt: existing.createdAt
27934
+ });
27935
+ return { token: existing.token, ownerPrincipalId: ownerPrincipalId2 };
27936
+ }
27937
+ return { token: existing.token, ownerPrincipalId: existing.ownerPrincipalId };
26932
27938
  }
26933
- const token = existing?.token || generate();
26934
- const signSecret = existing?.signSecret || generate();
26935
- const createdAt = existing?.createdAt || now().toISOString();
26936
- const next = { token, signSecret, createdAt };
26937
- writeAuthFile(file, next);
26938
- return next;
27939
+ const token = genToken();
27940
+ const ownerPrincipalId = genId();
27941
+ writeAuthFile(file, { token, ownerPrincipalId, createdAt: now.toISOString() });
27942
+ return { token, ownerPrincipalId };
26939
27943
  }
26940
- function defaultGenerate() {
27944
+ function defaultGenerateToken2() {
26941
27945
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
26942
27946
  }
27947
+ function defaultGenerateOwnerPrincipalId() {
27948
+ return `owner-${import_node_crypto8.default.randomUUID()}`;
27949
+ }
26943
27950
  function readAuthFile(file) {
26944
27951
  try {
26945
27952
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
26946
27953
  const parsed = JSON.parse(raw);
26947
- if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
26948
- return null;
26949
- }
27954
+ if (typeof parsed?.token !== "string" || parsed.token.length === 0) return null;
26950
27955
  return {
26951
27956
  token: parsed.token,
26952
- signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
26953
- createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
27957
+ ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : "",
27958
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
26954
27959
  };
26955
27960
  } catch (err) {
26956
27961
  const code = err?.code;
@@ -27063,10 +28068,63 @@ function forkSession(input) {
27063
28068
  return { forkedToolSessionId, forkedFilePath };
27064
28069
  }
27065
28070
 
28071
+ // src/permission/capability.ts
28072
+ function matchResource(grant, target) {
28073
+ if (grant.type === "*") return true;
28074
+ if (grant.type !== target.type) return false;
28075
+ if (grant.id === "*") return true;
28076
+ return grant.id === target.id;
28077
+ }
28078
+ function assertGrant(grants, resource, action) {
28079
+ for (const g2 of grants) {
28080
+ if (!matchResource(g2.resource, resource)) continue;
28081
+ if (g2.actions.includes(action)) return true;
28082
+ if (g2.actions.includes("admin")) return true;
28083
+ }
28084
+ return false;
28085
+ }
28086
+
28087
+ // src/permission/session-access.ts
28088
+ function canAccessSession(ctx, sessionId, action, deps) {
28089
+ if (ctx.principal.kind === "owner") return true;
28090
+ const file = deps.readSession(sessionId);
28091
+ if (!file) return true;
28092
+ return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
28093
+ }
28094
+ function canAccessPersona(grants, personaId, action) {
28095
+ if (!personaId) return false;
28096
+ const resource = { type: "persona", id: personaId };
28097
+ return assertGrant(grants, resource, action);
28098
+ }
28099
+
27066
28100
  // src/handlers/session.ts
28101
+ init_protocol();
27067
28102
  function buildSessionHandlers(deps) {
27068
- const { manager, observer, getAdapter: getAdapter2 } = deps;
27069
- const create = async (frame) => {
28103
+ const { manager, observer, getAdapter: getAdapter2, store, ownerPrincipalId } = deps;
28104
+ const ensureSessionAccess = (ctx, sessionId, action) => {
28105
+ if (!ctx) return;
28106
+ const ok = canAccessSession(ctx, sessionId, action, {
28107
+ readSession: (sid) => store.read(sid)
28108
+ });
28109
+ if (!ok) {
28110
+ throw new ClawdError(
28111
+ ERROR_CODES.UNAUTHORIZED,
28112
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} session ${sessionId}`
28113
+ );
28114
+ }
28115
+ };
28116
+ const ensurePersonaAccess = (ctx, personaId, action) => {
28117
+ if (!ctx) return;
28118
+ if (ctx.principal.kind === "owner") return;
28119
+ const ok = canAccessPersona(ctx.grants, personaId, action);
28120
+ if (!ok) {
28121
+ throw new ClawdError(
28122
+ ERROR_CODES.UNAUTHORIZED,
28123
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} on persona:${personaId ?? "<none>"}`
28124
+ );
28125
+ }
28126
+ };
28127
+ const create = async (frame, _client, ctx) => {
27070
28128
  const args = SessionCreateArgs.parse(frame);
27071
28129
  if (args.ownerPersonaId) {
27072
28130
  const persona = deps.personaRegistry.get(args.ownerPersonaId);
@@ -27074,58 +28132,75 @@ function buildSessionHandlers(deps) {
27074
28132
  throw new Error(`persona not found: ${args.ownerPersonaId}`);
27075
28133
  }
27076
28134
  }
27077
- const { response, broadcast } = manager.create(args);
28135
+ ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
28136
+ const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
28137
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
27078
28138
  return { response: { type: "session:info", ...response }, broadcast };
27079
28139
  };
27080
- const list = async () => {
28140
+ const list = async (_frame, _client, ctx) => {
27081
28141
  const { response } = manager.list();
28142
+ if (ctx && ctx.principal.kind === "guest") {
28143
+ const sessions = response.sessions ?? [];
28144
+ const capId = ctx.capabilityId;
28145
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === capId);
28146
+ return { response: { type: "session:list", sessions: filtered } };
28147
+ }
27082
28148
  return { response: { type: "session:list", ...response } };
27083
28149
  };
27084
- const get = async (frame) => {
28150
+ const get = async (frame, _client, ctx) => {
27085
28151
  const args = SessionIdArgs.parse(frame);
28152
+ ensureSessionAccess(ctx, args.sessionId, "read");
27086
28153
  const { response } = manager.get(args);
27087
28154
  return { response: { type: "session:info", ...response } };
27088
28155
  };
27089
- const update = async (frame) => {
28156
+ const update = async (frame, _client, ctx) => {
27090
28157
  const args = SessionUpdateArgs.parse(frame);
28158
+ ensureSessionAccess(ctx, args.sessionId, "send");
27091
28159
  const { response, broadcast } = manager.update(args);
27092
28160
  return { response: { type: "session:info", ...response }, broadcast };
27093
28161
  };
27094
- const del = async (frame) => {
28162
+ const del = async (frame, _client, ctx) => {
27095
28163
  const args = SessionIdArgs.parse(frame);
28164
+ ensureSessionAccess(ctx, args.sessionId, "send");
27096
28165
  const { broadcast } = manager.delete(args);
27097
28166
  return {
27098
28167
  response: { type: "session:deleted", sessionId: args.sessionId },
27099
28168
  broadcast
27100
28169
  };
27101
28170
  };
27102
- const send = async (frame) => {
28171
+ const send = async (frame, _client, ctx) => {
27103
28172
  const args = SessionSendArgs.parse(frame);
28173
+ ensureSessionAccess(ctx, args.sessionId, "send");
27104
28174
  const { broadcast } = manager.send(args);
27105
28175
  return { response: { type: "session:send", ok: true }, broadcast };
27106
28176
  };
27107
- const stop = async (frame) => {
28177
+ const stop = async (frame, _client, ctx) => {
27108
28178
  const args = SessionIdArgs.parse(frame);
28179
+ ensureSessionAccess(ctx, args.sessionId, "send");
27109
28180
  const { broadcast } = await manager.stop(args);
27110
28181
  return { response: { type: "session:stop", ok: true }, broadcast };
27111
28182
  };
27112
- const interrupt = async (frame) => {
28183
+ const interrupt = async (frame, _client, ctx) => {
27113
28184
  const args = SessionIdArgs.parse(frame);
28185
+ ensureSessionAccess(ctx, args.sessionId, "send");
27114
28186
  const { broadcast } = await manager.interrupt(args);
27115
28187
  return { response: { type: "session:interrupt", ok: true }, broadcast };
27116
28188
  };
27117
- const rewind = async (frame) => {
28189
+ const rewind = async (frame, _client, ctx) => {
27118
28190
  const args = SessionRewindArgs.parse(frame);
28191
+ ensureSessionAccess(ctx, args.sessionId, "send");
27119
28192
  const { response, broadcast } = await manager.rewind(args);
27120
28193
  return { response: { type: "session:rewind", ...response }, broadcast };
27121
28194
  };
27122
- const rewindDiff = async (frame) => {
28195
+ const rewindDiff = async (frame, _client, ctx) => {
27123
28196
  const args = SessionRewindDiffArgs.parse(frame);
28197
+ ensureSessionAccess(ctx, args.sessionId, "read");
27124
28198
  const { response } = await manager.rewindDiff(args);
27125
28199
  return { response: { type: "session:rewind-diff", ...response } };
27126
28200
  };
27127
- const rewindableMessageIds = async (frame) => {
28201
+ const rewindableMessageIds = async (frame, _client, ctx) => {
27128
28202
  const args = SessionRewindableMessageIdsArgs.parse(frame);
28203
+ ensureSessionAccess(ctx, args.sessionId, "read");
27129
28204
  const { response } = manager.rewindableMessageIds(args);
27130
28205
  return {
27131
28206
  response: { type: "session:rewindable-message-ids", ...response }
@@ -27136,19 +28211,22 @@ function buildSessionHandlers(deps) {
27136
28211
  const result = forkSession(args);
27137
28212
  return { response: { type: "session:fork", ...result } };
27138
28213
  };
27139
- const newSession = async (frame) => {
28214
+ const newSession = async (frame, _client, ctx) => {
27140
28215
  const args = SessionIdArgs.parse(frame);
28216
+ ensureSessionAccess(ctx, args.sessionId, "send");
27141
28217
  observer.stop(args.sessionId);
27142
28218
  const { response, broadcast } = manager.newSession(args);
27143
28219
  return { response: { type: "session:info", ...response }, broadcast };
27144
28220
  };
27145
- const resume = async (frame) => {
28221
+ const resume = async (frame, _client, ctx) => {
27146
28222
  const args = SessionResumeArgs.parse(frame);
28223
+ ensureSessionAccess(ctx, args.sessionId, "send");
27147
28224
  const { response, broadcast } = manager.resume(args);
27148
28225
  return { response: { type: "session:info", ...response }, broadcast };
27149
28226
  };
27150
- const observe = async (frame) => {
28227
+ const observe = async (frame, _client, ctx) => {
27151
28228
  const args = SessionObserveArgs.parse(frame);
28229
+ ensureSessionAccess(ctx, args.sessionId, "read");
27152
28230
  const { response: file } = manager.get({ sessionId: args.sessionId });
27153
28231
  const sessionFile = file;
27154
28232
  manager.ensureSession(sessionFile);
@@ -27162,14 +28240,17 @@ function buildSessionHandlers(deps) {
27162
28240
  });
27163
28241
  return { response: { type: "session:observe", ok: true } };
27164
28242
  };
27165
- const events = async (frame) => {
28243
+ const events = async (frame, _client, ctx) => {
27166
28244
  const args = SessionEventsArgs.parse(frame);
28245
+ ensureSessionAccess(ctx, args.sessionId, "read");
27167
28246
  const { response } = manager.getEvents(args);
27168
28247
  return { response: { type: "session:events", ...response } };
27169
28248
  };
27170
- const subscribe = async (frame, client) => {
27171
- if (typeof frame.sessionId === "string")
28249
+ const subscribe = async (frame, client, ctx) => {
28250
+ if (typeof frame.sessionId === "string") {
28251
+ ensureSessionAccess(ctx, frame.sessionId, "read");
27172
28252
  addSubscription(client, frame.sessionId);
28253
+ }
27173
28254
  return {
27174
28255
  response: { type: "subscribed", sessionId: frame.sessionId }
27175
28256
  };
@@ -27181,8 +28262,9 @@ function buildSessionHandlers(deps) {
27181
28262
  response: { type: "unsubscribed", sessionId: frame.sessionId }
27182
28263
  };
27183
28264
  };
27184
- const pin = async (frame) => {
28265
+ const pin = async (frame, _client, ctx) => {
27185
28266
  const args = SessionPinArgs.parse(frame);
28267
+ ensureSessionAccess(ctx, args.sessionId, "send");
27186
28268
  const { response, broadcast } = manager.pin(args);
27187
28269
  return { response: { type: "session:info", ...response }, broadcast };
27188
28270
  };
@@ -27191,13 +28273,15 @@ function buildSessionHandlers(deps) {
27191
28273
  const { response, broadcast } = manager.reorderPins(args);
27192
28274
  return { response: { type: "session:reorderPins", ...response }, broadcast };
27193
28275
  };
27194
- const answerQuestion = async (frame) => {
28276
+ const answerQuestion = async (frame, _client, ctx) => {
27195
28277
  const args = AnswerQuestionArgs.parse(frame);
28278
+ ensureSessionAccess(ctx, args.sessionId, "send");
27196
28279
  const { response, broadcast } = manager.answerQuestion(args);
27197
28280
  return { response: { type: "session:answerQuestion", ...response }, broadcast };
27198
28281
  };
27199
- const cancelQuestion = async (frame) => {
28282
+ const cancelQuestion = async (frame, _client, ctx) => {
27200
28283
  const args = CancelQuestionArgs.parse(frame);
28284
+ ensureSessionAccess(ctx, args.sessionId, "send");
27201
28285
  const { response, broadcast } = await manager.cancelQuestion(args);
27202
28286
  return { response: { type: "session:cancelQuestion", ...response }, broadcast };
27203
28287
  };
@@ -27292,6 +28376,7 @@ function listRecentDirs(store, limit = 50) {
27292
28376
  }
27293
28377
 
27294
28378
  // src/handlers/history.ts
28379
+ init_protocol();
27295
28380
  function buildHistoryHandlers(deps) {
27296
28381
  const { manager, history, store } = deps;
27297
28382
  const projects = async () => {
@@ -27303,17 +28388,27 @@ function buildHistoryHandlers(deps) {
27303
28388
  const sessions = await history.listSessions(args);
27304
28389
  return { response: { type: "history:list", sessions } };
27305
28390
  };
27306
- const read = async (frame) => {
28391
+ const read = async (frame, _client, ctx) => {
27307
28392
  const args = HistoryReadArgs.parse(frame);
27308
28393
  const { response: file } = manager.get({ sessionId: args.sessionId });
27309
- if (!file.toolSessionId) {
28394
+ const f = file;
28395
+ if (ctx && ctx.principal.kind === "guest") {
28396
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
28397
+ if (!ok) {
28398
+ throw new ClawdError(
28399
+ ERROR_CODES.UNAUTHORIZED,
28400
+ `principal guest:${ctx.principal.id} cannot read history of session ${args.sessionId}`
28401
+ );
28402
+ }
28403
+ }
28404
+ if (!f.toolSessionId) {
27310
28405
  return {
27311
28406
  response: { type: "history:read", messages: [], total: 0, offset: 0 }
27312
28407
  };
27313
28408
  }
27314
28409
  const res = await history.read({
27315
- cwd: file.cwd,
27316
- toolSessionId: file.toolSessionId,
28410
+ cwd: f.cwd,
28411
+ toolSessionId: f.toolSessionId,
27317
28412
  limit: args.limit,
27318
28413
  offset: args.offset
27319
28414
  });
@@ -27495,6 +28590,345 @@ function buildCapabilitiesHandlers(deps) {
27495
28590
  };
27496
28591
  }
27497
28592
 
28593
+ // src/handlers/capability.ts
28594
+ init_zod();
28595
+ init_protocol();
28596
+ var DeleteArgsSchema = external_exports.object({
28597
+ capabilityId: external_exports.string().min(1)
28598
+ }).strict();
28599
+ function buildCapabilityHandlers(deps) {
28600
+ const { manager, getShareBaseUrl } = deps;
28601
+ const issue = async (frame) => {
28602
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28603
+ const args = CapabilityIssueArgsSchema.parse(rest);
28604
+ const { token, capability } = manager.issue({
28605
+ displayName: args.displayName,
28606
+ grants: args.grants,
28607
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
28608
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
28609
+ });
28610
+ const base = getShareBaseUrl().replace(/\/$/, "");
28611
+ const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
28612
+ return {
28613
+ response: {
28614
+ type: "capability:issued",
28615
+ token,
28616
+ shareUrl,
28617
+ capability: stripSecretHash(capability)
28618
+ }
28619
+ };
28620
+ };
28621
+ const list = async () => {
28622
+ return {
28623
+ response: {
28624
+ type: "capability:list",
28625
+ capabilities: manager.list().map(stripSecretHash)
28626
+ }
28627
+ };
28628
+ };
28629
+ const del = async (frame) => {
28630
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28631
+ const args = DeleteArgsSchema.parse(rest);
28632
+ const result = manager.delete(args.capabilityId);
28633
+ if (!result) {
28634
+ throw new ClawdError(
28635
+ ERROR_CODES.VALIDATION_ERROR,
28636
+ `capability not found: ${args.capabilityId}`
28637
+ );
28638
+ }
28639
+ return {
28640
+ response: {
28641
+ type: "capability:deleted",
28642
+ capabilityId: args.capabilityId
28643
+ }
28644
+ };
28645
+ };
28646
+ const bindPeer = async (frame) => {
28647
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28648
+ const args = CapabilityBindPeerArgsSchema.parse(rest);
28649
+ try {
28650
+ manager.recordPeerHello(args.capabilityId, {
28651
+ ownerPrincipalId: args.peerOwnerId,
28652
+ displayName: args.peerDisplayName,
28653
+ now: Date.now()
28654
+ });
28655
+ } catch (e) {
28656
+ throw new ClawdError(
28657
+ ERROR_CODES.VALIDATION_ERROR,
28658
+ `capability:bindPeer: ${e.message}`
28659
+ );
28660
+ }
28661
+ return {
28662
+ response: { type: "capability:bindPeer:ok", capabilityId: args.capabilityId }
28663
+ };
28664
+ };
28665
+ return {
28666
+ "capability:issue": issue,
28667
+ "capability:list": list,
28668
+ "capability:delete": del,
28669
+ "capability:bindPeer": bindPeer
28670
+ };
28671
+ }
28672
+
28673
+ // src/handlers/inbox.ts
28674
+ init_protocol();
28675
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
28676
+ if (ctx.principal.kind === "owner") {
28677
+ if (!argsPeerOwnerId) {
28678
+ throw new ClawdError(
28679
+ ERROR_CODES.VALIDATION_ERROR,
28680
+ "inbox: owner ctx requires peerOwnerId"
28681
+ );
28682
+ }
28683
+ return argsPeerOwnerId;
28684
+ }
28685
+ if (!ctx.capabilityId) {
28686
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
28687
+ }
28688
+ const cap = capManager.findById(ctx.capabilityId);
28689
+ if (!cap?.peerOwnerId) {
28690
+ throw new ClawdError(
28691
+ ERROR_CODES.UNAUTHORIZED,
28692
+ `inbox: guest cap ${ctx.capabilityId} has no peerOwnerId (recordPeerHello not yet)`
28693
+ );
28694
+ }
28695
+ if (argsPeerOwnerId && argsPeerOwnerId !== cap.peerOwnerId) {
28696
+ throw new ClawdError(
28697
+ ERROR_CODES.UNAUTHORIZED,
28698
+ `inbox: guest args.peerOwnerId mismatch (cap.peerOwnerId=${cap.peerOwnerId}, args=${argsPeerOwnerId})`
28699
+ );
28700
+ }
28701
+ return cap.peerOwnerId;
28702
+ }
28703
+ function buildInboxHandlers(deps) {
28704
+ const { manager, capManager } = deps;
28705
+ const postMessage = async (frame, _client, ctx) => {
28706
+ const { type: _t, requestId: _r, ...rest } = frame;
28707
+ const args = InboxPostMessageArgsSchema.parse(rest);
28708
+ if (!ctx) {
28709
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
28710
+ }
28711
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28712
+ const message = manager.postMessage({
28713
+ peerOwnerId,
28714
+ senderPrincipalId: ctx.principal.id,
28715
+ text: args.text,
28716
+ id: args.id,
28717
+ createdAt: args.createdAt
28718
+ });
28719
+ return {
28720
+ response: { type: "inbox:postMessage:ok", message }
28721
+ };
28722
+ };
28723
+ const list = async (frame, _client, ctx) => {
28724
+ const { type: _t, requestId: _r, ...rest } = frame;
28725
+ const args = InboxListArgsSchema.parse(rest);
28726
+ if (!ctx) {
28727
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
28728
+ }
28729
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28730
+ const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
28731
+ return {
28732
+ response: { type: "inbox:list:ok", peerOwnerId, messages }
28733
+ };
28734
+ };
28735
+ const markRead = async (frame, _client, ctx) => {
28736
+ const { type: _t, requestId: _r, ...rest } = frame;
28737
+ const args = InboxMarkReadArgsSchema.parse(rest);
28738
+ if (!ctx) {
28739
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
28740
+ }
28741
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28742
+ const updated = manager.markRead({
28743
+ peerOwnerId,
28744
+ principalId: ctx.principal.id,
28745
+ upToCreatedAt: args.upToCreatedAt
28746
+ });
28747
+ return {
28748
+ response: {
28749
+ type: "inbox:markRead:ok",
28750
+ peerOwnerId,
28751
+ upToCreatedAt: args.upToCreatedAt,
28752
+ updated
28753
+ }
28754
+ };
28755
+ };
28756
+ return {
28757
+ "inbox:postMessage": postMessage,
28758
+ "inbox:list": list,
28759
+ "inbox:markRead": markRead
28760
+ };
28761
+ }
28762
+
28763
+ // src/handlers/received-capability.ts
28764
+ init_protocol();
28765
+ function ensureOwner(ctx) {
28766
+ if (!ctx || ctx.principal.kind !== "owner") {
28767
+ throw new ClawdError(
28768
+ ERROR_CODES.UNAUTHORIZED,
28769
+ "UNAUTHORIZED: received-capability:* requires owner ctx"
28770
+ );
28771
+ }
28772
+ }
28773
+ function ensureGuestCtx(ctx) {
28774
+ if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
28775
+ throw new ClawdError(
28776
+ ERROR_CODES.UNAUTHORIZED,
28777
+ "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
28778
+ );
28779
+ }
28780
+ }
28781
+ function buildReceivedCapabilityHandlers(deps) {
28782
+ const now = deps.now ?? Date.now;
28783
+ const list = async (_frame, _client, ctx) => {
28784
+ ensureOwner(ctx);
28785
+ return {
28786
+ response: {
28787
+ type: "received-capability:list:ok",
28788
+ receivedCaps: deps.store.list()
28789
+ }
28790
+ };
28791
+ };
28792
+ const add = async (frame, _client, ctx) => {
28793
+ ensureOwner(ctx);
28794
+ const { type: _t, requestId: _r, ...rest } = frame;
28795
+ const args = ReceivedCapabilityAddArgsSchema.parse(rest);
28796
+ let conn;
28797
+ try {
28798
+ conn = await deps.connectRemote({
28799
+ url: args.remoteUrl,
28800
+ token: args.token,
28801
+ selfPrincipalId: deps.selfPrincipalId(),
28802
+ selfDisplayName: deps.selfDisplayName()
28803
+ });
28804
+ } catch (e) {
28805
+ throw new ClawdError(
28806
+ ERROR_CODES.VALIDATION_ERROR,
28807
+ `INVITE_REMOTE_UNREACHABLE: ${e.message}`
28808
+ );
28809
+ }
28810
+ try {
28811
+ const wh = await conn.whoami();
28812
+ const rc = {
28813
+ ownerPrincipalId: wh.ownerId,
28814
+ ownerDisplayName: wh.displayName,
28815
+ remoteUrl: args.remoteUrl,
28816
+ capabilityId: wh.capabilityId,
28817
+ capabilityToken: args.token,
28818
+ grants: wh.grants,
28819
+ receivedAt: now()
28820
+ };
28821
+ deps.store.upsert(rc);
28822
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
28823
+ return {
28824
+ response: { type: "received-capability:add:ok", receivedCap: rc }
28825
+ };
28826
+ } finally {
28827
+ try {
28828
+ conn.close();
28829
+ } catch {
28830
+ }
28831
+ }
28832
+ };
28833
+ const remove = async (frame, _client, ctx) => {
28834
+ ensureOwner(ctx);
28835
+ const { type: _t, requestId: _r, ...rest } = frame;
28836
+ const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
28837
+ deps.store.remove(args.ownerPrincipalId);
28838
+ deps.broadcast({
28839
+ type: "received-capability:removed",
28840
+ ownerPrincipalId: args.ownerPrincipalId
28841
+ });
28842
+ return {
28843
+ response: {
28844
+ type: "received-capability:remove:ok",
28845
+ ownerPrincipalId: args.ownerPrincipalId
28846
+ }
28847
+ };
28848
+ };
28849
+ const autoAttach = async (frame, _client, ctx) => {
28850
+ ensureGuestCtx(ctx);
28851
+ const { type: _t, requestId: _r, ...rest } = frame;
28852
+ const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
28853
+ const rc = {
28854
+ ownerPrincipalId: args.ownerPrincipalId,
28855
+ ownerDisplayName: args.ownerDisplayName,
28856
+ remoteUrl: args.remoteUrl,
28857
+ capabilityId: args.capabilityId,
28858
+ capabilityToken: args.token,
28859
+ grants: args.grants,
28860
+ receivedAt: now()
28861
+ };
28862
+ deps.store.upsert(rc);
28863
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
28864
+ return {
28865
+ response: { type: "received-capability:autoAttach:ok" }
28866
+ };
28867
+ };
28868
+ return {
28869
+ "received-capability:list": list,
28870
+ "received-capability:add": add,
28871
+ "received-capability:remove": remove,
28872
+ "received-capability:autoAttach": autoAttach
28873
+ };
28874
+ }
28875
+
28876
+ // src/handlers/whoami.ts
28877
+ init_protocol();
28878
+ function buildWhoamiHandler(deps) {
28879
+ return async (_frame, _client, ctx) => {
28880
+ if (!ctx) {
28881
+ throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
28882
+ }
28883
+ const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
28884
+ let capability;
28885
+ if (ctx.principal.kind === "owner") {
28886
+ capability = {
28887
+ id: deps.ownerPrincipalId,
28888
+ displayName: deps.ownerDisplayName,
28889
+ grants: ctx.grants,
28890
+ issuedAt: 0,
28891
+ usedCount: 0
28892
+ };
28893
+ } else {
28894
+ if (!ctx.capabilityId) {
28895
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
28896
+ }
28897
+ const cap = deps.capabilityManager.findById(ctx.capabilityId);
28898
+ if (!cap) {
28899
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
28900
+ }
28901
+ capability = stripSecretHash(cap);
28902
+ }
28903
+ const grantedPersonas = [];
28904
+ const isGuest = ctx.principal.kind === "guest";
28905
+ const hasWildcard = capability.grants.some((g2) => g2.resource.type === "*");
28906
+ if (hasWildcard) {
28907
+ for (const id of deps.personaStore.list()) {
28908
+ const file = deps.personaStore.read(id);
28909
+ if (!file) continue;
28910
+ if (isGuest && !file.public) continue;
28911
+ grantedPersonas.push({ id: file.personaId, displayName: file.label });
28912
+ }
28913
+ } else {
28914
+ for (const g2 of capability.grants) {
28915
+ if (g2.resource.type !== "persona") continue;
28916
+ const file = deps.personaStore.read(g2.resource.id);
28917
+ if (!file) continue;
28918
+ grantedPersonas.push({ id: file.personaId, displayName: file.label });
28919
+ }
28920
+ }
28921
+ return {
28922
+ response: {
28923
+ type: "whoami:ok",
28924
+ owner,
28925
+ capability,
28926
+ grantedPersonas
28927
+ }
28928
+ };
28929
+ };
28930
+ }
28931
+
27498
28932
  // src/handlers/meta.ts
27499
28933
  var import_node_os13 = __toESM(require("os"), 1);
27500
28934
  init_protocol();
@@ -27558,9 +28992,14 @@ function buildPersonaHandlers(deps) {
27558
28992
  const persona = personaManager.create(args);
27559
28993
  return { response: { type: "persona:info", ...persona } };
27560
28994
  };
27561
- const list = async () => {
28995
+ const list = async (_frame, _client, ctx) => {
28996
+ const all = personaRegistry.list();
28997
+ const isGuest = ctx?.principal.kind === "guest";
28998
+ const personas = isGuest ? all.filter(
28999
+ (p2) => p2.public && assertGrant(ctx.grants, { type: "persona", id: p2.personaId }, "read")
29000
+ ) : all;
27562
29001
  return {
27563
- response: { type: "persona:list", personas: personaRegistry.list() }
29002
+ response: { type: "persona:list", personas }
27564
29003
  };
27565
29004
  };
27566
29005
  const get = async (frame) => {
@@ -27571,7 +29010,6 @@ function buildPersonaHandlers(deps) {
27571
29010
  }
27572
29011
  const personality = personaManager.readPersonality(args.personaId) ?? "";
27573
29012
  const skills = personaManager.listSkills(args.personaId);
27574
- const plugins = personaManager.listEnabledPlugins(args.personaId);
27575
29013
  const sandboxSettings = personaManager.readSandboxSettings(args.personaId);
27576
29014
  return {
27577
29015
  response: {
@@ -27579,7 +29017,6 @@ function buildPersonaHandlers(deps) {
27579
29017
  ...persona,
27580
29018
  personality,
27581
29019
  skills,
27582
- plugins,
27583
29020
  sandboxSettings
27584
29021
  }
27585
29022
  };
@@ -27598,31 +29035,16 @@ function buildPersonaHandlers(deps) {
27598
29035
  response: { type: "persona:deleted", personaId: args.personaId }
27599
29036
  };
27600
29037
  };
27601
- const issueToken = async (frame) => {
27602
- const args = PersonaIssueTokenArgsSchema.parse(frame);
27603
- const { token, persona } = personaManager.issueToken(args.personaId, args.label);
27604
- return {
27605
- response: { type: "persona:tokenIssued", token, persona }
27606
- };
27607
- };
27608
- const revokeToken = async (frame) => {
27609
- const args = PersonaRevokeTokenArgsSchema.parse(frame);
27610
- const persona = personaManager.revokeToken(args.personaId, args.token);
27611
- return { response: { type: "persona:info", ...persona } };
27612
- };
27613
29038
  return {
27614
29039
  "persona:create": create,
27615
29040
  "persona:list": list,
27616
29041
  "persona:get": get,
27617
29042
  "persona:update": update,
27618
- "persona:delete": del,
27619
- "persona:issueToken": issueToken,
27620
- "persona:revokeToken": revokeToken
29043
+ "persona:delete": del
27621
29044
  };
27622
29045
  }
27623
29046
 
27624
29047
  // src/handlers/attachment.ts
27625
- var import_node_path26 = __toESM(require("path"), 1);
27626
29048
  init_protocol();
27627
29049
  init_protocol();
27628
29050
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -27647,41 +29069,8 @@ function buildAttachmentHandlers(deps) {
27647
29069
  "httpBaseUrl unavailable (daemon HTTP not ready)"
27648
29070
  );
27649
29071
  }
27650
- if (!deps.sessionStore || !deps.getSessionScope || !deps.groupFileStore) {
27651
- throw new ClawdError(
27652
- ERROR_CODES.METHOD_NOT_IMPLEMENTED,
27653
- "signUrl requires session/group stores"
27654
- );
27655
- }
27656
- const sessionFile = deps.sessionStore.read(args.sessionId);
27657
- if (!sessionFile) {
27658
- throw new ClawdError(
27659
- ERROR_CODES.VALIDATION_ERROR,
27660
- `session ${args.sessionId} not found`
27661
- );
27662
- }
27663
- const scope = deps.getSessionScope(args.sessionId);
27664
- if (!scope) {
27665
- throw new ClawdError(
27666
- ERROR_CODES.VALIDATION_ERROR,
27667
- `session ${args.sessionId} scope unresolved`
27668
- );
27669
- }
27670
- const cwdAbs = import_node_path26.default.resolve(sessionFile.cwd);
27671
- const candidateAbs = import_node_path26.default.isAbsolute(args.relPath) ? import_node_path26.default.resolve(args.relPath) : import_node_path26.default.resolve(cwdAbs, args.relPath);
27672
- const entries = deps.groupFileStore.list(scope, args.sessionId);
27673
- const entry = entries.find((e) => {
27674
- const storedAbs = import_node_path26.default.isAbsolute(e.relPath) ? import_node_path26.default.resolve(e.relPath) : import_node_path26.default.resolve(cwdAbs, e.relPath);
27675
- return storedAbs === candidateAbs && !e.stale;
27676
- });
27677
- if (!entry) {
27678
- throw new ClawdError(
27679
- ERROR_CODES.VALIDATION_ERROR,
27680
- `relPath not in session group files or stale: ${args.relPath}`
27681
- );
27682
- }
27683
29072
  const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
27684
- const parts = signUrlParts(secret, candidateAbs, ttl);
29073
+ const parts = signUrlParts(secret, args.absPath, ttl);
27685
29074
  const url = buildSignedFileUrl(httpBaseUrl, parts);
27686
29075
  return {
27687
29076
  response: {
@@ -27748,23 +29137,11 @@ function buildAttachmentHandlers(deps) {
27748
29137
  const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
27749
29138
  return { response: { type: "attachment.groupList", entries } };
27750
29139
  };
27751
- const groupListPersona = async (frame) => {
27752
- if (!deps.groupFileStore) {
27753
- throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
27754
- }
27755
- const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
27756
- if (!parsed.success) {
27757
- throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
27758
- }
27759
- const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
27760
- return { response: { type: "attachment.groupListPersona", perSession } };
27761
- };
27762
29140
  return {
27763
29141
  "attachment.signUrl": signUrl,
27764
29142
  "attachment.groupAdd": groupAdd,
27765
29143
  "attachment.groupRemove": groupRemove,
27766
- "attachment.groupList": groupList,
27767
- "attachment.groupListPersona": groupListPersona
29144
+ "attachment.groupList": groupList
27768
29145
  };
27769
29146
  }
27770
29147
 
@@ -27782,15 +29159,141 @@ function buildMethodHandlers(deps) {
27782
29159
  personaManager: deps.personaManager,
27783
29160
  personaRegistry: deps.personaRegistry
27784
29161
  }),
29162
+ ...buildCapabilityHandlers({
29163
+ manager: deps.capabilityManager,
29164
+ getShareBaseUrl: deps.getShareBaseUrl
29165
+ }),
29166
+ ...buildInboxHandlers({
29167
+ manager: deps.inboxManager,
29168
+ capManager: deps.capabilityManager
29169
+ }),
29170
+ ...buildReceivedCapabilityHandlers({
29171
+ store: deps.receivedCapabilityStore,
29172
+ connectRemote: deps.connectRemote,
29173
+ broadcast: deps.broadcastToOwners,
29174
+ selfPrincipalId: () => deps.ownerPrincipalId,
29175
+ selfDisplayName: () => deps.ownerDisplayName
29176
+ }),
29177
+ whoami: buildWhoamiHandler({
29178
+ ownerDisplayName: deps.ownerDisplayName,
29179
+ ownerPrincipalId: deps.ownerPrincipalId,
29180
+ personaStore: deps.personaStore,
29181
+ capabilityManager: deps.capabilityManager
29182
+ }),
27785
29183
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27786
29184
  };
27787
29185
  }
27788
29186
 
29187
+ // src/handlers/method-grants.ts
29188
+ var ADMIN_ANY = {
29189
+ kind: "fixed",
29190
+ resource: { type: "*" },
29191
+ action: "admin"
29192
+ };
29193
+ var CAPABILITY_SCOPED = { kind: "capability-scoped" };
29194
+ var METHOD_GRANT_MAP = {
29195
+ // ---- public(meta-only,guest 也能调) ----
29196
+ "info": { kind: "public" },
29197
+ "ping": { kind: "public" },
29198
+ // v2 Phase 6: whoami 是身份反查,任何 authed connection (owner / guest) 都能调;
29199
+ // handler 内部按 ctx.principal.kind 决定返回 "合成 owner self capability" 还是
29200
+ // "查 capabilityManager 拿 caller 的 guest capability"。
29201
+ "whoami": { kind: "public" },
29202
+ // ---- capability platform(admin-only,本 PR 新增) ----
29203
+ "capability:issue": ADMIN_ANY,
29204
+ "capability:list": ADMIN_ANY,
29205
+ "capability:delete": ADMIN_ANY,
29206
+ "capability:bindPeer": ADMIN_ANY,
29207
+ // ---- inbox: channel-based IM (capability platform v3) ----
29208
+ // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
29209
+ // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
29210
+ "inbox:list": { kind: "public" },
29211
+ "inbox:markRead": { kind: "public" },
29212
+ "inbox:postMessage": { kind: "public" },
29213
+ // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
29214
+ // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
29215
+ "received-capability:list": ADMIN_ANY,
29216
+ "received-capability:add": ADMIN_ANY,
29217
+ "received-capability:remove": ADMIN_ANY,
29218
+ "received-capability:autoAttach": { kind: "public" },
29219
+ // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
29220
+ // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
29221
+ // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
29222
+ "session:create": CAPABILITY_SCOPED,
29223
+ "session:list": CAPABILITY_SCOPED,
29224
+ "session:get": CAPABILITY_SCOPED,
29225
+ "session:update": CAPABILITY_SCOPED,
29226
+ "session:delete": CAPABILITY_SCOPED,
29227
+ "session:send": CAPABILITY_SCOPED,
29228
+ "session:stop": CAPABILITY_SCOPED,
29229
+ "session:interrupt": CAPABILITY_SCOPED,
29230
+ "session:rewind": CAPABILITY_SCOPED,
29231
+ "session:rewind-diff": CAPABILITY_SCOPED,
29232
+ "session:rewindable-message-ids": CAPABILITY_SCOPED,
29233
+ "session:fork": CAPABILITY_SCOPED,
29234
+ "session:new": CAPABILITY_SCOPED,
29235
+ "session:resume": CAPABILITY_SCOPED,
29236
+ "session:observe": CAPABILITY_SCOPED,
29237
+ "session:events": CAPABILITY_SCOPED,
29238
+ "session:subscribe": CAPABILITY_SCOPED,
29239
+ "session:unsubscribe": CAPABILITY_SCOPED,
29240
+ "session:pin": CAPABILITY_SCOPED,
29241
+ "session:reorderPins": ADMIN_ANY,
29242
+ // owner 全局操作,无 personaId 维度
29243
+ "permission:respond": CAPABILITY_SCOPED,
29244
+ "session:answerQuestion": CAPABILITY_SCOPED,
29245
+ "session:cancelQuestion": CAPABILITY_SCOPED,
29246
+ "history:projects": ADMIN_ANY,
29247
+ "history:list": ADMIN_ANY,
29248
+ // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
29249
+ // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
29250
+ // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
29251
+ "history:read": CAPABILITY_SCOPED,
29252
+ "history:subagents": ADMIN_ANY,
29253
+ "history:subagent-read": ADMIN_ANY,
29254
+ "history:recentDirs": ADMIN_ANY,
29255
+ "workspace:list": ADMIN_ANY,
29256
+ "workspace:read": ADMIN_ANY,
29257
+ "skills:list": ADMIN_ANY,
29258
+ "agents:list": ADMIN_ANY,
29259
+ "git:root": ADMIN_ANY,
29260
+ "git:branch": ADMIN_ANY,
29261
+ "git:branches": ADMIN_ANY,
29262
+ "capabilities:get": ADMIN_ANY,
29263
+ "persona:create": ADMIN_ANY,
29264
+ // persona:list 下沉到 CAPABILITY_SCOPED:guest 调用合法(用于 People 面板展开
29265
+ // 对方分享的 personas),handler 端按 ctx + persona.public + assertGrant 过滤。
29266
+ "persona:list": CAPABILITY_SCOPED,
29267
+ "persona:get": ADMIN_ANY,
29268
+ "persona:update": ADMIN_ANY,
29269
+ "persona:delete": ADMIN_ANY,
29270
+ "session:pty:input": ADMIN_ANY,
29271
+ "session:pty:resize": ADMIN_ANY,
29272
+ // file-sharing attachment.* RPC:dispatcher 用 admin-only 兜底(wire-level 拦),
29273
+ // 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
29274
+ "attachment.signUrl": ADMIN_ANY,
29275
+ "attachment.groupAdd": ADMIN_ANY,
29276
+ "attachment.groupRemove": ADMIN_ANY,
29277
+ "attachment.groupList": ADMIN_ANY
29278
+ };
29279
+ function computeGrantForFrame(method, frame) {
29280
+ const rule = METHOD_GRANT_MAP[method];
29281
+ if (!rule) return { kind: "public" };
29282
+ if (rule.kind === "public") return { kind: "public" };
29283
+ if (rule.kind === "capability-scoped") return { kind: "public" };
29284
+ if (rule.kind === "fixed") {
29285
+ return { kind: "check", resource: rule.resource, action: rule.action };
29286
+ }
29287
+ const picked = rule.pick(frame);
29288
+ if (!picked) return { kind: "public" };
29289
+ return { kind: "check", resource: picked.resource, action: picked.action };
29290
+ }
29291
+
27789
29292
  // src/index.ts
27790
29293
  async function startDaemon(config) {
27791
29294
  const logger = createLogger({
27792
29295
  level: config.logLevel,
27793
- file: import_node_path27.default.join(config.dataDir, "clawd.log")
29296
+ file: import_node_path26.default.join(config.dataDir, "clawd.log")
27794
29297
  });
27795
29298
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
27796
29299
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -27801,47 +29304,116 @@ async function startDaemon(config) {
27801
29304
  if (pre.status === "stale") {
27802
29305
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
27803
29306
  }
29307
+ const auth = loadOrCreateAuth({ dataDir: config.dataDir });
27804
29308
  let resolvedAuthToken = null;
27805
- let authFile = null;
27806
29309
  if (config.authToken && config.authToken.trim()) {
27807
29310
  resolvedAuthToken = config.authToken.trim();
27808
29311
  } else if (config.tunnel) {
27809
- authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27810
- resolvedAuthToken = authFile.token;
29312
+ resolvedAuthToken = auth.token;
27811
29313
  }
29314
+ const ownerPrincipalId = auth.ownerPrincipalId;
29315
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
27812
29316
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
29317
+ migrateLegacyFiles(config.dataDir);
29318
+ const inboxStore = new InboxStore(config.dataDir);
29319
+ const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
29320
+ wsServer?.broadcastToOwners(frame);
29321
+ });
29322
+ const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
29323
+ receivedCapabilityStore.load();
29324
+ const capabilityStore = new CapabilityStore(config.dataDir);
29325
+ const capabilityRegistry = new CapabilityRegistry(capabilityStore);
29326
+ const capabilityManager = new CapabilityManager(capabilityRegistry, {
29327
+ onIssued: (cap, token) => {
29328
+ wsServer?.broadcastToOwners({
29329
+ type: "capability:tokenIssued",
29330
+ capability: stripSecretHash(cap),
29331
+ token
29332
+ });
29333
+ },
29334
+ onDeleted: (cap) => {
29335
+ const deletedAt = Date.now();
29336
+ wsServer?.broadcastToOwners({
29337
+ type: "capability:tokenDeleted",
29338
+ capabilityId: cap.id,
29339
+ deletedAt
29340
+ });
29341
+ wsServer?.closeConnectionsByCapability(cap.id);
29342
+ const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
29343
+ if (cleanup.removed.length > 0) {
29344
+ logger.info("capability delete cascade: guest sessions removed", {
29345
+ capabilityId: cap.id,
29346
+ removedDirs: cleanup.removed
29347
+ });
29348
+ }
29349
+ }
29350
+ });
29351
+ migrateDropPersonsDir(config.dataDir);
27813
29352
  let wsServer = null;
27814
29353
  const authGate = authMode === "first-message" ? new AuthGate({
27815
29354
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
29355
+ // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
29356
+ // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
27816
29357
  expectedToken: resolvedAuthToken,
29358
+ authenticate: (t, selfPrincipalId, selfDisplayName) => {
29359
+ return authenticate(t, {
29360
+ isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
29361
+ ownerPrincipalId,
29362
+ ownerDisplayName,
29363
+ capabilityRegistry,
29364
+ selfPrincipalId,
29365
+ selfDisplayName,
29366
+ onGuestHello: (capId, ownerId, name) => {
29367
+ capabilityManager.recordPeerHello(capId, {
29368
+ ownerPrincipalId: ownerId,
29369
+ displayName: name,
29370
+ now: Date.now()
29371
+ });
29372
+ }
29373
+ });
29374
+ },
29375
+ onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
29376
+ buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
27817
29377
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
27818
29378
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
27819
29379
  }) : null;
27820
29380
  resetRegistry();
27821
- const store = new SessionStore({ dataDir: config.dataDir });
29381
+ const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
29382
+ if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
29383
+ logger.info("sessions migration applied", {
29384
+ movedBare: migrateResult.movedBare,
29385
+ movedVmOwner: migrateResult.movedVmOwner,
29386
+ archivedListener: migrateResult.archivedListener
29387
+ });
29388
+ }
29389
+ const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
29390
+ const store = sessionStoreFactory.forBare();
27822
29391
  const workspace = new WorkspaceBrowser();
27823
29392
  const skills = new SkillsScanner();
27824
29393
  const agents = new AgentsScanner();
27825
29394
  const history = new ClaudeHistoryReader();
27826
29395
  let transport = null;
27827
- const personaStore = new PersonaStore(import_node_path27.default.join(config.dataDir, "personas"));
29396
+ const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
27828
29397
  const defaultsRoot = findDefaultsRoot();
27829
29398
  if (defaultsRoot) {
27830
29399
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
27831
29400
  } else {
27832
29401
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
27833
29402
  }
27834
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
27835
29403
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
27836
29404
  const manager = new SessionManager({
27837
29405
  store,
29406
+ // Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
29407
+ // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
29408
+ storeFactory: sessionStoreFactory,
27838
29409
  logger,
27839
29410
  getAdapter,
27840
29411
  historyReader: history,
27841
29412
  dataDir: config.dataDir,
27842
- personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
29413
+ personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
27843
29414
  personaStore,
27844
29415
  ownerDisplayName,
29416
+ ownerPrincipalId,
27845
29417
  mode: config.mode,
27846
29418
  broadcastFrame: (frame, target) => {
27847
29419
  if (target === "all") {
@@ -27862,7 +29434,7 @@ async function startDaemon(config) {
27862
29434
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
27863
29435
  attachmentGroup: {
27864
29436
  onFileEdit: (input) => {
27865
- const absPath = import_node_path27.default.isAbsolute(input.relPath) ? input.relPath : import_node_path27.default.join(input.cwd, input.relPath);
29437
+ const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
27866
29438
  let size = 0;
27867
29439
  try {
27868
29440
  size = import_node_fs24.default.statSync(absPath).size;
@@ -27961,43 +29533,63 @@ async function startDaemon(config) {
27961
29533
  httpToken: resolvedAuthToken,
27962
29534
  // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
27963
29535
  // 根据 sessionId 反查 scope 写盘。
27964
- //
27965
- // sessionStore / getSessionScope 都走 manager 的跨 scope 公开 API(findOwnedSession /
27966
- // findOwnedSessionScope)—— 否则 default-only SessionStore 找不到 persona owner-mode
27967
- // session,attachment 整套 RPC 对 persona session 都会报 "session not found"。
27968
- // 详见 fix(daemon) #703:file-sharing v1 预存 wiring bug,#701 把 desktop 默认 --tunnel
27969
- // 后首次让 desktop 用户用上 file-sharing 才被暴露。
27970
29536
  attachment: {
27971
29537
  groupFileStore,
27972
- sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
27973
29538
  getHttpBaseUrl,
27974
- // HMAC sign secret:~/.clawd/auth.json signSecret 字段(与 WS Bearer token 独立)。
27975
- // --auth-token CLI 模式 / noAuth 模式 authFile 为 null → handler 自己返 NOT_IMPLEMENTED。
27976
- getSignSecret: () => authFile?.signSecret ?? "",
27977
- // group RPC + sign 都用:根据 sessionId 反查 scopeowner-mode persona session 走
29539
+ // HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
29540
+ // noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
29541
+ getSignSecret: () => resolvedAuthToken ?? "",
29542
+ // group RPC:根据 sessionId 反查 scopeowner-mode persona session 走
27978
29543
  // 'persona/<pid>/owner',default 走 'default'。
27979
- getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
27980
- }
29544
+ getSessionScope: (sessionId) => {
29545
+ const file = store.read(sessionId);
29546
+ if (!file) return null;
29547
+ if (file.ownerPersonaId) {
29548
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
29549
+ }
29550
+ return { kind: "default" };
29551
+ }
29552
+ },
29553
+ // Task 1.9: capability:issue/list/revoke handler 依赖
29554
+ capabilityManager,
29555
+ // v2 Phase 5: capability:issue 返回的 shareUrl 用此 base URL 拼接。
29556
+ // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
29557
+ getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
29558
+ // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
29559
+ ownerDisplayName,
29560
+ // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
29561
+ ownerPrincipalId,
29562
+ personaStore,
29563
+ // capability handler 也用 (capability:issue 走 registry / capability:list)
29564
+ capabilityRegistry,
29565
+ // capability platform v3 inbox: handler 接 manager (post/list/markRead),
29566
+ // cascade 接 store (list 统计 deletedInboxEvents).
29567
+ inboxManager,
29568
+ inboxStore,
29569
+ // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
29570
+ receivedCapabilityStore,
29571
+ // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
29572
+ broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
29573
+ // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
29574
+ connectRemote
27981
29575
  });
27982
29576
  const authResolver = new AuthContextResolver({
27983
- ownerToken: resolvedAuthToken,
27984
- personaRegistry
29577
+ ownerToken: resolvedAuthToken
27985
29578
  });
27986
29579
  const httpRouter = createHttpRouter({
27987
29580
  authResolver,
27988
29581
  daemonVersion: version,
27989
29582
  logger,
27990
- personaStore,
27991
- groupFileStore,
27992
29583
  sessionStore: store,
27993
- // /files HMAC verify auth.json signSecret 字段(与 attachment.signUrl 同源)。
27994
- // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
27995
- getSignSecret: () => authFile?.signSecret ?? null
29584
+ // /files HMAC verify 用同一份 owner token secret(与 attachment.signUrl 同源)
29585
+ getSignSecret: () => resolvedAuthToken ?? null
27996
29586
  });
27997
29587
  wsServer = new LocalWsServer({
27998
29588
  host: config.host,
27999
29589
  port: config.port,
28000
29590
  logger,
29591
+ // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
29592
+ ownerPrincipalId,
28001
29593
  readyFrameBuilder: (ctx) => buildReadyFrame(
28002
29594
  {
28003
29595
  manager,
@@ -28013,6 +29605,29 @@ async function startDaemon(config) {
28013
29605
  ),
28014
29606
  protocolVersion: PROTOCOL_VERSION,
28015
29607
  authGate: authGate ?? void 0,
29608
+ // noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
29609
+ // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
29610
+ // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
29611
+ // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
29612
+ tryVerifyCapabilityToken: (token, selfPrincipalId, selfDisplayName) => {
29613
+ const v2 = capabilityRegistry.verifyToken(token);
29614
+ if (!v2.ok) return null;
29615
+ if (selfPrincipalId && selfDisplayName) {
29616
+ try {
29617
+ capabilityManager.recordPeerHello(v2.capability.id, {
29618
+ ownerPrincipalId: selfPrincipalId,
29619
+ displayName: selfDisplayName,
29620
+ now: Date.now()
29621
+ });
29622
+ } catch {
29623
+ }
29624
+ }
29625
+ return {
29626
+ principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
29627
+ grants: v2.capability.grants,
29628
+ capabilityId: v2.capability.id
29629
+ };
29630
+ },
28016
29631
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
28017
29632
  httpRequestHandler: httpRouter,
28018
29633
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
@@ -28065,7 +29680,18 @@ async function startDaemon(config) {
28065
29680
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
28066
29681
  const handler = handlers[type];
28067
29682
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
28068
- const result = await handler(frame, client);
29683
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
29684
+ const verdict = computeGrantForFrame(type, frame);
29685
+ if (verdict.kind === "check") {
29686
+ const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
29687
+ if (!ok) {
29688
+ throw new ClawdError(
29689
+ ERROR_CODES.UNAUTHORIZED,
29690
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
29691
+ );
29692
+ }
29693
+ }
29694
+ const result = await handler(frame, client, ctx);
28069
29695
  if (requestId && result.response) {
28070
29696
  client.send({ ...result.response, requestId });
28071
29697
  }
@@ -28123,15 +29749,15 @@ async function startDaemon(config) {
28123
29749
  });
28124
29750
  try {
28125
29751
  const r = await tunnelMgr.start({ localPort: config.port });
28126
- stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
29752
+ stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
28127
29753
  stateMgr.write(stateSnapshot);
28128
29754
  currentTunnelUrl = r.url;
28129
29755
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
28130
29756
  const lines = [
28131
29757
  `Tunnel: ${r.url}`,
28132
29758
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
28133
- `Frpc config: ${import_node_path27.default.join(config.dataDir, "frpc.toml")}`,
28134
- `Frpc log: ${import_node_path27.default.join(config.dataDir, "frpc.log")}`
29759
+ `Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
29760
+ `Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
28135
29761
  ];
28136
29762
  const width = Math.max(...lines.map((l) => l.length));
28137
29763
  const bar = "\u2550".repeat(width + 4);
@@ -28144,30 +29770,19 @@ ${bar}
28144
29770
 
28145
29771
  `);
28146
29772
  try {
28147
- const connectPath = import_node_path27.default.join(config.dataDir, "connect.txt");
29773
+ const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
28148
29774
  import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
28149
29775
  } catch {
28150
29776
  }
28151
29777
  } catch (err) {
28152
- const tunnelError = err?.message ?? String(err);
28153
29778
  try {
28154
29779
  await tunnelMgr.stop();
28155
29780
  } catch {
28156
29781
  }
28157
29782
  tunnelMgr = null;
28158
- stateSnapshot = { ...stateSnapshot, tunnelUrl: void 0, tunnelError };
28159
- try {
28160
- stateMgr.write(stateSnapshot);
28161
- } catch {
28162
- }
28163
- wss.broadcastAll({
28164
- type: "tunnel:unavailable",
28165
- reason: tunnelError,
28166
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
28167
- });
28168
- process.stdout.write(`Tunnel: unavailable (local mode) \u2014 ${tunnelError}
28169
- `);
28170
- logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
29783
+ stateMgr.delete();
29784
+ await wss.stop();
29785
+ throw err;
28171
29786
  }
28172
29787
  }
28173
29788
  const shutdown = async () => {
@@ -28188,6 +29803,13 @@ ${bar}
28188
29803
  manager
28189
29804
  };
28190
29805
  }
29806
+ function migrateDropPersonsDir(dataDir) {
29807
+ const dir = import_node_path26.default.join(dataDir, "persons");
29808
+ try {
29809
+ import_node_fs24.default.rmSync(dir, { recursive: true, force: true });
29810
+ } catch {
29811
+ }
29812
+ }
28191
29813
 
28192
29814
  // src/cli.ts
28193
29815
  async function main() {