@clawos-dev/clawd 0.2.76-beta.144.0648ad1 → 0.2.76

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 +2349 -601
  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
  ];
@@ -609,8 +641,8 @@ var init_parseUtil = __esm({
609
641
  init_errors2();
610
642
  init_en();
611
643
  makeIssue = (params) => {
612
- const { data, path: path32, errorMaps, issueData } = params;
613
- const fullPath = [...path32, ...issueData.path || []];
644
+ const { data, path: path39, errorMaps, issueData } = params;
645
+ const fullPath = [...path39, ...issueData.path || []];
614
646
  const fullIssue = {
615
647
  ...issueData,
616
648
  path: fullPath
@@ -921,11 +953,11 @@ var init_types = __esm({
921
953
  init_parseUtil();
922
954
  init_util();
923
955
  ParseInputLazyPath = class {
924
- constructor(parent, value, path32, key) {
956
+ constructor(parent, value, path39, key) {
925
957
  this._cachedPath = [];
926
958
  this.parent = parent;
927
959
  this.data = value;
928
- this._path = path32;
960
+ this._path = path39;
929
961
  this._key = key;
930
962
  }
931
963
  get path() {
@@ -4309,12 +4341,12 @@ var init_zod = __esm({
4309
4341
  });
4310
4342
 
4311
4343
  // ../protocol/src/attachment-schemas.ts
4312
- var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4344
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
4313
4345
  var init_attachment_schemas = __esm({
4314
4346
  "../protocol/src/attachment-schemas.ts"() {
4315
4347
  "use strict";
4316
4348
  init_zod();
4317
- TOKEN_ROLES = ["owner", "personal"];
4349
+ TOKEN_ROLES = ["owner"];
4318
4350
  GROUP_FILE_SOURCES = ["agent", "owner"];
4319
4351
  GroupFileEntrySchema = external_exports.object({
4320
4352
  /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
@@ -4370,31 +4402,15 @@ var init_attachment_schemas = __esm({
4370
4402
  AttachmentGroupListResponseSchema = external_exports.object({
4371
4403
  entries: external_exports.array(GroupFileEntrySchema)
4372
4404
  });
4373
- AttachmentGroupListPersonaArgs = external_exports.object({
4374
- personaId: external_exports.string().min(1)
4375
- });
4376
- AttachmentGroupListPersonaResponseSchema = external_exports.object({
4377
- perSession: external_exports.array(
4378
- external_exports.object({
4379
- sessionId: external_exports.string().min(1),
4380
- entries: external_exports.array(GroupFileEntrySchema)
4381
- })
4382
- )
4383
- });
4384
4405
  }
4385
4406
  });
4386
4407
 
4387
4408
  // ../protocol/src/persona-schemas.ts
4388
- var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema;
4409
+ var PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
4389
4410
  var init_persona_schemas = __esm({
4390
4411
  "../protocol/src/persona-schemas.ts"() {
4391
4412
  "use strict";
4392
4413
  init_zod();
4393
- PersonaTokenEntrySchema = external_exports.object({
4394
- label: external_exports.string(),
4395
- issuedAt: external_exports.number(),
4396
- revoked: external_exports.boolean()
4397
- });
4398
4414
  PersonaFileSchema = external_exports.object({
4399
4415
  personaId: external_exports.string(),
4400
4416
  label: external_exports.string(),
@@ -4403,13 +4419,6 @@ var init_persona_schemas = __esm({
4403
4419
  // 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
4404
4420
  // for forward compatibility, consumer fallbacks to default when key not found.
4405
4421
  iconKey: external_exports.string().optional(),
4406
- /**
4407
- * Persona token 列表:file-sharing HTTP Bearer 鉴权用。
4408
- * - 写:`PersonaManager.issueToken` / `revokeToken`
4409
- * - 读:`PersonaRegistry.findByToken` 反向查表(需 persona.public === true)
4410
- * - optional:兼容历史 persona.json 不含此字段的情形(旧 daemon 写文件时省略)
4411
- */
4412
- tokenMap: external_exports.record(external_exports.string(), PersonaTokenEntrySchema).optional(),
4413
4422
  createdAt: external_exports.number(),
4414
4423
  updatedAt: external_exports.number()
4415
4424
  }).strict();
@@ -4468,14 +4477,6 @@ var init_persona_schemas = __esm({
4468
4477
  iconKey: external_exports.string().nullable().optional()
4469
4478
  }).strict()
4470
4479
  }).strict();
4471
- PersonaIssueTokenArgsSchema = external_exports.object({
4472
- personaId: external_exports.string().min(1),
4473
- label: external_exports.string().min(1)
4474
- });
4475
- PersonaRevokeTokenArgsSchema = external_exports.object({
4476
- personaId: external_exports.string().min(1),
4477
- token: external_exports.string().min(1)
4478
- });
4479
4480
  }
4480
4481
  });
4481
4482
 
@@ -4582,6 +4583,15 @@ var init_schemas = __esm({
4582
4583
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
4583
4584
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4584
4585
  chatId: external_exports.string().min(1).optional(),
4586
+ /**
4587
+ * 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
4588
+ * 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4589
+ * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4590
+ * 后者再通过 PersonAlias 表反查 personId 二级分组。
4591
+ *
4592
+ * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4593
+ */
4594
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4585
4595
  createdAt: external_exports.string().min(1),
4586
4596
  updatedAt: external_exports.string().min(1)
4587
4597
  });
@@ -4991,7 +5001,20 @@ var init_schemas = __esm({
4991
5001
  AuthRequestFrameSchema = external_exports.object({
4992
5002
  type: external_exports.literal("auth"),
4993
5003
  token: external_exports.string().min(1),
4994
- scheme: external_exports.literal("bearer").optional()
5004
+ scheme: external_exports.literal("bearer").optional(),
5005
+ /**
5006
+ * guest 端 ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5007
+ * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5008
+ * 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5009
+ * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5010
+ */
5011
+ selfPrincipalId: external_exports.string().min(1).optional(),
5012
+ /**
5013
+ * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5014
+ * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5015
+ * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5016
+ */
5017
+ selfDisplayName: external_exports.string().optional()
4995
5018
  });
4996
5019
  AuthOkFrameSchema = external_exports.object({
4997
5020
  type: external_exports.literal("auth:ok"),
@@ -5029,10 +5052,9 @@ var init_schemas = __esm({
5029
5052
  // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5030
5053
  // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5031
5054
  // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5032
- /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5055
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
5056
+ * 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
5033
5057
  tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5034
- /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5035
- tokenPersonaId: external_exports.string().min(1).optional(),
5036
5058
  /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5037
5059
  isLoopback: external_exports.boolean().optional(),
5038
5060
  /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
@@ -5059,6 +5081,194 @@ var init_persona_mode = __esm({
5059
5081
  }
5060
5082
  });
5061
5083
 
5084
+ // ../protocol/src/principal.ts
5085
+ function makeOwnerPrincipal(id, displayName) {
5086
+ return PrincipalSchema.parse({ id, kind: "owner", displayName });
5087
+ }
5088
+ var PrincipalKindSchema, PrincipalSchema;
5089
+ var init_principal = __esm({
5090
+ "../protocol/src/principal.ts"() {
5091
+ "use strict";
5092
+ init_zod();
5093
+ PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
5094
+ PrincipalSchema = external_exports.object({
5095
+ id: external_exports.string().min(1),
5096
+ kind: PrincipalKindSchema,
5097
+ displayName: external_exports.string()
5098
+ }).strict();
5099
+ }
5100
+ });
5101
+
5102
+ // ../protocol/src/capability.ts
5103
+ function stripSecretHash(cap) {
5104
+ const { secretHash: _hash, ...wire } = cap;
5105
+ return wire;
5106
+ }
5107
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema, CapabilityBindPeerArgsSchema;
5108
+ var init_capability = __esm({
5109
+ "../protocol/src/capability.ts"() {
5110
+ "use strict";
5111
+ init_zod();
5112
+ ResourceSchema = external_exports.discriminatedUnion("type", [
5113
+ external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
5114
+ external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
5115
+ external_exports.object({ type: external_exports.literal("*") }).strict()
5116
+ ]);
5117
+ ActionSchema = external_exports.enum(["read", "send", "admin"]);
5118
+ GrantSchema = external_exports.object({
5119
+ resource: ResourceSchema,
5120
+ actions: external_exports.array(ActionSchema).min(1)
5121
+ }).strict();
5122
+ CapabilitySchema = external_exports.object({
5123
+ id: external_exports.string().min(1),
5124
+ // sha256(token) hex 64
5125
+ secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
5126
+ displayName: external_exports.string(),
5127
+ // 空数组合法 = 纯访客(只能 DM)
5128
+ grants: external_exports.array(GrantSchema),
5129
+ issuedAt: external_exports.number().int().nonnegative(),
5130
+ expiresAt: external_exports.number().int().positive().optional(),
5131
+ maxUses: external_exports.number().int().positive().optional(),
5132
+ usedCount: external_exports.number().int().nonnegative(),
5133
+ /**
5134
+ * guest 在 auth 帧捎带 selfPrincipalId 时由 AuthGate.authenticate 回写;
5135
+ * 未被消费的 cap 此字段缺失。
5136
+ */
5137
+ peerOwnerId: external_exports.string().min(1).optional(),
5138
+ /** 首次 client hello 时间戳(与 peerOwnerId 同步写入) */
5139
+ firstUsedByPeerAt: external_exports.number().int().positive().optional(),
5140
+ /**
5141
+ * 对方 daemon owner 的 displayName,guest auth 帧 selfDisplayName 触发
5142
+ * recordPeerHello 时同步回写。视角 B (2026-05-23) UI 派生联系人列表用此字段
5143
+ * 显示对方名字(避免 fallback 到我颁 cap 时填的 displayName)。
5144
+ */
5145
+ peerDisplayName: external_exports.string().optional()
5146
+ }).strict();
5147
+ CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
5148
+ CapabilityErrorCodeSchema = external_exports.enum([
5149
+ "TOKEN_INVALID",
5150
+ "TOKEN_REVOKED",
5151
+ "TOKEN_EXPIRED",
5152
+ "TOKEN_EXHAUSTED"
5153
+ ]);
5154
+ WhoamiResponseSchema = external_exports.object({
5155
+ type: external_exports.literal("whoami:ok"),
5156
+ owner: external_exports.object({
5157
+ id: external_exports.string(),
5158
+ kind: external_exports.enum(["owner", "guest"]),
5159
+ displayName: external_exports.string()
5160
+ }).strict(),
5161
+ capability: CapabilityWireSchema,
5162
+ grantedPersonas: external_exports.array(
5163
+ external_exports.object({
5164
+ id: external_exports.string().min(1),
5165
+ displayName: external_exports.string()
5166
+ }).strict()
5167
+ )
5168
+ }).strict();
5169
+ CapabilityIssueArgsSchema = external_exports.object({
5170
+ displayName: external_exports.string().min(1),
5171
+ grants: external_exports.array(GrantSchema),
5172
+ expiresAt: external_exports.number().int().positive().optional(),
5173
+ maxUses: external_exports.number().int().positive().optional()
5174
+ }).strict();
5175
+ CapabilityBindPeerArgsSchema = external_exports.object({
5176
+ capabilityId: external_exports.string().min(1),
5177
+ peerOwnerId: external_exports.string().min(1),
5178
+ peerDisplayName: external_exports.string()
5179
+ }).strict();
5180
+ }
5181
+ });
5182
+
5183
+ // ../protocol/src/inbox.ts
5184
+ var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
5185
+ var init_inbox = __esm({
5186
+ "../protocol/src/inbox.ts"() {
5187
+ "use strict";
5188
+ init_zod();
5189
+ InboxMessageSchema = external_exports.object({
5190
+ id: external_exports.string().min(1),
5191
+ peerOwnerId: external_exports.string().min(1),
5192
+ senderPrincipalId: external_exports.string().min(1),
5193
+ text: external_exports.string().min(1),
5194
+ createdAt: external_exports.number().int().nonnegative(),
5195
+ readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5196
+ }).strict();
5197
+ InboxPostMessageArgsSchema = external_exports.object({
5198
+ peerOwnerId: external_exports.string().min(1).optional(),
5199
+ id: external_exports.string().min(1),
5200
+ text: external_exports.string().min(1),
5201
+ createdAt: external_exports.number().int().nonnegative()
5202
+ }).strict();
5203
+ InboxListArgsSchema = external_exports.object({
5204
+ peerOwnerId: external_exports.string().min(1),
5205
+ sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5206
+ }).strict();
5207
+ InboxMarkReadArgsSchema = external_exports.object({
5208
+ peerOwnerId: external_exports.string().min(1),
5209
+ upToCreatedAt: external_exports.number().int().nonnegative()
5210
+ }).strict();
5211
+ }
5212
+ });
5213
+
5214
+ // ../protocol/src/received-capability.ts
5215
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5216
+ var init_received_capability = __esm({
5217
+ "../protocol/src/received-capability.ts"() {
5218
+ "use strict";
5219
+ init_zod();
5220
+ init_capability();
5221
+ ReceivedCapabilitySchema = external_exports.object({
5222
+ ownerPrincipalId: external_exports.string().min(1),
5223
+ ownerDisplayName: external_exports.string(),
5224
+ remoteUrl: external_exports.string().min(1),
5225
+ capabilityId: external_exports.string().min(1),
5226
+ capabilityToken: external_exports.string().min(1),
5227
+ grants: external_exports.array(GrantSchema),
5228
+ receivedAt: external_exports.number().int().nonnegative()
5229
+ }).strict();
5230
+ ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5231
+ ReceivedCapabilityAddArgsSchema = external_exports.object({
5232
+ remoteUrl: external_exports.string().min(1),
5233
+ token: external_exports.string().min(1)
5234
+ }).strict();
5235
+ ReceivedCapabilityAddOkSchema = external_exports.object({
5236
+ type: external_exports.literal("received-capability:add:ok"),
5237
+ receivedCap: ReceivedCapabilityWireSchema
5238
+ }).strict();
5239
+ ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5240
+ ownerPrincipalId: external_exports.string().min(1)
5241
+ }).strict();
5242
+ ReceivedCapabilityRemoveOkSchema = external_exports.object({
5243
+ type: external_exports.literal("received-capability:remove:ok"),
5244
+ ownerPrincipalId: external_exports.string().min(1)
5245
+ }).strict();
5246
+ ReceivedCapabilityListOkSchema = external_exports.object({
5247
+ type: external_exports.literal("received-capability:list:ok"),
5248
+ receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5249
+ }).strict();
5250
+ ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5251
+ ownerPrincipalId: external_exports.string().min(1),
5252
+ ownerDisplayName: external_exports.string(),
5253
+ remoteUrl: external_exports.string().min(1),
5254
+ capabilityId: external_exports.string().min(1),
5255
+ token: external_exports.string().min(1),
5256
+ grants: external_exports.array(GrantSchema)
5257
+ }).strict();
5258
+ ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5259
+ type: external_exports.literal("received-capability:autoAttach:ok")
5260
+ }).strict();
5261
+ ReceivedCapabilityAddedFrameSchema = external_exports.object({
5262
+ type: external_exports.literal("received-capability:added"),
5263
+ receivedCap: ReceivedCapabilityWireSchema
5264
+ }).strict();
5265
+ ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5266
+ type: external_exports.literal("received-capability:removed"),
5267
+ ownerPrincipalId: external_exports.string().min(1)
5268
+ }).strict();
5269
+ }
5270
+ });
5271
+
5062
5272
  // ../protocol/src/runtime.ts
5063
5273
  var init_runtime = __esm({
5064
5274
  "../protocol/src/runtime.ts"() {
@@ -5071,6 +5281,10 @@ var init_runtime = __esm({
5071
5281
  init_persona_schemas();
5072
5282
  init_persona_mode();
5073
5283
  init_attachment_schemas();
5284
+ init_principal();
5285
+ init_capability();
5286
+ init_inbox();
5287
+ init_received_capability();
5074
5288
  }
5075
5289
  });
5076
5290
 
@@ -5345,8 +5559,8 @@ var require_req = __commonJS({
5345
5559
  if (req.originalUrl) {
5346
5560
  _req.url = req.originalUrl;
5347
5561
  } else {
5348
- const path32 = req.path;
5349
- _req.url = typeof path32 === "string" ? path32 : req.url ? req.url.path || req.url : void 0;
5562
+ const path39 = req.path;
5563
+ _req.url = typeof path39 === "string" ? path39 : req.url ? req.url.path || req.url : void 0;
5350
5564
  }
5351
5565
  if (req.query) {
5352
5566
  _req.query = req.query;
@@ -5511,14 +5725,14 @@ var require_redact = __commonJS({
5511
5725
  }
5512
5726
  return obj;
5513
5727
  }
5514
- function parsePath(path32) {
5728
+ function parsePath(path39) {
5515
5729
  const parts = [];
5516
5730
  let current = "";
5517
5731
  let inBrackets = false;
5518
5732
  let inQuotes = false;
5519
5733
  let quoteChar = "";
5520
- for (let i = 0; i < path32.length; i++) {
5521
- const char = path32[i];
5734
+ for (let i = 0; i < path39.length; i++) {
5735
+ const char = path39[i];
5522
5736
  if (!inBrackets && char === ".") {
5523
5737
  if (current) {
5524
5738
  parts.push(current);
@@ -5649,10 +5863,10 @@ var require_redact = __commonJS({
5649
5863
  return current;
5650
5864
  }
5651
5865
  function redactPaths(obj, paths, censor, remove = false) {
5652
- for (const path32 of paths) {
5653
- const parts = parsePath(path32);
5866
+ for (const path39 of paths) {
5867
+ const parts = parsePath(path39);
5654
5868
  if (parts.includes("*")) {
5655
- redactWildcardPath(obj, parts, censor, path32, remove);
5869
+ redactWildcardPath(obj, parts, censor, path39, remove);
5656
5870
  } else {
5657
5871
  if (remove) {
5658
5872
  removeKey(obj, parts);
@@ -5737,8 +5951,8 @@ var require_redact = __commonJS({
5737
5951
  }
5738
5952
  } else {
5739
5953
  if (afterWildcard.includes("*")) {
5740
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
5741
- const fullPath = [...pathArray.slice(0, pathLength), ...path32];
5954
+ const wrappedCensor = typeof censor === "function" ? (value, path39) => {
5955
+ const fullPath = [...pathArray.slice(0, pathLength), ...path39];
5742
5956
  return censor(value, fullPath);
5743
5957
  } : censor;
5744
5958
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5773,8 +5987,8 @@ var require_redact = __commonJS({
5773
5987
  return null;
5774
5988
  }
5775
5989
  const pathStructure = /* @__PURE__ */ new Map();
5776
- for (const path32 of pathsToClone) {
5777
- const parts = parsePath(path32);
5990
+ for (const path39 of pathsToClone) {
5991
+ const parts = parsePath(path39);
5778
5992
  let current = pathStructure;
5779
5993
  for (let i = 0; i < parts.length; i++) {
5780
5994
  const part = parts[i];
@@ -5826,24 +6040,24 @@ var require_redact = __commonJS({
5826
6040
  }
5827
6041
  return cloneSelectively(obj, pathStructure);
5828
6042
  }
5829
- function validatePath(path32) {
5830
- if (typeof path32 !== "string") {
6043
+ function validatePath(path39) {
6044
+ if (typeof path39 !== "string") {
5831
6045
  throw new Error("Paths must be (non-empty) strings");
5832
6046
  }
5833
- if (path32 === "") {
6047
+ if (path39 === "") {
5834
6048
  throw new Error("Invalid redaction path ()");
5835
6049
  }
5836
- if (path32.includes("..")) {
5837
- throw new Error(`Invalid redaction path (${path32})`);
6050
+ if (path39.includes("..")) {
6051
+ throw new Error(`Invalid redaction path (${path39})`);
5838
6052
  }
5839
- if (path32.includes(",")) {
5840
- throw new Error(`Invalid redaction path (${path32})`);
6053
+ if (path39.includes(",")) {
6054
+ throw new Error(`Invalid redaction path (${path39})`);
5841
6055
  }
5842
6056
  let bracketCount = 0;
5843
6057
  let inQuotes = false;
5844
6058
  let quoteChar = "";
5845
- for (let i = 0; i < path32.length; i++) {
5846
- const char = path32[i];
6059
+ for (let i = 0; i < path39.length; i++) {
6060
+ const char = path39[i];
5847
6061
  if ((char === '"' || char === "'") && bracketCount > 0) {
5848
6062
  if (!inQuotes) {
5849
6063
  inQuotes = true;
@@ -5857,20 +6071,20 @@ var require_redact = __commonJS({
5857
6071
  } else if (char === "]" && !inQuotes) {
5858
6072
  bracketCount--;
5859
6073
  if (bracketCount < 0) {
5860
- throw new Error(`Invalid redaction path (${path32})`);
6074
+ throw new Error(`Invalid redaction path (${path39})`);
5861
6075
  }
5862
6076
  }
5863
6077
  }
5864
6078
  if (bracketCount !== 0) {
5865
- throw new Error(`Invalid redaction path (${path32})`);
6079
+ throw new Error(`Invalid redaction path (${path39})`);
5866
6080
  }
5867
6081
  }
5868
6082
  function validatePaths(paths) {
5869
6083
  if (!Array.isArray(paths)) {
5870
6084
  throw new TypeError("paths must be an array");
5871
6085
  }
5872
- for (const path32 of paths) {
5873
- validatePath(path32);
6086
+ for (const path39 of paths) {
6087
+ validatePath(path39);
5874
6088
  }
5875
6089
  }
5876
6090
  function slowRedact(options = {}) {
@@ -6038,8 +6252,8 @@ var require_redaction = __commonJS({
6038
6252
  if (shape[k2] === null) {
6039
6253
  o[k2] = (value) => topCensor(value, [k2]);
6040
6254
  } else {
6041
- const wrappedCensor = typeof censor === "function" ? (value, path32) => {
6042
- return censor(value, [k2, ...path32]);
6255
+ const wrappedCensor = typeof censor === "function" ? (value, path39) => {
6256
+ return censor(value, [k2, ...path39]);
6043
6257
  } : censor;
6044
6258
  o[k2] = Redact({
6045
6259
  paths: shape[k2],
@@ -6257,10 +6471,10 @@ var require_atomic_sleep = __commonJS({
6257
6471
  var require_sonic_boom = __commonJS({
6258
6472
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6259
6473
  "use strict";
6260
- var fs28 = require("fs");
6474
+ var fs34 = require("fs");
6261
6475
  var EventEmitter2 = require("events");
6262
6476
  var inherits = require("util").inherits;
6263
- var path32 = require("path");
6477
+ var path39 = require("path");
6264
6478
  var sleep = require_atomic_sleep();
6265
6479
  var assert = require("assert");
6266
6480
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6314,20 +6528,20 @@ var require_sonic_boom = __commonJS({
6314
6528
  const mode = sonic.mode;
6315
6529
  if (sonic.sync) {
6316
6530
  try {
6317
- if (sonic.mkdir) fs28.mkdirSync(path32.dirname(file), { recursive: true });
6318
- const fd = fs28.openSync(file, flags, mode);
6531
+ if (sonic.mkdir) fs34.mkdirSync(path39.dirname(file), { recursive: true });
6532
+ const fd = fs34.openSync(file, flags, mode);
6319
6533
  fileOpened(null, fd);
6320
6534
  } catch (err) {
6321
6535
  fileOpened(err);
6322
6536
  throw err;
6323
6537
  }
6324
6538
  } else if (sonic.mkdir) {
6325
- fs28.mkdir(path32.dirname(file), { recursive: true }, (err) => {
6539
+ fs34.mkdir(path39.dirname(file), { recursive: true }, (err) => {
6326
6540
  if (err) return fileOpened(err);
6327
- fs28.open(file, flags, mode, fileOpened);
6541
+ fs34.open(file, flags, mode, fileOpened);
6328
6542
  });
6329
6543
  } else {
6330
- fs28.open(file, flags, mode, fileOpened);
6544
+ fs34.open(file, flags, mode, fileOpened);
6331
6545
  }
6332
6546
  }
6333
6547
  function SonicBoom(opts) {
@@ -6368,8 +6582,8 @@ var require_sonic_boom = __commonJS({
6368
6582
  this.flush = flushBuffer;
6369
6583
  this.flushSync = flushBufferSync;
6370
6584
  this._actualWrite = actualWriteBuffer;
6371
- fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
6372
- fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
6585
+ fsWriteSync = () => fs34.writeSync(this.fd, this._writingBuf);
6586
+ fsWrite = () => fs34.write(this.fd, this._writingBuf, this.release);
6373
6587
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6374
6588
  this._writingBuf = "";
6375
6589
  this.write = write;
@@ -6378,15 +6592,15 @@ var require_sonic_boom = __commonJS({
6378
6592
  this._actualWrite = actualWrite;
6379
6593
  fsWriteSync = () => {
6380
6594
  if (Buffer.isBuffer(this._writingBuf)) {
6381
- return fs28.writeSync(this.fd, this._writingBuf);
6595
+ return fs34.writeSync(this.fd, this._writingBuf);
6382
6596
  }
6383
- return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6597
+ return fs34.writeSync(this.fd, this._writingBuf, "utf8");
6384
6598
  };
6385
6599
  fsWrite = () => {
6386
6600
  if (Buffer.isBuffer(this._writingBuf)) {
6387
- return fs28.write(this.fd, this._writingBuf, this.release);
6601
+ return fs34.write(this.fd, this._writingBuf, this.release);
6388
6602
  }
6389
- return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6603
+ return fs34.write(this.fd, this._writingBuf, "utf8", this.release);
6390
6604
  };
6391
6605
  } else {
6392
6606
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6443,7 +6657,7 @@ var require_sonic_boom = __commonJS({
6443
6657
  }
6444
6658
  }
6445
6659
  if (this._fsync) {
6446
- fs28.fsyncSync(this.fd);
6660
+ fs34.fsyncSync(this.fd);
6447
6661
  }
6448
6662
  const len = this._len;
6449
6663
  if (this._reopening) {
@@ -6557,7 +6771,7 @@ var require_sonic_boom = __commonJS({
6557
6771
  const onDrain = () => {
6558
6772
  if (!this._fsync) {
6559
6773
  try {
6560
- fs28.fsync(this.fd, (err) => {
6774
+ fs34.fsync(this.fd, (err) => {
6561
6775
  this._flushPending = false;
6562
6776
  cb(err);
6563
6777
  });
@@ -6659,7 +6873,7 @@ var require_sonic_boom = __commonJS({
6659
6873
  const fd = this.fd;
6660
6874
  this.once("ready", () => {
6661
6875
  if (fd !== this.fd) {
6662
- fs28.close(fd, (err) => {
6876
+ fs34.close(fd, (err) => {
6663
6877
  if (err) {
6664
6878
  return this.emit("error", err);
6665
6879
  }
@@ -6708,7 +6922,7 @@ var require_sonic_boom = __commonJS({
6708
6922
  buf = this._bufs[0];
6709
6923
  }
6710
6924
  try {
6711
- const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
6925
+ const n = Buffer.isBuffer(buf) ? fs34.writeSync(this.fd, buf) : fs34.writeSync(this.fd, buf, "utf8");
6712
6926
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6713
6927
  buf = releasedBufObj.writingBuf;
6714
6928
  this._len = releasedBufObj.len;
@@ -6724,7 +6938,7 @@ var require_sonic_boom = __commonJS({
6724
6938
  }
6725
6939
  }
6726
6940
  try {
6727
- fs28.fsyncSync(this.fd);
6941
+ fs34.fsyncSync(this.fd);
6728
6942
  } catch {
6729
6943
  }
6730
6944
  }
@@ -6745,7 +6959,7 @@ var require_sonic_boom = __commonJS({
6745
6959
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6746
6960
  }
6747
6961
  try {
6748
- const n = fs28.writeSync(this.fd, buf);
6962
+ const n = fs34.writeSync(this.fd, buf);
6749
6963
  buf = buf.subarray(n);
6750
6964
  this._len = Math.max(this._len - n, 0);
6751
6965
  if (buf.length <= 0) {
@@ -6773,13 +6987,13 @@ var require_sonic_boom = __commonJS({
6773
6987
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6774
6988
  if (this.sync) {
6775
6989
  try {
6776
- const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
6990
+ const written = Buffer.isBuffer(this._writingBuf) ? fs34.writeSync(this.fd, this._writingBuf) : fs34.writeSync(this.fd, this._writingBuf, "utf8");
6777
6991
  release(null, written);
6778
6992
  } catch (err) {
6779
6993
  release(err);
6780
6994
  }
6781
6995
  } else {
6782
- fs28.write(this.fd, this._writingBuf, release);
6996
+ fs34.write(this.fd, this._writingBuf, release);
6783
6997
  }
6784
6998
  }
6785
6999
  function actualWriteBuffer() {
@@ -6788,7 +7002,7 @@ var require_sonic_boom = __commonJS({
6788
7002
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6789
7003
  if (this.sync) {
6790
7004
  try {
6791
- const written = fs28.writeSync(this.fd, this._writingBuf);
7005
+ const written = fs34.writeSync(this.fd, this._writingBuf);
6792
7006
  release(null, written);
6793
7007
  } catch (err) {
6794
7008
  release(err);
@@ -6797,7 +7011,7 @@ var require_sonic_boom = __commonJS({
6797
7011
  if (kCopyBuffer) {
6798
7012
  this._writingBuf = Buffer.from(this._writingBuf);
6799
7013
  }
6800
- fs28.write(this.fd, this._writingBuf, release);
7014
+ fs34.write(this.fd, this._writingBuf, release);
6801
7015
  }
6802
7016
  }
6803
7017
  function actualClose(sonic) {
@@ -6813,12 +7027,12 @@ var require_sonic_boom = __commonJS({
6813
7027
  sonic._lens = [];
6814
7028
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6815
7029
  try {
6816
- fs28.fsync(sonic.fd, closeWrapped);
7030
+ fs34.fsync(sonic.fd, closeWrapped);
6817
7031
  } catch {
6818
7032
  }
6819
7033
  function closeWrapped() {
6820
7034
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6821
- fs28.close(sonic.fd, done);
7035
+ fs34.close(sonic.fd, done);
6822
7036
  } else {
6823
7037
  done();
6824
7038
  }
@@ -7075,7 +7289,7 @@ var require_thread_stream = __commonJS({
7075
7289
  var { version: version2 } = require_package();
7076
7290
  var { EventEmitter: EventEmitter2 } = require("events");
7077
7291
  var { Worker } = require("worker_threads");
7078
- var { join: join4 } = require("path");
7292
+ var { join: join11 } = require("path");
7079
7293
  var { pathToFileURL } = require("url");
7080
7294
  var { wait } = require_wait();
7081
7295
  var {
@@ -7111,7 +7325,7 @@ var require_thread_stream = __commonJS({
7111
7325
  function createWorker(stream, opts) {
7112
7326
  const { filename, workerData } = opts;
7113
7327
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7114
- const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
7328
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7115
7329
  const worker = new Worker(toExecute, {
7116
7330
  ...opts.workerOpts,
7117
7331
  trackUnmanagedFds: false,
@@ -7497,7 +7711,7 @@ var require_transport = __commonJS({
7497
7711
  "use strict";
7498
7712
  var { createRequire } = require("module");
7499
7713
  var getCallers = require_caller();
7500
- var { join: join4, isAbsolute, sep } = require("path");
7714
+ var { join: join11, isAbsolute, sep: sep2 } = require("path");
7501
7715
  var sleep = require_atomic_sleep();
7502
7716
  var onExit = require_on_exit_leak_free();
7503
7717
  var ThreadStream = require_thread_stream();
@@ -7560,7 +7774,7 @@ var require_transport = __commonJS({
7560
7774
  throw new Error("only one of target or targets can be specified");
7561
7775
  }
7562
7776
  if (targets) {
7563
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7777
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7564
7778
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
7565
7779
  return {
7566
7780
  ...dest,
@@ -7578,7 +7792,7 @@ var require_transport = __commonJS({
7578
7792
  });
7579
7793
  });
7580
7794
  } else if (pipeline2) {
7581
- target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7795
+ target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7582
7796
  options.pipelines = [pipeline2.map((dest) => {
7583
7797
  return {
7584
7798
  ...dest,
@@ -7600,12 +7814,12 @@ var require_transport = __commonJS({
7600
7814
  return origin;
7601
7815
  }
7602
7816
  if (origin === "pino/file") {
7603
- return join4(__dirname, "..", "file.js");
7817
+ return join11(__dirname, "..", "file.js");
7604
7818
  }
7605
7819
  let fixTarget2;
7606
7820
  for (const filePath of callers) {
7607
7821
  try {
7608
- const context = filePath === "node:repl" ? process.cwd() + sep : filePath;
7822
+ const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
7609
7823
  fixTarget2 = createRequire(context).resolve(origin);
7610
7824
  break;
7611
7825
  } catch (err) {
@@ -8590,7 +8804,7 @@ var require_safe_stable_stringify = __commonJS({
8590
8804
  return circularValue;
8591
8805
  }
8592
8806
  let res = "";
8593
- let join4 = ",";
8807
+ let join11 = ",";
8594
8808
  const originalIndentation = indentation;
8595
8809
  if (Array.isArray(value)) {
8596
8810
  if (value.length === 0) {
@@ -8604,7 +8818,7 @@ var require_safe_stable_stringify = __commonJS({
8604
8818
  indentation += spacer;
8605
8819
  res += `
8606
8820
  ${indentation}`;
8607
- join4 = `,
8821
+ join11 = `,
8608
8822
  ${indentation}`;
8609
8823
  }
8610
8824
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8612,13 +8826,13 @@ ${indentation}`;
8612
8826
  for (; i < maximumValuesToStringify - 1; i++) {
8613
8827
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8614
8828
  res += tmp2 !== void 0 ? tmp2 : "null";
8615
- res += join4;
8829
+ res += join11;
8616
8830
  }
8617
8831
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8618
8832
  res += tmp !== void 0 ? tmp : "null";
8619
8833
  if (value.length - 1 > maximumBreadth) {
8620
8834
  const removedKeys = value.length - maximumBreadth - 1;
8621
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8835
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8622
8836
  }
8623
8837
  if (spacer !== "") {
8624
8838
  res += `
@@ -8639,7 +8853,7 @@ ${originalIndentation}`;
8639
8853
  let separator = "";
8640
8854
  if (spacer !== "") {
8641
8855
  indentation += spacer;
8642
- join4 = `,
8856
+ join11 = `,
8643
8857
  ${indentation}`;
8644
8858
  whitespace = " ";
8645
8859
  }
@@ -8653,13 +8867,13 @@ ${indentation}`;
8653
8867
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
8654
8868
  if (tmp !== void 0) {
8655
8869
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8656
- separator = join4;
8870
+ separator = join11;
8657
8871
  }
8658
8872
  }
8659
8873
  if (keyLength > maximumBreadth) {
8660
8874
  const removedKeys = keyLength - maximumBreadth;
8661
8875
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
8662
- separator = join4;
8876
+ separator = join11;
8663
8877
  }
8664
8878
  if (spacer !== "" && separator.length > 1) {
8665
8879
  res = `
@@ -8700,7 +8914,7 @@ ${originalIndentation}`;
8700
8914
  }
8701
8915
  const originalIndentation = indentation;
8702
8916
  let res = "";
8703
- let join4 = ",";
8917
+ let join11 = ",";
8704
8918
  if (Array.isArray(value)) {
8705
8919
  if (value.length === 0) {
8706
8920
  return "[]";
@@ -8713,7 +8927,7 @@ ${originalIndentation}`;
8713
8927
  indentation += spacer;
8714
8928
  res += `
8715
8929
  ${indentation}`;
8716
- join4 = `,
8930
+ join11 = `,
8717
8931
  ${indentation}`;
8718
8932
  }
8719
8933
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8721,13 +8935,13 @@ ${indentation}`;
8721
8935
  for (; i < maximumValuesToStringify - 1; i++) {
8722
8936
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8723
8937
  res += tmp2 !== void 0 ? tmp2 : "null";
8724
- res += join4;
8938
+ res += join11;
8725
8939
  }
8726
8940
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8727
8941
  res += tmp !== void 0 ? tmp : "null";
8728
8942
  if (value.length - 1 > maximumBreadth) {
8729
8943
  const removedKeys = value.length - maximumBreadth - 1;
8730
- res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8944
+ res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8731
8945
  }
8732
8946
  if (spacer !== "") {
8733
8947
  res += `
@@ -8740,7 +8954,7 @@ ${originalIndentation}`;
8740
8954
  let whitespace = "";
8741
8955
  if (spacer !== "") {
8742
8956
  indentation += spacer;
8743
- join4 = `,
8957
+ join11 = `,
8744
8958
  ${indentation}`;
8745
8959
  whitespace = " ";
8746
8960
  }
@@ -8749,7 +8963,7 @@ ${indentation}`;
8749
8963
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
8750
8964
  if (tmp !== void 0) {
8751
8965
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8752
- separator = join4;
8966
+ separator = join11;
8753
8967
  }
8754
8968
  }
8755
8969
  if (spacer !== "" && separator.length > 1) {
@@ -8807,20 +9021,20 @@ ${originalIndentation}`;
8807
9021
  indentation += spacer;
8808
9022
  let res2 = `
8809
9023
  ${indentation}`;
8810
- const join5 = `,
9024
+ const join12 = `,
8811
9025
  ${indentation}`;
8812
9026
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
8813
9027
  let i = 0;
8814
9028
  for (; i < maximumValuesToStringify - 1; i++) {
8815
9029
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8816
9030
  res2 += tmp2 !== void 0 ? tmp2 : "null";
8817
- res2 += join5;
9031
+ res2 += join12;
8818
9032
  }
8819
9033
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8820
9034
  res2 += tmp !== void 0 ? tmp : "null";
8821
9035
  if (value.length - 1 > maximumBreadth) {
8822
9036
  const removedKeys = value.length - maximumBreadth - 1;
8823
- res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
9037
+ res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
8824
9038
  }
8825
9039
  res2 += `
8826
9040
  ${originalIndentation}`;
@@ -8836,16 +9050,16 @@ ${originalIndentation}`;
8836
9050
  return '"[Object]"';
8837
9051
  }
8838
9052
  indentation += spacer;
8839
- const join4 = `,
9053
+ const join11 = `,
8840
9054
  ${indentation}`;
8841
9055
  let res = "";
8842
9056
  let separator = "";
8843
9057
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
8844
9058
  if (isTypedArrayWithEntries(value)) {
8845
- res += stringifyTypedArray(value, join4, maximumBreadth);
9059
+ res += stringifyTypedArray(value, join11, maximumBreadth);
8846
9060
  keys = keys.slice(value.length);
8847
9061
  maximumPropertiesToStringify -= value.length;
8848
- separator = join4;
9062
+ separator = join11;
8849
9063
  }
8850
9064
  if (deterministic) {
8851
9065
  keys = sort(keys, comparator);
@@ -8856,13 +9070,13 @@ ${indentation}`;
8856
9070
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
8857
9071
  if (tmp !== void 0) {
8858
9072
  res += `${separator}${strEscape(key2)}: ${tmp}`;
8859
- separator = join4;
9073
+ separator = join11;
8860
9074
  }
8861
9075
  }
8862
9076
  if (keyLength > maximumBreadth) {
8863
9077
  const removedKeys = keyLength - maximumBreadth;
8864
9078
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
8865
- separator = join4;
9079
+ separator = join11;
8866
9080
  }
8867
9081
  if (separator !== "") {
8868
9082
  res = `
@@ -9953,11 +10167,11 @@ var init_lib = __esm({
9953
10167
  }
9954
10168
  }
9955
10169
  },
9956
- addToPath: function addToPath(path32, added, removed, oldPosInc, options) {
9957
- var last = path32.lastComponent;
10170
+ addToPath: function addToPath(path39, added, removed, oldPosInc, options) {
10171
+ var last = path39.lastComponent;
9958
10172
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
9959
10173
  return {
9960
- oldPos: path32.oldPos + oldPosInc,
10174
+ oldPos: path39.oldPos + oldPosInc,
9961
10175
  lastComponent: {
9962
10176
  count: last.count + 1,
9963
10177
  added,
@@ -9967,7 +10181,7 @@ var init_lib = __esm({
9967
10181
  };
9968
10182
  } else {
9969
10183
  return {
9970
- oldPos: path32.oldPos + oldPosInc,
10184
+ oldPos: path39.oldPos + oldPosInc,
9971
10185
  lastComponent: {
9972
10186
  count: 1,
9973
10187
  added,
@@ -10025,7 +10239,7 @@ var init_lib = __esm({
10025
10239
  tokenize: function tokenize(value) {
10026
10240
  return Array.from(value);
10027
10241
  },
10028
- join: function join3(chars) {
10242
+ join: function join4(chars) {
10029
10243
  return chars.join("");
10030
10244
  },
10031
10245
  postProcess: function postProcess(changeObjects) {
@@ -10398,10 +10612,10 @@ function attachmentToHistoryMessage(o, ts) {
10398
10612
  const memories = raw.map((m2) => {
10399
10613
  if (!m2 || typeof m2 !== "object") return null;
10400
10614
  const rec = m2;
10401
- const path32 = typeof rec.path === "string" ? rec.path : null;
10615
+ const path39 = typeof rec.path === "string" ? rec.path : null;
10402
10616
  const content = typeof rec.content === "string" ? rec.content : null;
10403
- if (!path32 || content == null) return null;
10404
- const entry = { path: path32, content };
10617
+ if (!path39 || content == null) return null;
10618
+ const entry = { path: path39, content };
10405
10619
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10406
10620
  return entry;
10407
10621
  }).filter((m2) => m2 !== null);
@@ -10861,6 +11075,27 @@ var init_claude_history = __esm({
10861
11075
  }
10862
11076
  });
10863
11077
 
11078
+ // src/tools/sandbox.ts
11079
+ function shouldSandbox(cwd, personaRoot) {
11080
+ if (!personaRoot) return false;
11081
+ const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
11082
+ return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
11083
+ }
11084
+ function inferSandboxSettingsPath(cwd, personaRoot) {
11085
+ if (!shouldSandbox(cwd, personaRoot)) return null;
11086
+ const rel = path12.relative(personaRoot, cwd);
11087
+ const personaId = rel.split(path12.sep)[0];
11088
+ if (!personaId) return null;
11089
+ return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
11090
+ }
11091
+ var path12;
11092
+ var init_sandbox = __esm({
11093
+ "src/tools/sandbox.ts"() {
11094
+ "use strict";
11095
+ path12 = __toESM(require("path"), 1);
11096
+ }
11097
+ });
11098
+
10864
11099
  // src/tools/claude.ts
10865
11100
  function macOSDesktopCandidates(home) {
10866
11101
  return [
@@ -10910,7 +11145,7 @@ function buildSpawnArgs(ctx) {
10910
11145
  ];
10911
11146
  if (ctx.model) args.push("--model", ctx.model);
10912
11147
  switch (ctx.personaMode) {
10913
- case "listener":
11148
+ case "guest":
10914
11149
  args.push("--setting-sources", "project,local");
10915
11150
  break;
10916
11151
  case "owner":
@@ -10925,7 +11160,8 @@ function buildSpawnArgs(ctx) {
10925
11160
  throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
10926
11161
  }
10927
11162
  }
10928
- if (ctx.extraSettings) args.push("--settings", ctx.extraSettings);
11163
+ const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
11164
+ if (sandboxSettings) args.push("--settings", sandboxSettings);
10929
11165
  if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
10930
11166
  if (ctx.effort) args.push("--effort", ctx.effort);
10931
11167
  if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
@@ -11205,10 +11441,10 @@ function parseAttachment(obj) {
11205
11441
  const memories = raw.map((m2) => {
11206
11442
  if (!m2 || typeof m2 !== "object") return null;
11207
11443
  const rec = m2;
11208
- const path32 = typeof rec.path === "string" ? rec.path : null;
11444
+ const path39 = typeof rec.path === "string" ? rec.path : null;
11209
11445
  const content = typeof rec.content === "string" ? rec.content : null;
11210
- if (!path32 || content == null) return null;
11211
- const out = { path: path32, content };
11446
+ if (!path39 || content == null) return null;
11447
+ const out = { path: path39, content };
11212
11448
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11213
11449
  return out;
11214
11450
  }).filter((m2) => m2 !== null);
@@ -11324,6 +11560,7 @@ var init_claude = __esm({
11324
11560
  init_protocol();
11325
11561
  init_claude_history();
11326
11562
  init_tool_result_extra();
11563
+ init_sandbox();
11327
11564
  ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
11328
11565
  "hook_additional_context",
11329
11566
  "hook_success",
@@ -18706,7 +18943,7 @@ var require_websocket = __commonJS({
18706
18943
  var http2 = require("http");
18707
18944
  var net = require("net");
18708
18945
  var tls = require("tls");
18709
- var { randomBytes, createHash } = require("crypto");
18946
+ var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
18710
18947
  var { Duplex, Readable: Readable3 } = require("stream");
18711
18948
  var { URL: URL2 } = require("url");
18712
18949
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19236,7 +19473,7 @@ var require_websocket = __commonJS({
19236
19473
  }
19237
19474
  }
19238
19475
  const defaultPort = isSecure ? 443 : 80;
19239
- const key = randomBytes(16).toString("base64");
19476
+ const key = randomBytes2(16).toString("base64");
19240
19477
  const request = isSecure ? https.request : http2.request;
19241
19478
  const protocolSet = /* @__PURE__ */ new Set();
19242
19479
  let perMessageDeflate;
@@ -19366,7 +19603,7 @@ var require_websocket = __commonJS({
19366
19603
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19367
19604
  return;
19368
19605
  }
19369
- const digest = createHash("sha1").update(key + GUID).digest("base64");
19606
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
19370
19607
  if (res.headers["sec-websocket-accept"] !== digest) {
19371
19608
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19372
19609
  return;
@@ -19733,7 +19970,7 @@ var require_websocket_server = __commonJS({
19733
19970
  var EventEmitter2 = require("events");
19734
19971
  var http2 = require("http");
19735
19972
  var { Duplex } = require("stream");
19736
- var { createHash } = require("crypto");
19973
+ var { createHash: createHash3 } = require("crypto");
19737
19974
  var extension2 = require_extension();
19738
19975
  var PerMessageDeflate2 = require_permessage_deflate();
19739
19976
  var subprotocol2 = require_subprotocol();
@@ -20034,7 +20271,7 @@ var require_websocket_server = __commonJS({
20034
20271
  );
20035
20272
  }
20036
20273
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20037
- const digest = createHash("sha1").update(key + GUID).digest("base64");
20274
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
20038
20275
  const headers = [
20039
20276
  "HTTP/1.1 101 Switching Protocols",
20040
20277
  "Upgrade: websocket",
@@ -20653,6 +20890,9 @@ function createLogger(opts = {}) {
20653
20890
  return wrap(base);
20654
20891
  }
20655
20892
 
20893
+ // src/session/store-factory.ts
20894
+ var path5 = __toESM(require("path"), 1);
20895
+
20656
20896
  // src/session/store.ts
20657
20897
  var import_node_fs3 = __toESM(require("fs"), 1);
20658
20898
  var import_node_path4 = __toESM(require("path"), 1);
@@ -20661,10 +20901,14 @@ init_protocol();
20661
20901
  // src/session/scope.ts
20662
20902
  var import_node_path3 = __toESM(require("path"), 1);
20663
20903
  function scopeKey(scope) {
20664
- return scope.kind === "default" ? "default" : `persona:${scope.personaId}:${scope.mode}`;
20904
+ if (scope.kind === "default") return "default";
20905
+ if (scope.mode === "guest") return `persona:${scope.personaId}:guest:${scope.capId}`;
20906
+ return `persona:${scope.personaId}:${scope.mode}`;
20665
20907
  }
20666
20908
  function scopeSubPath(scope) {
20667
- return scope.kind === "default" ? ["default"] : [scope.personaId, scope.mode];
20909
+ if (scope.kind === "default") return ["default"];
20910
+ if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
20911
+ return [scope.personaId, scope.mode];
20668
20912
  }
20669
20913
  function metaFromScope(scope, personaRoot) {
20670
20914
  if (scope.kind === "default") return void 0;
@@ -20673,10 +20917,18 @@ function metaFromScope(scope, personaRoot) {
20673
20917
  }
20674
20918
  return {
20675
20919
  idleKillEnabled: true,
20676
- personaMode: "listener",
20920
+ personaMode: "guest",
20677
20921
  extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json")
20678
20922
  };
20679
20923
  }
20924
+ function scopeForFile(file, ownerPrincipalId) {
20925
+ if (!file.ownerPersonaId) return { kind: "default" };
20926
+ const creator = file.creatorPrincipalId;
20927
+ if (ownerPrincipalId && creator && creator !== ownerPrincipalId) {
20928
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "guest", capId: creator };
20929
+ }
20930
+ return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
20931
+ }
20680
20932
 
20681
20933
  // src/session/store.ts
20682
20934
  function safeFileName(sessionId) {
@@ -20690,12 +20942,16 @@ function safeFileName(sessionId) {
20690
20942
  var SessionStore = class {
20691
20943
  root;
20692
20944
  constructor(opts) {
20693
- const scope = opts.scope ?? { kind: "default" };
20694
- this.root = import_node_path4.default.join(
20695
- opts.dataDir,
20696
- "sessions",
20697
- ...scopeSubPath(scope).map(safeFileName)
20698
- );
20945
+ if ("root" in opts) {
20946
+ this.root = opts.root;
20947
+ } else {
20948
+ const scope = opts.scope ?? { kind: "default" };
20949
+ this.root = import_node_path4.default.join(
20950
+ opts.dataDir,
20951
+ "sessions",
20952
+ ...scopeSubPath(scope).map(safeFileName)
20953
+ );
20954
+ }
20699
20955
  }
20700
20956
  filePath(sessionId) {
20701
20957
  return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
@@ -20760,6 +21016,66 @@ var SessionStore = class {
20760
21016
  }
20761
21017
  };
20762
21018
 
21019
+ // src/session/store-factory.ts
21020
+ var SessionStoreFactory = class {
21021
+ dataDir;
21022
+ bareStore = null;
21023
+ vmOwnerStores = /* @__PURE__ */ new Map();
21024
+ // vmGuest 索引 key = `${pid}::${capId}`(按 cap.id 隔离,一张 cap 一个目录)
21025
+ vmGuestStores = /* @__PURE__ */ new Map();
21026
+ constructor(opts) {
21027
+ this.dataDir = opts.dataDir;
21028
+ }
21029
+ // ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
21030
+ bareRoot() {
21031
+ return path5.join(this.dataDir, "sessions");
21032
+ }
21033
+ vmOwnerRoot(personaId) {
21034
+ return path5.join(
21035
+ this.dataDir,
21036
+ "personas",
21037
+ safeFileName(personaId),
21038
+ ".clawd",
21039
+ "sessions",
21040
+ "owner"
21041
+ );
21042
+ }
21043
+ vmGuestRoot(personaId, capId) {
21044
+ return path5.join(
21045
+ this.dataDir,
21046
+ "personas",
21047
+ safeFileName(personaId),
21048
+ ".clawd",
21049
+ "sessions",
21050
+ "guests",
21051
+ safeFileName(capId)
21052
+ );
21053
+ }
21054
+ // ---- SessionStore 工厂(缓存) ----
21055
+ forBare() {
21056
+ if (!this.bareStore) {
21057
+ this.bareStore = new SessionStore({ root: this.bareRoot() });
21058
+ }
21059
+ return this.bareStore;
21060
+ }
21061
+ forVmOwner(personaId) {
21062
+ const key = personaId;
21063
+ const cached = this.vmOwnerStores.get(key);
21064
+ if (cached) return cached;
21065
+ const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
21066
+ this.vmOwnerStores.set(key, st);
21067
+ return st;
21068
+ }
21069
+ forVmGuest(personaId, capId) {
21070
+ const key = `${personaId}::${capId}`;
21071
+ const cached = this.vmGuestStores.get(key);
21072
+ if (cached) return cached;
21073
+ const st = new SessionStore({ root: this.vmGuestRoot(personaId, capId) });
21074
+ this.vmGuestStores.set(key, st);
21075
+ return st;
21076
+ }
21077
+ };
21078
+
20763
21079
  // src/session/manager.ts
20764
21080
  var import_node_fs5 = __toESM(require("fs"), 1);
20765
21081
  var import_node_path6 = __toESM(require("path"), 1);
@@ -20881,7 +21197,7 @@ function cloneState(s) {
20881
21197
  pendingSend: s.pendingSend
20882
21198
  };
20883
21199
  }
20884
- var IDLE_KILL_DELAY_MS = 5e3;
21200
+ var IDLE_KILL_DELAY_MS = 6e4;
20885
21201
  var SEEN_UUID_CAP = 2e3;
20886
21202
  function applyEventBatch(state, events, deps) {
20887
21203
  if (events.length === 0) return { state, effects: [] };
@@ -20921,7 +21237,10 @@ function buildSpawnContext(state, deps) {
20921
21237
  toolSessionId: file.toolSessionId,
20922
21238
  model: file.model,
20923
21239
  permissionMode: file.permissionMode,
20924
- effort: file.effort
21240
+ effort: file.effort,
21241
+ // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
21242
+ // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
21243
+ personaRoot: deps.personaRoot
20925
21244
  };
20926
21245
  const meta = state.subSessionMeta;
20927
21246
  if (meta?.personaMode) {
@@ -21706,6 +22025,7 @@ var SessionRunner = class {
21706
22025
  resolveContextWindow: this.hooks.resolveContextWindow,
21707
22026
  genUuid: this.hooks.genUuid ?? v4_default,
21708
22027
  ownerDisplayName: this.hooks.ownerDisplayName,
22028
+ personaRoot: this.hooks.personaRoot,
21709
22029
  mode: this.hooks.mode ?? "sdk"
21710
22030
  };
21711
22031
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
@@ -22137,9 +22457,18 @@ var SessionManager = class {
22137
22457
  attachObserver(observer) {
22138
22458
  this.attachedObserver = observer;
22139
22459
  }
22140
- // 按 scope 拿对应的 SessionStore。default scope 复用 deps.store(保证与
22141
- // bootstrap 注入的 store 一致);persona scope 第一次访问时按需创建并缓存。
22460
+ // 按 scope 拿对应的 SessionStore.
22461
+ // Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
22462
+ // default → factory.forBare() <dataDir>/sessions/
22463
+ // persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
22464
+ // persona/<pid>/guest → factory.forVmGuest(pid, capId) <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
22465
+ // 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
22142
22466
  storeFor(scope) {
22467
+ if (this.deps.storeFactory) {
22468
+ if (scope.kind === "default") return this.deps.storeFactory.forBare();
22469
+ if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
22470
+ return this.deps.storeFactory.forVmGuest(scope.personaId, scope.capId);
22471
+ }
22143
22472
  if (scope.kind === "default") return this.deps.store;
22144
22473
  const key = scopeKey(scope);
22145
22474
  const cached = this.storesByScope.get(key);
@@ -22159,19 +22488,18 @@ var SessionManager = class {
22159
22488
  this.capabilitiesCache.set(tool, caps);
22160
22489
  return caps;
22161
22490
  }
22162
- // 由 SessionFile.ownerPersonaId schema 字段推回写入 scope
22163
- // ownerPersonaId create({ownerPersonaId}) schema 落盘时记入,永远只反映
22164
- // session 出身(不可变);此函数只用于 SessionStore 的写入路由,不参与运行时行为
22165
- // 决策(personaMode='owner' 等由路径 → scope → metaFromScope 派生)。
22166
- // 仅覆盖 owner + default 两类——listener sub-session 不会从 default RPC 路径进来。
22491
+ // 由 SessionFile 反推 scope 的 thin wrapper —— 决策矩阵收敛到 scope.scopeForFile
22492
+ // free function(scope.spec.ts transition 表驱动测试),manager 只负责注入 deps.
22167
22493
  scopeForFile(file) {
22168
- return file.ownerPersonaId ? { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" } : { kind: "default" };
22494
+ return scopeForFile(file, this.deps.ownerPrincipalId);
22169
22495
  }
22170
- // <dataDir>/sessions/ 列出所有 persona 命名空间(不含 'default')。
22171
- // 用于 findOwnedSession / listAllOwned scope 查询。
22496
+ // 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned scope 查询.
22497
+ // Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
22498
+ // 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
22499
+ // <dataDir>/sessions/ 列子目录 (排除 'default').
22172
22500
  listPersonaIdsOnDisk() {
22173
22501
  if (!this.deps.dataDir) return [];
22174
- const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
22502
+ const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
22175
22503
  let entries;
22176
22504
  try {
22177
22505
  entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
@@ -22182,13 +22510,37 @@ var SessionManager = class {
22182
22510
  }
22183
22511
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
22184
22512
  }
22185
- // owner / default 两个分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
22186
- // SessionManager 在这里定位。三层快慢路径:
22513
+ // 扫某 persona guests/ 子目录, 拿所有 capId.
22514
+ // 路径: <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
22515
+ // 只在 storeFactory 注入 (新布局) 下生效, 老布局无 guest 目录.
22516
+ listGuestCapIdsForPersona(personaId) {
22517
+ if (!this.deps.dataDir || !this.deps.storeFactory) return [];
22518
+ const root = import_node_path6.default.join(
22519
+ this.deps.dataDir,
22520
+ "personas",
22521
+ personaId,
22522
+ ".clawd",
22523
+ "sessions",
22524
+ "guests"
22525
+ );
22526
+ let entries;
22527
+ try {
22528
+ entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
22529
+ } catch (err) {
22530
+ const code = err?.code;
22531
+ if (code === "ENOENT") return [];
22532
+ throw err;
22533
+ }
22534
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
22535
+ }
22536
+ // owner / default + persona guests 三大分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
22537
+ // SessionManager 在这里定位。四层快慢路径:
22187
22538
  // 1) active runner(用户当前在用的 session):runner.state.file 是 SessionFile 内存权威副本,
22188
22539
  // 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
22189
22540
  // 绝大多数场景
22190
22541
  // 2) default scope 磁盘:inactive default session 命中
22191
22542
  // 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
22543
+ // 4) 每个 persona 的 guests/<capId>/ 扫盘:guest 通过我颁发 token 接进来创建的 session
22192
22544
  // listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
22193
22545
  //
22194
22546
  // 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
@@ -22200,8 +22552,13 @@ var SessionManager = class {
22200
22552
  if (dflt) return dflt;
22201
22553
  for (const personaId of this.listPersonaIdsOnDisk()) {
22202
22554
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22203
- const file = ownerStore.read(sessionId);
22204
- if (file) return file;
22555
+ const found = ownerStore.read(sessionId);
22556
+ if (found) return found;
22557
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22558
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22559
+ const g2 = guestStore.read(sessionId);
22560
+ if (g2) return g2;
22561
+ }
22205
22562
  }
22206
22563
  return null;
22207
22564
  }
@@ -22212,30 +22569,40 @@ var SessionManager = class {
22212
22569
  const file = this.findOwnedSession(sessionId);
22213
22570
  return file ? this.scopeForFile(file) : null;
22214
22571
  }
22215
- // 合并 default + 所有 persona owner SessionFile —— 桌面 App 主 session 列表入口。
22216
- // 同样不含 listener sub-session(listener persona:listSubSessions RPC 单独入口)。
22572
+ // 合并 default + 每个 persona owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
22573
+ // guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
22574
+ // (creatorPrincipalId === capId), 这里仅负责数据源汇总.
22575
+ // listener sub-session 不走这条(listener 走 persona:listSubSessions RPC 单独入口)。
22217
22576
  listAllOwned() {
22218
22577
  const out = [];
22219
22578
  out.push(...this.deps.store.list());
22220
22579
  for (const personaId of this.listPersonaIdsOnDisk()) {
22221
22580
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22222
22581
  out.push(...ownerStore.list());
22582
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22583
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22584
+ out.push(...guestStore.list());
22585
+ }
22223
22586
  }
22224
22587
  return out.sort(
22225
22588
  (a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
22226
22589
  );
22227
22590
  }
22228
- // 写回 SessionFile,自动根据 ownerPersonaId 派生写入 scope
22229
- // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由。
22591
+ // 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
22592
+ // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
22230
22593
  writeOwned(file) {
22231
22594
  return this.storeFor(this.scopeForFile(file)).write(file);
22232
22595
  }
22233
- // 删除 owned SessionFile(default persona/owner),与 findOwnedSession 对称扫盘。
22596
+ // 删除 owned SessionFile(default / persona owner / persona guest),与 findOwnedSession 对称扫盘.
22234
22597
  deleteOwned(sessionId) {
22235
22598
  if (this.deps.store.delete(sessionId)) return true;
22236
22599
  for (const personaId of this.listPersonaIdsOnDisk()) {
22237
22600
  const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
22238
22601
  if (ownerStore.delete(sessionId)) return true;
22602
+ for (const capId of this.listGuestCapIdsForPersona(personaId)) {
22603
+ const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
22604
+ if (guestStore.delete(sessionId)) return true;
22605
+ }
22239
22606
  }
22240
22607
  return false;
22241
22608
  }
@@ -22243,8 +22610,8 @@ var SessionManager = class {
22243
22610
  // 经过 compressFrameForWire 后决定是 push collector 还是走 deps.broadcastFrame。
22244
22611
  // scope 决定两件事:SessionStore 路由 + SubSessionMeta 派生(metaFromScope)。
22245
22612
  // - default → 复用 deps.store,无 subSessionMeta(普通 session)
22246
- // - persona/owner → <dataDir>/sessions/<personaId>/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
22247
- // - persona/listener<dataDir>/sessions/<personaId>/listener/,meta={idleKillEnabled:true, personaMode:'listener', extraSettings:<sandbox>}
22613
+ // - persona/owner → personas/<pid>/.clawd/sessions/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
22614
+ // - persona/guestpersonas/<pid>/.clawd/sessions/guests/<capId>/,meta={idleKillEnabled:true, personaMode:'guest', extraSettings:<sandbox>}
22248
22615
  newRunner(file, scope) {
22249
22616
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
22250
22617
  const store = this.storeFor(scope);
@@ -22263,6 +22630,9 @@ var SessionManager = class {
22263
22630
  dataDir: this.deps.dataDir,
22264
22631
  personaStore: this.deps.personaStore,
22265
22632
  ownerDisplayName: this.deps.ownerDisplayName,
22633
+ // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
22634
+ // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
22635
+ personaRoot: this.deps.personaRoot,
22266
22636
  // 透传给 ReducerDeps.mode:reducer send !procAlive spawn 路径据此判断要不要把
22267
22637
  // readyForSend 翻 false(TUI 翻 false 让 user_text 走 pendingSend 暂存;SDK 不动保持直写)
22268
22638
  mode: this.deps.mode,
@@ -22360,7 +22730,7 @@ var SessionManager = class {
22360
22730
  }
22361
22731
  }
22362
22732
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
22363
- create(args) {
22733
+ create(args, creatorPrincipalId) {
22364
22734
  let cwd;
22365
22735
  if (args.ownerPersonaId) {
22366
22736
  cwd = derivePersonaSpawnCwd(
@@ -22401,10 +22771,11 @@ var SessionManager = class {
22401
22771
  iconKey: args.iconKey,
22402
22772
  forkedFromSessionId: args.forkedFromSessionId,
22403
22773
  ownerPersonaId: args.ownerPersonaId,
22774
+ creatorPrincipalId,
22404
22775
  createdAt: iso,
22405
22776
  updatedAt: iso
22406
22777
  };
22407
- const scope = args.ownerPersonaId ? { kind: "persona", personaId: args.ownerPersonaId, mode: "owner" } : { kind: "default" };
22778
+ const scope = this.scopeForFile(file);
22408
22779
  const written = this.storeFor(scope).write(file);
22409
22780
  return { response: written, broadcast: [] };
22410
22781
  }
@@ -22844,28 +23215,18 @@ var SessionManager = class {
22844
23215
  this.runners.set(file.sessionId, r);
22845
23216
  return r;
22846
23217
  }
22847
- // ---------------- persona / listener sub-session 专用 API ----------------
23218
+ // ---------------- scope-explicit sub-session API ----------------
22848
23219
  //
22849
- // PersonaManager SessionManager 之上的薄编排层,listener sub-session 的持久化
22850
- // (SessionFile)仍由 SessionManager 持有。区别于普通 / owner session:
22851
- // - SessionFile 落到 <dataDir>/sessions/<personaId>/listener/(拓扑分类)
22852
- // - reducer state.subSessionMeta.idleKillEnabled = true(turn_end 自动 idle-kill)
22853
- // - sessionId 由 PersonaManager 派生(personaId-<tokenHash>),不走自动 newSessionId
23220
+ // 区别于 owner / default sessionId 入口,这些 API 接 SessionScope 参数 ——
23221
+ // 调用方(PersonaManager / observer 等)明确传 scope,由 storeFor 路由到正确目录:
23222
+ // - {kind:'persona', personaId, mode:'guest', capId} → guests/<capId>/,meta={idleKill:true, sandbox}
23223
+ // - {kind:'persona', personaId, mode:'owner'} → owner/,meta={idleKill:false}
22854
23224
  //
22855
- // 这些 API SessionScope 参数(不再是单参 agentId)—— transport 始终明确传
22856
- // {kind:'persona', personaId, mode:'listener'},与 owner / default 分类隔离。
23225
+ // SubSessionMeta scope metaFromScope 派生,不进 SessionFile schema。
22857
23226
  /** 按 scope 读取 SessionFile;不存在返回 null */
22858
23227
  readForScope(sessionId, scope) {
22859
23228
  return this.storeFor(scope).read(sessionId);
22860
23229
  }
22861
- /**
22862
- * 列出指定 persona 在指定 mode 下的所有 SessionFile。
22863
- * persona:listSubSessions RPC 入口(mode='listener');
22864
- * 路径下还未写过任何 session 时 list() 返回空数组(SessionStore.list 内部 ENOENT 处理)
22865
- */
22866
- listPersonaSessions(personaId, mode) {
22867
- return this.storeFor({ kind: "persona", personaId, mode }).list();
22868
- }
22869
23230
  /**
22870
23231
  * 创建 sub-session 的 SessionFile + 准备 runner(不 spawn)。
22871
23232
  * SubSessionMeta 不进 SessionFile schema,运行时由 scope → metaFromScope 派生。
@@ -23010,7 +23371,7 @@ var SessionManager = class {
23010
23371
  return { response: { ok: true }, broadcast };
23011
23372
  }
23012
23373
  /**
23013
- * 改 SessionFile.label(用于 listener-bound chat:rename RPC)。
23374
+ * 改 SessionFile.label(scope-explicit sub-session 重命名入口)。
23014
23375
  * 不存在抛 SESSION_NOT_FOUND。
23015
23376
  */
23016
23377
  renameForScope(args) {
@@ -23025,7 +23386,7 @@ var SessionManager = class {
23025
23386
  store.write({ ...file, label: args.label });
23026
23387
  }
23027
23388
  /**
23028
- * 删除 listener-scope sub-session(用于 chat:delete RPC)。
23389
+ * 删除 scope-explicit sub-session
23029
23390
  * - 若 runner 存在:走 reducer 'delete' command 收尾(SIGKILL + 文件 unlink + broadcast)
23030
23391
  * - 若 runner 不存在:直接删 scope store 文件 + 广播 session:deleted
23031
23392
  * 不存在抛 SESSION_NOT_FOUND。
@@ -23296,7 +23657,7 @@ var SessionManager = class {
23296
23657
 
23297
23658
  // src/persona/store.ts
23298
23659
  var fs6 = __toESM(require("fs"), 1);
23299
- var path7 = __toESM(require("path"), 1);
23660
+ var path8 = __toESM(require("path"), 1);
23300
23661
  init_protocol();
23301
23662
  var DEFAULT_SETTINGS = {
23302
23663
  permissions: {
@@ -23325,13 +23686,13 @@ var PersonaStore = class {
23325
23686
  }
23326
23687
  root;
23327
23688
  personaDir(personaId) {
23328
- return path7.join(this.root, safeFileName(personaId));
23689
+ return path8.join(this.root, safeFileName(personaId));
23329
23690
  }
23330
23691
  metaPath(personaId) {
23331
- return path7.join(this.personaDir(personaId), ".clawd", "persona.json");
23692
+ return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
23332
23693
  }
23333
23694
  claudeMdPath(personaId) {
23334
- return path7.join(this.personaDir(personaId), "CLAUDE.md");
23695
+ return path8.join(this.personaDir(personaId), "CLAUDE.md");
23335
23696
  }
23336
23697
  /**
23337
23698
  * Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
@@ -23341,11 +23702,11 @@ var PersonaStore = class {
23341
23702
  * 加载 persona 人格,只有 listener 多一层 OS sandbox。
23342
23703
  */
23343
23704
  sandboxSettingsPath(personaId) {
23344
- return path7.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23705
+ return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23345
23706
  }
23346
23707
  write(persona, personality) {
23347
23708
  const dir = this.personaDir(persona.personaId);
23348
- fs6.mkdirSync(path7.join(dir, ".clawd"), { recursive: true });
23709
+ fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
23349
23710
  this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
23350
23711
  this.atomicWrite(
23351
23712
  this.sandboxSettingsPath(persona.personaId),
@@ -23363,6 +23724,10 @@ var PersonaStore = class {
23363
23724
  const p2 = this.metaPath(personaId);
23364
23725
  if (!fs6.existsSync(p2)) return null;
23365
23726
  const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23727
+ if (raw && typeof raw === "object" && "tokenMap" in raw) {
23728
+ delete raw.tokenMap;
23729
+ this.atomicWrite(p2, JSON.stringify(raw, null, 2));
23730
+ }
23366
23731
  return PersonaFileSchema.parse(raw);
23367
23732
  }
23368
23733
  has(personaId) {
@@ -23388,14 +23753,14 @@ var PersonaStore = class {
23388
23753
  }
23389
23754
  /** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
23390
23755
  skillsDir(personaId) {
23391
- return path7.join(this.personaDir(personaId), ".claude", "skills");
23756
+ return path8.join(this.personaDir(personaId), ".claude", "skills");
23392
23757
  }
23393
23758
  /**
23394
23759
  * Claude Code 项目级 settings 路径:`<personaDir>/.claude/settings.json`。
23395
23760
  * 这里只读 `enabledPlugins` 字段,由 owner 通过 CC `/plugin` 之类命令维护,daemon 不写。
23396
23761
  */
23397
23762
  claudeSettingsPath(personaId) {
23398
- return path7.join(this.personaDir(personaId), ".claude", "settings.json");
23763
+ return path8.join(this.personaDir(personaId), ".claude", "settings.json");
23399
23764
  }
23400
23765
  /**
23401
23766
  * 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
@@ -23429,7 +23794,7 @@ var PersonaStore = class {
23429
23794
  list() {
23430
23795
  if (!fs6.existsSync(this.root)) return [];
23431
23796
  return fs6.readdirSync(this.root).filter((name) => {
23432
- return fs6.existsSync(path7.join(this.root, name, ".clawd", "persona.json"));
23797
+ return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
23433
23798
  });
23434
23799
  }
23435
23800
  remove(personaId) {
@@ -23454,12 +23819,19 @@ var PersonaRegistry = class {
23454
23819
  }
23455
23820
  store;
23456
23821
  cache = /* @__PURE__ */ new Map();
23457
- /** 从 store 全量重建缓存(boot 时 + 测试 setup 时) */
23822
+ /**
23823
+ * 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
23824
+ * 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
23825
+ * try/catch 隔离,让其它 persona 仍能加载。
23826
+ */
23458
23827
  reload() {
23459
23828
  this.cache.clear();
23460
23829
  for (const id of this.store.list()) {
23461
- const p2 = this.store.read(id);
23462
- if (p2) this.cache.set(p2.personaId, p2);
23830
+ try {
23831
+ const p2 = this.store.read(id);
23832
+ if (p2) this.cache.set(p2.personaId, p2);
23833
+ } catch {
23834
+ }
23463
23835
  }
23464
23836
  }
23465
23837
  get(personaId) {
@@ -23475,21 +23847,6 @@ var PersonaRegistry = class {
23475
23847
  list() {
23476
23848
  return Array.from(this.cache.values());
23477
23849
  }
23478
- /**
23479
- * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23480
- * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23481
- * persona 数量通常 < 100,性能可忽略。
23482
- */
23483
- findByToken(token) {
23484
- if (!token) return null;
23485
- for (const persona of this.cache.values()) {
23486
- if (!persona.public) continue;
23487
- const entry = persona.tokenMap?.[token];
23488
- if (!entry || entry.revoked) continue;
23489
- return { personaId: persona.personaId, label: entry.label };
23490
- }
23491
- return null;
23492
- }
23493
23850
  };
23494
23851
 
23495
23852
  // src/persona/manager.ts
@@ -23783,9 +24140,6 @@ var PersonaManager = class {
23783
24140
  model: args.model,
23784
24141
  public: args.public ?? false,
23785
24142
  iconKey: args.iconKey,
23786
- // 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
23787
- // file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
23788
- tokenMap: {},
23789
24143
  createdAt: now,
23790
24144
  updatedAt: now
23791
24145
  };
@@ -23835,52 +24189,22 @@ var PersonaManager = class {
23835
24189
  }
23836
24190
  /**
23837
24191
  * 删除 persona。
23838
- * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录。
24192
+ * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
24193
+ * Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
24194
+ * .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
24195
+ *
24196
+ * ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
24197
+ * ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
24198
+ * 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
24199
+ * 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
24200
+ * 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
24201
+ * force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
24202
+ * "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
23839
24203
  */
23840
24204
  delete(personaId) {
23841
24205
  this.deps.store.remove(personaId);
23842
24206
  this.deps.registry.remove(personaId);
23843
24207
  }
23844
- /**
23845
- * 生成 32-char base64url token 并写入 tokenMap,给 file-sharing HTTP Bearer
23846
- * 鉴权用(PersonaRegistry.findByToken 反查 personaId/label)。
23847
- */
23848
- issueToken(personaId, label) {
23849
- const existing = this.deps.registry.get(personaId);
23850
- if (!existing) throw new Error(`persona not found: ${personaId}`);
23851
- const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
23852
- const entry = {
23853
- label,
23854
- issuedAt: Date.now(),
23855
- revoked: false
23856
- };
23857
- const updated = {
23858
- ...existing,
23859
- tokenMap: { ...existing.tokenMap ?? {}, [token]: entry },
23860
- updatedAt: Date.now()
23861
- };
23862
- this.deps.store.writeMeta(updated);
23863
- this.deps.registry.set(updated);
23864
- return { token, persona: updated };
23865
- }
23866
- /** 标记 token 为 revoked(保留条目用于审计);不存在的 token 抛错。 */
23867
- revokeToken(personaId, token) {
23868
- const existing = this.deps.registry.get(personaId);
23869
- if (!existing) throw new Error(`persona not found: ${personaId}`);
23870
- const tokenEntry = existing.tokenMap?.[token];
23871
- if (!tokenEntry) throw new Error(`token not found in persona ${personaId}`);
23872
- const updated = {
23873
- ...existing,
23874
- tokenMap: {
23875
- ...existing.tokenMap ?? {},
23876
- [token]: { ...tokenEntry, revoked: true }
23877
- },
23878
- updatedAt: Date.now()
23879
- };
23880
- this.deps.store.writeMeta(updated);
23881
- this.deps.registry.set(updated);
23882
- return updated;
23883
- }
23884
24208
  /**
23885
24209
  * label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
23886
24210
  * base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
@@ -23900,7 +24224,7 @@ var PersonaManager = class {
23900
24224
 
23901
24225
  // src/persona/seed.ts
23902
24226
  var fs8 = __toESM(require("fs"), 1);
23903
- var path9 = __toESM(require("path"), 1);
24227
+ var path10 = __toESM(require("path"), 1);
23904
24228
  var import_node_url = require("url");
23905
24229
  var import_meta = {};
23906
24230
  var DEFAULT_PERSONAS = [
@@ -23936,14 +24260,14 @@ var DEFAULT_PERSONAS = [
23936
24260
  function findDefaultsRoot() {
23937
24261
  const candidates = [];
23938
24262
  try {
23939
- const here = path9.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
23940
- candidates.push(path9.resolve(here, "defaults"));
23941
- candidates.push(path9.resolve(here, "persona-defaults"));
24263
+ const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
24264
+ candidates.push(path10.resolve(here, "defaults"));
24265
+ candidates.push(path10.resolve(here, "persona-defaults"));
23942
24266
  } catch {
23943
24267
  }
23944
24268
  if (process.argv[1]) {
23945
- const argvDir = path9.dirname(process.argv[1]);
23946
- candidates.push(path9.resolve(argvDir, "persona-defaults"));
24269
+ const argvDir = path10.dirname(process.argv[1]);
24270
+ candidates.push(path10.resolve(argvDir, "persona-defaults"));
23947
24271
  }
23948
24272
  for (const c of candidates) {
23949
24273
  try {
@@ -23960,7 +24284,7 @@ function seedDefaultPersonas(args) {
23960
24284
  args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
23961
24285
  continue;
23962
24286
  }
23963
- const bundleDir = path9.join(args.defaultsRoot, entry.personaId);
24287
+ const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
23964
24288
  if (!fs8.existsSync(bundleDir)) {
23965
24289
  args.logger.warn("persona.seed.skip", {
23966
24290
  personaId: entry.personaId,
@@ -23969,7 +24293,7 @@ function seedDefaultPersonas(args) {
23969
24293
  });
23970
24294
  continue;
23971
24295
  }
23972
- const claudeMdPath = path9.join(bundleDir, "CLAUDE.md");
24296
+ const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
23973
24297
  if (!fs8.existsSync(claudeMdPath)) {
23974
24298
  args.logger.warn("persona.seed.skip", {
23975
24299
  personaId: entry.personaId,
@@ -23986,7 +24310,6 @@ function seedDefaultPersonas(args) {
23986
24310
  model: entry.model,
23987
24311
  public: entry.public,
23988
24312
  iconKey: entry.iconKey,
23989
- tokenMap: {},
23990
24313
  createdAt: now,
23991
24314
  updatedAt: now
23992
24315
  };
@@ -23998,8 +24321,8 @@ function seedDefaultPersonas(args) {
23998
24321
  function copyBundleExtras(srcDir, dstDir) {
23999
24322
  for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
24000
24323
  if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
24001
- const srcPath = path9.join(srcDir, entry.name);
24002
- const dstPath = path9.join(dstDir, entry.name);
24324
+ const srcPath = path10.join(srcDir, entry.name);
24325
+ const dstPath = path10.join(dstDir, entry.name);
24003
24326
  if (entry.isDirectory()) {
24004
24327
  fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
24005
24328
  } else if (entry.isFile()) {
@@ -25340,6 +25663,7 @@ var import_sender = __toESM(require_sender(), 1);
25340
25663
  var import_subprotocol = __toESM(require_subprotocol(), 1);
25341
25664
  var import_websocket = __toESM(require_websocket(), 1);
25342
25665
  var import_websocket_server = __toESM(require_websocket_server(), 1);
25666
+ var wrapper_default = import_websocket.default;
25343
25667
 
25344
25668
  // src/transport/local-ws-server.ts
25345
25669
  var import_node_http = __toESM(require("http"), 1);
@@ -25354,6 +25678,9 @@ var LocalWsServer = class {
25354
25678
  httpServer = null;
25355
25679
  frameHandler = null;
25356
25680
  clients = /* @__PURE__ */ new Map();
25681
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
25682
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
25683
+ capabilityIdToClients = /* @__PURE__ */ new Map();
25357
25684
  logger;
25358
25685
  pingIntervalMs;
25359
25686
  async start() {
@@ -25444,6 +25771,49 @@ var LocalWsServer = class {
25444
25771
  this.safeSend(c.ws, frame);
25445
25772
  }
25446
25773
  }
25774
+ // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25775
+ // 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
25776
+ // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25777
+ broadcastToOwners(frame) {
25778
+ const gate = this.opts.authGate;
25779
+ for (const c of this.clients.values()) {
25780
+ if (gate && !gate.isAuthed(c.handle.id)) continue;
25781
+ if (c.ctx && c.ctx.principal.kind !== "owner") continue;
25782
+ this.safeSend(c.ws, frame);
25783
+ }
25784
+ }
25785
+ // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
25786
+ // - 所有 owner ws (owner 看自家所有 channel)
25787
+ // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
25788
+ // 一次调用同时推两端, inbox:event push 帧由此走.
25789
+ broadcastToCapabilityChannel(capabilityId, frame) {
25790
+ this.broadcastToOwners(frame);
25791
+ const set = this.capabilityIdToClients.get(capabilityId);
25792
+ if (!set) return;
25793
+ for (const id of set) {
25794
+ const c = this.clients.get(id);
25795
+ if (!c) continue;
25796
+ this.safeSend(c.ws, frame);
25797
+ }
25798
+ }
25799
+ // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
25800
+ // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
25801
+ // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
25802
+ // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
25803
+ // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
25804
+ broadcastToPrincipal(principalId, frame) {
25805
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
25806
+ this.broadcastToOwners(frame);
25807
+ return;
25808
+ }
25809
+ const set = this.capabilityIdToClients.get(principalId);
25810
+ if (!set) return;
25811
+ for (const id of set) {
25812
+ const c = this.clients.get(id);
25813
+ if (!c) continue;
25814
+ this.safeSend(c.ws, frame);
25815
+ }
25816
+ }
25447
25817
  firstSubscriber(sessionId) {
25448
25818
  for (const c of this.clients.values()) {
25449
25819
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -25465,6 +25835,40 @@ var LocalWsServer = class {
25465
25835
  if (!c) return;
25466
25836
  this.safeSend(c.ws, frame);
25467
25837
  }
25838
+ // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
25839
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
25840
+ attachClientContext(clientId, ctx) {
25841
+ const c = this.clients.get(clientId);
25842
+ if (!c) return;
25843
+ c.ctx = ctx;
25844
+ if (ctx.capabilityId) {
25845
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
25846
+ if (!set) {
25847
+ set = /* @__PURE__ */ new Set();
25848
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
25849
+ }
25850
+ set.add(clientId);
25851
+ }
25852
+ }
25853
+ // 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
25854
+ getClientContext(clientId) {
25855
+ return this.clients.get(clientId)?.ctx ?? null;
25856
+ }
25857
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
25858
+ // 让客户端识别 'capability revoked' 而非普通断连。
25859
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
25860
+ const set = this.capabilityIdToClients.get(capabilityId);
25861
+ if (!set) return;
25862
+ const ids = [...set];
25863
+ for (const id of ids) {
25864
+ const c = this.clients.get(id);
25865
+ if (!c) continue;
25866
+ try {
25867
+ c.ws.close(code, reason);
25868
+ } catch {
25869
+ }
25870
+ }
25871
+ }
25468
25872
  // URL path 路由:'/' → 主连接路径;其他 → close 4404
25469
25873
  routeConnection(socket, req) {
25470
25874
  const remoteAddress = req?.socket?.remoteAddress;
@@ -25499,7 +25903,7 @@ var LocalWsServer = class {
25499
25903
  } catch {
25500
25904
  }
25501
25905
  }, this.pingIntervalMs);
25502
- this.clients.set(id, { handle, ws: socket, pingTimer });
25906
+ this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
25503
25907
  this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
25504
25908
  const authGate = this.opts.authGate;
25505
25909
  let authed = true;
@@ -25523,6 +25927,12 @@ var LocalWsServer = class {
25523
25927
  socket.on("close", () => {
25524
25928
  const c = this.clients.get(id);
25525
25929
  if (c?.pingTimer) clearInterval(c.pingTimer);
25930
+ const capId = c?.ctx?.capabilityId;
25931
+ if (capId) {
25932
+ const set = this.capabilityIdToClients.get(capId);
25933
+ set?.delete(id);
25934
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
25935
+ }
25526
25936
  this.clients.delete(id);
25527
25937
  authGate?.unregister(id);
25528
25938
  this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
@@ -25555,6 +25965,17 @@ var LocalWsServer = class {
25555
25965
  return;
25556
25966
  }
25557
25967
  } else if (frame.type === "auth") {
25968
+ const token = typeof frame.token === "string" ? frame.token ?? "" : "";
25969
+ const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
25970
+ const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
25971
+ if (token && this.opts.tryVerifyCapabilityToken) {
25972
+ const guestCtx = this.opts.tryVerifyCapabilityToken(
25973
+ token,
25974
+ selfPrincipalId || void 0,
25975
+ selfDisplayName || void 0
25976
+ );
25977
+ if (guestCtx) this.attachClientContext(client.id, guestCtx);
25978
+ }
25558
25979
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
25559
25980
  return;
25560
25981
  }
@@ -25671,15 +26092,31 @@ var AuthGate = class {
25671
26092
  this.markFailed(handle.id);
25672
26093
  return frame.type === "auth" ? "consumed" : "reject";
25673
26094
  }
25674
- if (!this.opts.expectedToken) {
25675
- this.opts.closeConnection(handle, 1008, "auth not configured");
25676
- this.markFailed(handle.id);
25677
- return "consumed";
25678
- }
25679
- if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25680
- this.opts.closeConnection(handle, 1008, "auth failed");
25681
- this.markFailed(handle.id);
25682
- return "consumed";
26095
+ let ctx = null;
26096
+ if (this.opts.authenticate) {
26097
+ const r = this.opts.authenticate(
26098
+ parsed.data.token,
26099
+ parsed.data.selfPrincipalId,
26100
+ parsed.data.selfDisplayName
26101
+ );
26102
+ if (!r.ok) {
26103
+ this.opts.closeConnection(handle, 4401, r.code);
26104
+ this.markFailed(handle.id);
26105
+ return "consumed";
26106
+ }
26107
+ ctx = r.context;
26108
+ } else {
26109
+ if (!this.opts.expectedToken) {
26110
+ this.opts.closeConnection(handle, 1008, "auth not configured");
26111
+ this.markFailed(handle.id);
26112
+ return "consumed";
26113
+ }
26114
+ if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
26115
+ this.opts.closeConnection(handle, 1008, "auth failed");
26116
+ this.markFailed(handle.id);
26117
+ return "consumed";
26118
+ }
26119
+ ctx = this.opts.buildOwnerContext?.() ?? null;
25683
26120
  }
25684
26121
  if (st.timer != null) {
25685
26122
  const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -25687,6 +26124,7 @@ var AuthGate = class {
25687
26124
  }
25688
26125
  st.authed = true;
25689
26126
  st.timer = null;
26127
+ if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
25690
26128
  this.opts.sendOk(handle, { type: "auth:ok" });
25691
26129
  return "consumed";
25692
26130
  }
@@ -25722,7 +26160,7 @@ var AuthContextResolver = class {
25722
26160
  }
25723
26161
  opts;
25724
26162
  /**
25725
- * 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26163
+ * 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
25726
26164
  * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
25727
26165
  */
25728
26166
  resolveFromHeader(authHeader, remoteAddress) {
@@ -25732,15 +26170,6 @@ var AuthContextResolver = class {
25732
26170
  if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
25733
26171
  return { role: "owner", isLoopback };
25734
26172
  }
25735
- const personalHit = this.opts.personaRegistry.findByToken(token);
25736
- if (personalHit) {
25737
- return {
25738
- role: "personal",
25739
- personaId: personalHit.personaId,
25740
- label: personalHit.label,
25741
- isLoopback
25742
- };
25743
- }
25744
26173
  return null;
25745
26174
  }
25746
26175
  };
@@ -25762,206 +26191,753 @@ function constantTimeEqual2(a, b2) {
25762
26191
  return diff2 === 0;
25763
26192
  }
25764
26193
 
25765
- // src/transport/http-router.ts
25766
- var import_node_fs14 = __toESM(require("fs"), 1);
25767
- var import_node_path16 = __toESM(require("path"), 1);
26194
+ // ../protocol/src/index.ts
26195
+ init_runtime();
25768
26196
 
25769
- // src/attachment/group.ts
25770
- var import_node_fs13 = __toESM(require("fs"), 1);
25771
- var import_node_path14 = __toESM(require("path"), 1);
25772
- var import_node_crypto4 = __toESM(require("crypto"), 1);
25773
- init_protocol();
25774
- var GroupFileStore = class {
26197
+ // src/transport/connection-context.ts
26198
+ function ownerContext(ownerPrincipalId, displayName) {
26199
+ return {
26200
+ principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
26201
+ grants: [{ resource: { type: "*" }, actions: ["admin"] }]
26202
+ };
26203
+ }
26204
+ function guestContext(cap) {
26205
+ return {
26206
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
26207
+ grants: cap.grants,
26208
+ capabilityId: cap.id
26209
+ };
26210
+ }
26211
+ function authenticate(token, deps) {
26212
+ if (!token) return { ok: false, code: "NO_TOKEN" };
26213
+ if (deps.isOwnerToken(token)) {
26214
+ return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
26215
+ }
26216
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
26217
+ const v2 = deps.capabilityRegistry.verifyToken(token);
26218
+ if (!v2.ok) {
26219
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
26220
+ return { ok: false, code: v2.code };
26221
+ }
26222
+ if (deps.selfPrincipalId && deps.selfDisplayName && deps.onGuestHello) {
26223
+ try {
26224
+ deps.onGuestHello(v2.capability.id, deps.selfPrincipalId, deps.selfDisplayName);
26225
+ } catch (e) {
26226
+ if (e.message === "TOKEN_SHARED_LEAK") {
26227
+ return { ok: false, code: "TOKEN_REVOKED" };
26228
+ }
26229
+ throw e;
26230
+ }
26231
+ }
26232
+ return { ok: true, context: guestContext(v2.capability) };
26233
+ }
26234
+
26235
+ // src/permission/capability-store.ts
26236
+ var fs15 = __toESM(require("fs"), 1);
26237
+ var path18 = __toESM(require("path"), 1);
26238
+ var CAPABILITIES_FILE_NAME = "capabilities.json";
26239
+ var FILE_VERSION = 1;
26240
+ var CapabilityStore = class {
26241
+ constructor(dataDir) {
26242
+ this.dataDir = dataDir;
26243
+ fs15.mkdirSync(dataDir, { recursive: true });
26244
+ this.cache = this.readFromDisk();
26245
+ }
25775
26246
  dataDir;
25776
- logger;
25777
- cache = /* @__PURE__ */ new Map();
25778
- constructor(opts) {
25779
- this.dataDir = opts.dataDir;
25780
- this.logger = opts.logger;
26247
+ cache;
26248
+ list() {
26249
+ return [...this.cache];
25781
26250
  }
25782
- rootForScope(scope) {
25783
- return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26251
+ upsert(cap) {
26252
+ const idx = this.cache.findIndex((c) => c.id === cap.id);
26253
+ if (idx >= 0) {
26254
+ this.cache[idx] = cap;
26255
+ } else {
26256
+ this.cache.push(cap);
26257
+ }
26258
+ this.flush();
25784
26259
  }
25785
- /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
25786
- filePath(scope, sessionId) {
25787
- return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26260
+ remove(id) {
26261
+ const next = this.cache.filter((c) => c.id !== id);
26262
+ if (next.length === this.cache.length) return;
26263
+ this.cache = next;
26264
+ this.flush();
25788
26265
  }
25789
- cacheKey(scope, sessionId) {
25790
- return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26266
+ filePath() {
26267
+ return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
25791
26268
  }
25792
- /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
25793
- readFile(scope, sessionId) {
25794
- const file = this.filePath(scope, sessionId);
26269
+ readFromDisk() {
26270
+ const file = this.filePath();
26271
+ let raw;
25795
26272
  try {
25796
- const raw = import_node_fs13.default.readFileSync(file, "utf8");
25797
- const parsed = JSON.parse(raw);
25798
- if (!Array.isArray(parsed)) {
25799
- this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
25800
- file
25801
- });
25802
- return [];
25803
- }
25804
- const out = [];
25805
- for (const entry of parsed) {
25806
- const r = GroupFileEntrySchema.safeParse(entry);
25807
- if (r.success) out.push(r.data);
25808
- }
25809
- return out;
26273
+ raw = fs15.readFileSync(file, "utf8");
25810
26274
  } catch (err) {
25811
- const code = err?.code;
25812
- if (code === "ENOENT") return [];
25813
- this.logger?.warn("GroupFileStore.readFile failed", {
25814
- file,
25815
- err: err.message
25816
- });
26275
+ if (err?.code === "ENOENT") return [];
25817
26276
  return [];
25818
26277
  }
26278
+ if (!raw.trim()) return [];
26279
+ let parsed;
26280
+ try {
26281
+ parsed = JSON.parse(raw);
26282
+ } catch {
26283
+ return [];
26284
+ }
26285
+ if (!parsed || typeof parsed !== "object") return [];
26286
+ const arr = parsed.capabilities;
26287
+ if (!Array.isArray(arr)) return [];
26288
+ const out = [];
26289
+ for (const item of arr) {
26290
+ const r = CapabilitySchema.safeParse(item);
26291
+ if (r.success) out.push(r.data);
26292
+ }
26293
+ return out;
25819
26294
  }
25820
- writeFile(scope, sessionId, entries) {
25821
- const file = this.filePath(scope, sessionId);
25822
- import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
25823
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
25824
- import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
25825
- import_node_fs13.default.renameSync(tmp, file);
26295
+ flush() {
26296
+ const content = { version: FILE_VERSION, capabilities: this.cache };
26297
+ this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
25826
26298
  }
25827
- /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
25828
- list(scope, sessionId) {
25829
- const key = this.cacheKey(scope, sessionId);
25830
- const cached = this.cache.get(key);
25831
- if (cached) return cached.entries;
25832
- const entries = this.readFile(scope, sessionId);
25833
- this.cache.set(key, { entries, scope });
25834
- return entries;
26299
+ atomicWrite(file, content) {
26300
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26301
+ fs15.writeFileSync(tmp, content, { mode: 384 });
26302
+ fs15.renameSync(tmp, file);
26303
+ try {
26304
+ fs15.chmodSync(file, 384);
26305
+ } catch {
26306
+ }
26307
+ }
26308
+ };
26309
+
26310
+ // src/permission/capability-registry.ts
26311
+ var crypto4 = __toESM(require("crypto"), 1);
26312
+ var CapabilityRegistry = class {
26313
+ constructor(store, now = () => Date.now()) {
26314
+ this.store = store;
26315
+ this.now = now;
26316
+ for (const cap of store.list()) {
26317
+ this.bySecretHash.set(cap.secretHash, cap);
26318
+ }
26319
+ }
26320
+ store;
26321
+ now;
26322
+ // sha256(token) → Capability
26323
+ bySecretHash = /* @__PURE__ */ new Map();
26324
+ list() {
26325
+ return this.store.list();
26326
+ }
26327
+ verifyToken(token) {
26328
+ if (!token) return { ok: false, code: "TOKEN_INVALID" };
26329
+ const hash = sha256Hex(token);
26330
+ const cap = this.bySecretHash.get(hash);
26331
+ if (!cap) return { ok: false, code: "TOKEN_INVALID" };
26332
+ if (cap.expiresAt && this.now() >= cap.expiresAt) {
26333
+ return { ok: false, code: "TOKEN_EXPIRED" };
26334
+ }
26335
+ if (cap.maxUses && cap.usedCount >= cap.maxUses) {
26336
+ return { ok: false, code: "TOKEN_EXHAUSTED" };
26337
+ }
26338
+ const updated = { ...cap, usedCount: cap.usedCount + 1 };
26339
+ this.bySecretHash.set(hash, updated);
26340
+ this.store.upsert(updated);
26341
+ return { ok: true, capability: updated };
26342
+ }
26343
+ upsertCapability(cap) {
26344
+ this.bySecretHash.set(cap.secretHash, cap);
26345
+ this.store.upsert(cap);
25835
26346
  }
25836
26347
  /**
25837
- * upsert:
25838
- * - relPath 已存在 更新 lastEditedAt + clear stale;不动 from / addedAt / id
25839
- * (保留首次入群人 / 入群时刻)
25840
- * - 不存在 → append,id 派生稳定 uuid,addedAt = now
25841
- *
25842
- * 返回最新 entry(caller 可用来 broadcast 通知)。
26348
+ * Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
26349
+ * 后续 verifyToken token 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
26350
+ * idempotent:不存在 → null。
25843
26351
  */
25844
- upsert(scope, sessionId, input, now = Date.now()) {
25845
- const entries = this.list(scope, sessionId).slice();
25846
- const idx = entries.findIndex((e) => e.relPath === input.relPath);
25847
- let next;
25848
- if (idx >= 0) {
25849
- const prev = entries[idx];
25850
- next = {
25851
- ...prev,
25852
- size: input.size,
25853
- mime: input.mime,
25854
- lastEditedAt: now,
25855
- stale: false
25856
- // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
25857
- };
25858
- entries[idx] = next;
25859
- } else {
25860
- next = {
25861
- id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
25862
- relPath: input.relPath,
25863
- from: input.from,
25864
- label: input.label,
25865
- size: input.size,
25866
- mime: input.mime,
25867
- addedAt: now
25868
- // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
25869
- };
25870
- entries.push(next);
26352
+ delete(id) {
26353
+ const current = this.store.list().find((c) => c.id === id);
26354
+ if (!current) return null;
26355
+ this.bySecretHash.delete(current.secretHash);
26356
+ this.store.remove(id);
26357
+ return current;
26358
+ }
26359
+ findById(id) {
26360
+ return this.store.list().find((c) => c.id === id) ?? null;
26361
+ }
26362
+ };
26363
+ function sha256Hex(s) {
26364
+ return crypto4.createHash("sha256").update(s).digest("hex");
26365
+ }
26366
+
26367
+ // src/permission/capability-manager.ts
26368
+ var crypto5 = __toESM(require("crypto"), 1);
26369
+ var CapabilityManager = class {
26370
+ constructor(registry2, hooks = {}) {
26371
+ this.registry = registry2;
26372
+ this.hooks = hooks;
26373
+ }
26374
+ registry;
26375
+ hooks;
26376
+ list() {
26377
+ return this.registry.list();
26378
+ }
26379
+ // whoami handler 用:根据 capabilityId 反查 caller 的 capability 实体。
26380
+ // 返回 Capability 持久化形态;handler 调 stripSecretHash 后再上 wire。
26381
+ findById(id) {
26382
+ return this.registry.findById(id);
26383
+ }
26384
+ issue(args) {
26385
+ if (!args.displayName.trim()) {
26386
+ throw new Error("CapabilityManager.issue: displayName must be non-empty");
25871
26387
  }
25872
- this.writeFile(scope, sessionId, entries);
25873
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25874
- return next;
26388
+ const token = (this.hooks.generateToken ?? defaultGenerateToken)();
26389
+ const id = (this.hooks.generateId ?? defaultGenerateId)();
26390
+ const now = (this.hooks.now ?? Date.now)();
26391
+ const cap = {
26392
+ id,
26393
+ secretHash: sha256Hex2(token),
26394
+ displayName: args.displayName,
26395
+ grants: args.grants,
26396
+ issuedAt: now,
26397
+ usedCount: 0,
26398
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
26399
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
26400
+ };
26401
+ this.registry.upsertCapability(cap);
26402
+ this.hooks.onIssued?.(cap, token);
26403
+ return { token, capability: cap };
25875
26404
  }
25876
26405
  /**
25877
- * 标记一个 relPath stale(agent rm / mv 后文件不在)。
25878
- * - 命中 stale=true,UI 灰显
25879
- * - 未命中 noop(不需要为不在群里的文件创建 stale 条目)
25880
- *
25881
- * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26406
+ * guest auth 帧捎带 selfPrincipalId/selfDisplayName 时,AuthGate.authenticate
26407
+ * 校验 cap token 通过后调本方法把 peer 字段写回 cap:
26408
+ * - 首次(cap.firstUsedByPeerAt 未设)→ 写盘 + 触发 onIssued 复用 push 频道
26409
+ * 让 owner UI 通过 capability:updated 实时拿到 peer 字段
26410
+ * - 重复(同 peerOwnerId)→ noop
26411
+ * - 冲突(不同 peerOwnerId)→ throw TOKEN_SHARED_LEAK,AuthGate 转 close 4401
25882
26412
  */
25883
- markStale(scope, sessionId, relPath) {
25884
- const entries = this.list(scope, sessionId).slice();
25885
- const idx = entries.findIndex((e) => e.relPath === relPath);
25886
- if (idx < 0) return;
25887
- if (entries[idx].stale) return;
25888
- entries[idx] = { ...entries[idx], stale: true };
25889
- this.writeFile(scope, sessionId, entries);
25890
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26413
+ recordPeerHello(capId, args) {
26414
+ const cap = this.registry.findById(capId);
26415
+ if (!cap) return { changed: false };
26416
+ if (cap.firstUsedByPeerAt != null) {
26417
+ if (cap.peerOwnerId !== args.ownerPrincipalId) {
26418
+ throw new Error("TOKEN_SHARED_LEAK");
26419
+ }
26420
+ return { changed: false };
26421
+ }
26422
+ const updated = {
26423
+ ...cap,
26424
+ peerOwnerId: args.ownerPrincipalId,
26425
+ firstUsedByPeerAt: args.now,
26426
+ peerDisplayName: args.displayName
26427
+ };
26428
+ this.registry.upsertCapability(updated);
26429
+ this.hooks.onIssued?.(updated, "__peer_hello__");
26430
+ return { changed: true };
25891
26431
  }
25892
26432
  /**
25893
- * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
25894
- * agent 自动入群的不应走这条 —— markStale 表达"文件不在了"语义;
25895
- * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
25896
- *
25897
- * 返回值:true=命中并删除;false=relPath 不在群里。
26433
+ * Hard delete:从 registry / store 物理移除 capability。
26434
+ * idempotent:不存在 null(handler 翻译成 VALIDATION_ERROR)
26435
+ * onDeleted hook daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
25898
26436
  */
25899
- remove(scope, sessionId, relPath) {
25900
- const entries = this.list(scope, sessionId).slice();
25901
- const idx = entries.findIndex((e) => e.relPath === relPath);
25902
- if (idx < 0) return false;
25903
- entries.splice(idx, 1);
25904
- this.writeFile(scope, sessionId, entries);
25905
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25906
- return true;
26437
+ delete(id) {
26438
+ const removed = this.registry.delete(id);
26439
+ if (!removed) return null;
26440
+ this.hooks.onDeleted?.(removed);
26441
+ return { capability: removed };
25907
26442
  }
26443
+ };
26444
+ function defaultGenerateToken() {
26445
+ return crypto5.randomBytes(24).toString("base64url");
26446
+ }
26447
+ function defaultGenerateId() {
26448
+ return "cap_" + crypto5.randomBytes(6).toString("base64url");
26449
+ }
26450
+ function sha256Hex2(s) {
26451
+ return crypto5.createHash("sha256").update(s).digest("hex");
26452
+ }
26453
+
26454
+ // src/permission/cleanup.ts
26455
+ var fs16 = __toESM(require("fs"), 1);
26456
+ function cleanupGuestSessionsForCapability(cap, factory) {
26457
+ const removed = [];
26458
+ for (const g2 of cap.grants) {
26459
+ if (g2.resource.type !== "persona") continue;
26460
+ const dir = factory.vmGuestRoot(g2.resource.id, cap.id);
26461
+ try {
26462
+ fs16.rmSync(dir, { recursive: true, force: true });
26463
+ removed.push(dir);
26464
+ } catch {
26465
+ }
26466
+ }
26467
+ return { removed };
26468
+ }
26469
+
26470
+ // src/inbox/inbox-store.ts
26471
+ var fs17 = __toESM(require("fs"), 1);
26472
+ var path19 = __toESM(require("path"), 1);
26473
+ var INBOX_SUBDIR = "inbox";
26474
+ var InboxStore = class {
26475
+ constructor(dataDir) {
26476
+ this.dataDir = dataDir;
26477
+ fs17.mkdirSync(this.dirPath(), { recursive: true });
26478
+ }
26479
+ dataDir;
25908
26480
  /**
25909
- * session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
25910
- *
25911
- * <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
25912
- * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
26481
+ * 列出某 peer 的消息, createdAt 升序.
26482
+ * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
26483
+ * 文件不存在时返回 [] (不抛, 不创建).
26484
+ */
26485
+ list(peerOwnerId, sinceCreatedAt) {
26486
+ const file = this.filePath(peerOwnerId);
26487
+ let raw;
26488
+ try {
26489
+ raw = fs17.readFileSync(file, "utf8");
26490
+ } catch (err) {
26491
+ if (err?.code === "ENOENT") return [];
26492
+ return [];
26493
+ }
26494
+ const all = parseAllLines(raw);
26495
+ if (sinceCreatedAt === void 0) return all;
26496
+ return all.filter((m2) => m2.createdAt > sinceCreatedAt);
26497
+ }
26498
+ /**
26499
+ * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
26500
+ */
26501
+ listAllPeerOwnerIds() {
26502
+ const dir = this.dirPath();
26503
+ let entries;
26504
+ try {
26505
+ entries = fs17.readdirSync(dir);
26506
+ } catch (err) {
26507
+ if (err?.code === "ENOENT") return [];
26508
+ return [];
26509
+ }
26510
+ return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
26511
+ }
26512
+ /**
26513
+ * 追加一条消息,按 message.id 幂等去重。
26514
+ * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
26515
+ */
26516
+ append(message) {
26517
+ const existing = this.list(message.peerOwnerId);
26518
+ if (existing.some((m2) => m2.id === message.id)) return;
26519
+ const file = this.filePath(message.peerOwnerId);
26520
+ const line = JSON.stringify(message) + "\n";
26521
+ fs17.appendFileSync(file, line, { mode: 384 });
26522
+ try {
26523
+ fs17.chmodSync(file, 384);
26524
+ } catch {
26525
+ }
26526
+ }
26527
+ /**
26528
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
26529
+ * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
26530
+ * 返写入了几条.
26531
+ */
26532
+ markRead(peerOwnerId, principalId, upToCreatedAt) {
26533
+ const messages = this.list(peerOwnerId);
26534
+ let changed = 0;
26535
+ const next = messages.map((m2) => {
26536
+ if (m2.senderPrincipalId === principalId) return m2;
26537
+ if (m2.createdAt > upToCreatedAt) return m2;
26538
+ const cur = m2.readBy[principalId];
26539
+ if (cur !== void 0 && cur >= upToCreatedAt) return m2;
26540
+ changed++;
26541
+ return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
26542
+ });
26543
+ if (changed === 0) return 0;
26544
+ this.rewriteFile(peerOwnerId, next);
26545
+ return changed;
26546
+ }
26547
+ /**
26548
+ * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
26549
+ */
26550
+ removeByPeerOwnerId(peerOwnerId) {
26551
+ const file = this.filePath(peerOwnerId);
26552
+ try {
26553
+ fs17.unlinkSync(file);
26554
+ } catch (err) {
26555
+ if (err?.code === "ENOENT") return;
26556
+ }
26557
+ }
26558
+ rewriteFile(peerOwnerId, messages) {
26559
+ const file = this.filePath(peerOwnerId);
26560
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26561
+ const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
26562
+ fs17.writeFileSync(tmp, content, { mode: 384 });
26563
+ fs17.renameSync(tmp, file);
26564
+ try {
26565
+ fs17.chmodSync(file, 384);
26566
+ } catch {
26567
+ }
26568
+ }
26569
+ dirPath() {
26570
+ return path19.join(this.dataDir, INBOX_SUBDIR);
26571
+ }
26572
+ filePath(peerOwnerId) {
26573
+ return path19.join(this.dirPath(), `${peerOwnerId}.jsonl`);
26574
+ }
26575
+ };
26576
+ function parseAllLines(raw) {
26577
+ const out = [];
26578
+ for (const line of raw.split("\n")) {
26579
+ const trimmed = line.trim();
26580
+ if (!trimmed) continue;
26581
+ let parsed;
26582
+ try {
26583
+ parsed = JSON.parse(trimmed);
26584
+ } catch {
26585
+ continue;
26586
+ }
26587
+ const r = InboxMessageSchema.safeParse(parsed);
26588
+ if (r.success) out.push(r.data);
26589
+ }
26590
+ return out;
26591
+ }
26592
+
26593
+ // src/inbox/inbox-manager.ts
26594
+ var InboxManager = class {
26595
+ constructor(store, broadcast) {
26596
+ this.store = store;
26597
+ this.broadcast = broadcast;
26598
+ }
26599
+ store;
26600
+ broadcast;
26601
+ /**
26602
+ * 写一条消息到 peer.jsonl + broadcast 给本机所有 owner ws。
26603
+ * 按 id 幂等:同 id 已落盘则 no-op(不广播、不重写)。
25913
26604
  *
25914
- * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26605
+ * caller 必须已通过 handler 鉴权
26606
+ * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
25915
26607
  */
25916
- listByPersona(personaId) {
25917
- const out = [];
25918
- for (const mode of ["owner", "listener"]) {
25919
- const scope = { kind: "persona", personaId, mode };
25920
- const root = this.rootForScope(scope);
25921
- let names;
26608
+ postMessage(args) {
26609
+ const beforeLen = this.store.list(args.peerOwnerId).length;
26610
+ const msg = {
26611
+ id: args.id,
26612
+ peerOwnerId: args.peerOwnerId,
26613
+ senderPrincipalId: args.senderPrincipalId,
26614
+ text: args.text,
26615
+ createdAt: args.createdAt,
26616
+ readBy: {}
26617
+ };
26618
+ this.store.append(msg);
26619
+ const afterLen = this.store.list(args.peerOwnerId).length;
26620
+ if (afterLen === beforeLen) return msg;
26621
+ this.broadcast(args.peerOwnerId, {
26622
+ type: "inbox:event",
26623
+ peerOwnerId: args.peerOwnerId,
26624
+ message: msg
26625
+ });
26626
+ return msg;
26627
+ }
26628
+ list(peerOwnerId, sinceCreatedAt) {
26629
+ return this.store.list(peerOwnerId, sinceCreatedAt);
26630
+ }
26631
+ /**
26632
+ * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
26633
+ * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
26634
+ */
26635
+ markRead(args) {
26636
+ const before = this.store.list(args.peerOwnerId);
26637
+ const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
26638
+ if (changed === 0) return 0;
26639
+ const after = this.store.list(args.peerOwnerId);
26640
+ const beforeById = new Map(before.map((m2) => [m2.id, m2]));
26641
+ for (const m2 of after) {
26642
+ const prev = beforeById.get(m2.id);
26643
+ if (!prev) continue;
26644
+ if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
26645
+ this.broadcast(args.peerOwnerId, {
26646
+ type: "inbox:event",
26647
+ peerOwnerId: args.peerOwnerId,
26648
+ message: m2
26649
+ });
26650
+ }
26651
+ return changed;
26652
+ }
26653
+ /** peer:remove cascade. 删整个 peer 的 jsonl. */
26654
+ removeChannel(peerOwnerId) {
26655
+ this.store.removeByPeerOwnerId(peerOwnerId);
26656
+ }
26657
+ };
26658
+
26659
+ // src/state/received-capability-store.ts
26660
+ var fs18 = __toESM(require("fs"), 1);
26661
+ var path20 = __toESM(require("path"), 1);
26662
+ var FILE_NAME = "received-capabilities.json";
26663
+ var ReceivedCapabilityStore = class {
26664
+ constructor(dataDir) {
26665
+ this.dataDir = dataDir;
26666
+ }
26667
+ dataDir;
26668
+ caps = /* @__PURE__ */ new Map();
26669
+ load() {
26670
+ this.caps.clear();
26671
+ const file = path20.join(this.dataDir, FILE_NAME);
26672
+ let raw;
26673
+ try {
26674
+ raw = fs18.readFileSync(file, "utf8");
26675
+ } catch (err) {
26676
+ if (err?.code !== "ENOENT") this.renameBak(file);
26677
+ return;
26678
+ }
26679
+ let parsed;
26680
+ try {
26681
+ parsed = JSON.parse(raw);
26682
+ } catch {
26683
+ this.renameBak(file);
26684
+ return;
26685
+ }
26686
+ const arr = parsed?.receivedCaps;
26687
+ if (!Array.isArray(arr)) return;
26688
+ for (const entry of arr) {
26689
+ const r = ReceivedCapabilitySchema.safeParse(entry);
26690
+ if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
26691
+ }
26692
+ }
26693
+ list() {
26694
+ return Array.from(this.caps.values());
26695
+ }
26696
+ get(ownerPrincipalId) {
26697
+ return this.caps.get(ownerPrincipalId) ?? null;
26698
+ }
26699
+ hasOwner(ownerPrincipalId) {
26700
+ return this.caps.has(ownerPrincipalId);
26701
+ }
26702
+ upsert(cap) {
26703
+ this.caps.set(cap.ownerPrincipalId, cap);
26704
+ this.flush();
26705
+ }
26706
+ remove(ownerPrincipalId) {
26707
+ if (!this.caps.delete(ownerPrincipalId)) return;
26708
+ this.flush();
26709
+ }
26710
+ flush() {
26711
+ const file = path20.join(this.dataDir, FILE_NAME);
26712
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
26713
+ const content = JSON.stringify(
26714
+ { receivedCaps: Array.from(this.caps.values()) },
26715
+ null,
26716
+ 2
26717
+ );
26718
+ fs18.mkdirSync(this.dataDir, { recursive: true });
26719
+ fs18.writeFileSync(tmp, content, { mode: 384 });
26720
+ fs18.renameSync(tmp, file);
26721
+ }
26722
+ renameBak(file) {
26723
+ try {
26724
+ fs18.renameSync(file, `${file}.bak`);
26725
+ } catch {
26726
+ }
26727
+ }
26728
+ };
26729
+
26730
+ // src/state/legacy-migration.ts
26731
+ var fs19 = __toESM(require("fs"), 1);
26732
+ var path21 = __toESM(require("path"), 1);
26733
+ var TARGETS = [
26734
+ { src: "capabilities.json", bak: "capabilities.legacy.bak" },
26735
+ { src: "remote-personas", bak: "remote-personas.legacy.bak" },
26736
+ { src: "peers.json", bak: "peers.legacy.bak" }
26737
+ ];
26738
+ function migrateLegacyFiles(dataDir) {
26739
+ for (const t of TARGETS) {
26740
+ const srcPath = path21.join(dataDir, t.src);
26741
+ const bakPath = path21.join(dataDir, t.bak);
26742
+ if (!fs19.existsSync(srcPath)) continue;
26743
+ if (fs19.existsSync(bakPath)) continue;
26744
+ try {
26745
+ fs19.renameSync(srcPath, bakPath);
26746
+ } catch {
26747
+ }
26748
+ }
26749
+ }
26750
+
26751
+ // src/received-capability/connect-remote.ts
26752
+ var crypto6 = __toESM(require("crypto"), 1);
26753
+ var HANDSHAKE_TIMEOUT_MS = 5e3;
26754
+ var RPC_TIMEOUT_MS = 5e3;
26755
+ async function connectRemote(args) {
26756
+ const ws = new wrapper_default(args.url);
26757
+ await new Promise((resolve2, reject) => {
26758
+ const onError = (e) => reject(e);
26759
+ ws.once("open", () => {
26760
+ ws.off("error", onError);
26761
+ resolve2();
26762
+ });
26763
+ ws.once("error", onError);
26764
+ });
26765
+ ws.send(
26766
+ JSON.stringify({
26767
+ type: "auth",
26768
+ token: args.token,
26769
+ selfPrincipalId: args.selfPrincipalId,
26770
+ selfDisplayName: args.selfDisplayName
26771
+ })
26772
+ );
26773
+ await new Promise((resolve2, reject) => {
26774
+ const onMessage = (raw) => {
26775
+ let f;
25922
26776
  try {
25923
- names = import_node_fs13.default.readdirSync(root);
25924
- } catch (err) {
25925
- const code = err?.code;
25926
- if (code === "ENOENT") continue;
25927
- continue;
26777
+ f = JSON.parse(raw.toString());
26778
+ } catch {
26779
+ return;
26780
+ }
26781
+ if (f.type === "auth:ok" || f.type === "ready") {
26782
+ ws.off("message", onMessage);
26783
+ resolve2();
26784
+ return;
25928
26785
  }
25929
- for (const name of names) {
25930
- if (!name.endsWith(".group-files.json")) continue;
25931
- const sessionId = name.slice(0, -".group-files.json".length);
25932
- if (!sessionId) continue;
25933
- const entries = this.list(scope, sessionId);
25934
- out.push({ sessionId, entries });
26786
+ if (f.type === "auth:error" || f.code === 4401) {
26787
+ ws.off("message", onMessage);
26788
+ reject(new Error(`auth failed: ${f.message ?? f.type ?? "unknown"}`));
26789
+ }
26790
+ };
26791
+ ws.on("message", onMessage);
26792
+ setTimeout(() => {
26793
+ ws.off("message", onMessage);
26794
+ reject(new Error("auth handshake timeout"));
26795
+ }, HANDSHAKE_TIMEOUT_MS);
26796
+ });
26797
+ function call(method, payload) {
26798
+ const requestId = crypto6.randomUUID();
26799
+ return new Promise((resolve2, reject) => {
26800
+ const onMessage = (raw) => {
26801
+ let f;
26802
+ try {
26803
+ f = JSON.parse(raw.toString());
26804
+ } catch {
26805
+ return;
26806
+ }
26807
+ if (f.requestId !== requestId) return;
26808
+ ws.off("message", onMessage);
26809
+ if (f.error || f.type?.endsWith(":error")) {
26810
+ reject(new Error(f.message ?? f.error ?? "rpc error"));
26811
+ } else {
26812
+ resolve2(f);
26813
+ }
26814
+ };
26815
+ ws.on("message", onMessage);
26816
+ ws.send(JSON.stringify({ type: method, requestId, ...payload }));
26817
+ setTimeout(() => {
26818
+ ws.off("message", onMessage);
26819
+ reject(new Error(`${method} timeout`));
26820
+ }, RPC_TIMEOUT_MS);
26821
+ });
26822
+ }
26823
+ return {
26824
+ whoami: async () => {
26825
+ const f = await call("whoami", {});
26826
+ return {
26827
+ ownerId: f.owner.id,
26828
+ displayName: f.owner.displayName,
26829
+ capabilityId: f.capability.id,
26830
+ grants: f.capability.grants
26831
+ };
26832
+ },
26833
+ close: () => {
26834
+ try {
26835
+ ws.close();
26836
+ } catch {
25935
26837
  }
25936
26838
  }
25937
- return out;
26839
+ };
26840
+ }
26841
+
26842
+ // src/migrations/2026-05-20-flatten-sessions.ts
26843
+ var fs20 = __toESM(require("fs"), 1);
26844
+ var path22 = __toESM(require("path"), 1);
26845
+ var MIGRATION_FLAG_NAME = ".migration.v1.done";
26846
+ function migrateFlattenSessions(opts) {
26847
+ const dataDir = opts.dataDir;
26848
+ const now = opts.now ?? Date.now;
26849
+ const sessionsDir = path22.join(dataDir, "sessions");
26850
+ const flagPath = path22.join(sessionsDir, MIGRATION_FLAG_NAME);
26851
+ if (existsSync4(flagPath)) {
26852
+ return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26853
+ }
26854
+ let movedBare = 0;
26855
+ let movedVmOwner = 0;
26856
+ let archivedListener = 0;
26857
+ const defaultDir = path22.join(sessionsDir, "default");
26858
+ if (existsSync4(defaultDir)) {
26859
+ for (const entry of readdirSafe(defaultDir)) {
26860
+ if (!entry.endsWith(".json")) continue;
26861
+ const src = path22.join(defaultDir, entry);
26862
+ const dst = path22.join(sessionsDir, entry);
26863
+ fs20.renameSync(src, dst);
26864
+ movedBare += 1;
26865
+ }
26866
+ rmdirIfEmpty(defaultDir);
26867
+ }
26868
+ for (const pid of readdirSafe(sessionsDir)) {
26869
+ const personaDir = path22.join(sessionsDir, pid);
26870
+ if (!isDir(personaDir)) continue;
26871
+ if (pid === "default") continue;
26872
+ const ownerSrc = path22.join(personaDir, "owner");
26873
+ if (existsSync4(ownerSrc) && isDir(ownerSrc)) {
26874
+ const ownerDst = path22.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26875
+ fs20.mkdirSync(ownerDst, { recursive: true });
26876
+ for (const file of readdirSafe(ownerSrc)) {
26877
+ if (!file.endsWith(".json")) continue;
26878
+ fs20.renameSync(path22.join(ownerSrc, file), path22.join(ownerDst, file));
26879
+ movedVmOwner += 1;
26880
+ }
26881
+ rmdirIfEmpty(ownerSrc);
26882
+ }
26883
+ const listenerSrc = path22.join(personaDir, "listener");
26884
+ if (existsSync4(listenerSrc) && isDir(listenerSrc)) {
26885
+ const archiveDst = path22.join(dataDir, ".legacy", `listener-${pid}`);
26886
+ fs20.mkdirSync(archiveDst, { recursive: true });
26887
+ for (const file of readdirSafe(listenerSrc)) {
26888
+ if (!file.endsWith(".json")) continue;
26889
+ fs20.renameSync(path22.join(listenerSrc, file), path22.join(archiveDst, file));
26890
+ archivedListener += 1;
26891
+ }
26892
+ rmdirIfEmpty(listenerSrc);
26893
+ }
26894
+ rmdirIfEmpty(personaDir);
26895
+ }
26896
+ fs20.mkdirSync(sessionsDir, { recursive: true });
26897
+ fs20.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26898
+ return {
26899
+ skipped: false,
26900
+ flagWritten: true,
26901
+ movedBare,
26902
+ movedVmOwner,
26903
+ archivedListener
26904
+ };
26905
+ }
26906
+ function existsSync4(p2) {
26907
+ try {
26908
+ fs20.statSync(p2);
26909
+ return true;
26910
+ } catch {
26911
+ return false;
25938
26912
  }
25939
- };
25940
- function personalViewable(groupStore, personaDir, personaId, absPath) {
25941
- const realTarget = safeRealpath(absPath);
25942
- if (!realTarget) {
26913
+ }
26914
+ function isDir(p2) {
26915
+ try {
26916
+ return fs20.statSync(p2).isDirectory();
26917
+ } catch {
25943
26918
  return false;
25944
26919
  }
25945
- const personasUnion = groupStore.listByPersona(personaId);
25946
- for (const { entries } of personasUnion) {
25947
- for (const e of entries) {
25948
- if (e.stale) continue;
25949
- const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
25950
- if (realEntry && realEntry === realTarget) return true;
25951
- }
26920
+ }
26921
+ function readdirSafe(p2) {
26922
+ try {
26923
+ return fs20.readdirSync(p2);
26924
+ } catch {
26925
+ return [];
25952
26926
  }
25953
- return false;
25954
26927
  }
25955
- function safeRealpath(p2) {
26928
+ function rmdirIfEmpty(p2) {
25956
26929
  try {
25957
- return import_node_fs13.default.realpathSync(p2);
26930
+ fs20.rmdirSync(p2);
25958
26931
  } catch {
25959
- return null;
25960
26932
  }
25961
26933
  }
25962
26934
 
25963
- // src/attachment/mime.ts
26935
+ // src/transport/http-router.ts
26936
+ var import_node_fs13 = __toESM(require("fs"), 1);
25964
26937
  var import_node_path15 = __toESM(require("path"), 1);
26938
+
26939
+ // src/attachment/mime.ts
26940
+ var import_node_path14 = __toESM(require("path"), 1);
25965
26941
  var TEXT_PLAIN = "text/plain; charset=utf-8";
25966
26942
  var EXT_TO_NATIVE_MIME = {
25967
26943
  // 图片
@@ -26068,14 +27044,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
26068
27044
  ".mk"
26069
27045
  ]);
26070
27046
  function lookupMime(filePathOrName) {
26071
- const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
27047
+ const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
26072
27048
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
26073
27049
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
26074
27050
  return "application/octet-stream";
26075
27051
  }
26076
27052
 
26077
27053
  // src/attachment/sign-url.ts
26078
- var import_node_crypto5 = __toESM(require("crypto"), 1);
27054
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
26079
27055
  var HMAC_ALGO = "sha256";
26080
27056
  function base64urlEncode(buf) {
26081
27057
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -26092,7 +27068,7 @@ function decodeAbsPathFromUrl(encoded) {
26092
27068
  }
26093
27069
  function computeSig(secret, absPath, e) {
26094
27070
  const msg = e === null ? absPath : `${absPath}|${e}`;
26095
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
27071
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26096
27072
  }
26097
27073
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
26098
27074
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -26127,7 +27103,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
26127
27103
  if (provided.length !== expected.length) {
26128
27104
  return { ok: false, code: "BAD_SIG" };
26129
27105
  }
26130
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
27106
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
26131
27107
  return { ok: false, code: "BAD_SIG" };
26132
27108
  }
26133
27109
  if (e !== null && now() / 1e3 > e) {
@@ -26148,7 +27124,7 @@ function createHttpRouter(deps) {
26148
27124
  sendJson(res, 200, { ok: true, version: deps.daemonVersion });
26149
27125
  return true;
26150
27126
  }
26151
- if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
27127
+ if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26152
27128
  return false;
26153
27129
  }
26154
27130
  if (url.pathname.startsWith("/files/") && req.method === "GET") {
@@ -26188,43 +27164,8 @@ function createHttpRouter(deps) {
26188
27164
  sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
26189
27165
  return true;
26190
27166
  }
26191
- const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
26192
- if (personaFilesMatch && req.method === "GET") {
26193
- const pid = personaFilesMatch[1];
26194
- const pathParam = url.searchParams.get("path");
26195
- if (!pathParam) {
26196
- sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26197
- return true;
26198
- }
26199
- if (!deps.personaStore || !deps.groupFileStore) {
26200
- sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
26201
- return true;
26202
- }
26203
- const personaDir = deps.personaStore.personaDirPath(pid);
26204
- const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
26205
- if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
26206
- sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
26207
- return true;
26208
- }
26209
- if (ctx.role === "personal") {
26210
- if (ctx.personaId !== pid) {
26211
- sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
26212
- return true;
26213
- }
26214
- if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
26215
- sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
26216
- return true;
26217
- }
26218
- }
26219
- streamFile(res, absPath, deps.logger);
26220
- return true;
26221
- }
26222
27167
  const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
26223
27168
  if (sessionFilesMatch && req.method === "GET") {
26224
- if (ctx.role !== "owner") {
26225
- sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
26226
- return true;
26227
- }
26228
27169
  const sid = sessionFilesMatch[1];
26229
27170
  const pathParam = url.searchParams.get("path");
26230
27171
  if (!pathParam) {
@@ -26232,7 +27173,7 @@ function createHttpRouter(deps) {
26232
27173
  return true;
26233
27174
  }
26234
27175
  let absPath;
26235
- if (import_node_path16.default.isAbsolute(pathParam)) {
27176
+ if (import_node_path15.default.isAbsolute(pathParam)) {
26236
27177
  absPath = pathParam;
26237
27178
  } else if (deps.sessionStore) {
26238
27179
  const file = deps.sessionStore.read(sid);
@@ -26240,7 +27181,7 @@ function createHttpRouter(deps) {
26240
27181
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
26241
27182
  return true;
26242
27183
  }
26243
- absPath = import_node_path16.default.join(file.cwd, pathParam);
27184
+ absPath = import_node_path15.default.join(file.cwd, pathParam);
26244
27185
  } else {
26245
27186
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
26246
27187
  return true;
@@ -26265,18 +27206,12 @@ function sendJson(res, status, body) {
26265
27206
  res.end(JSON.stringify(body));
26266
27207
  }
26267
27208
  function withCtx(ctx, body) {
26268
- return { ...body, role: ctx.role, personaId: ctx.personaId };
26269
- }
26270
- function isContainedIn(abs, root) {
26271
- const normalized = import_node_path16.default.resolve(abs);
26272
- const normalizedRoot = import_node_path16.default.resolve(root);
26273
- if (normalized === normalizedRoot) return true;
26274
- return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
27209
+ return { ...body, role: ctx.role };
26275
27210
  }
26276
27211
  function streamFile(res, absPath, logger) {
26277
27212
  let stat;
26278
27213
  try {
26279
- stat = import_node_fs14.default.statSync(absPath);
27214
+ stat = import_node_fs13.default.statSync(absPath);
26280
27215
  } catch (err) {
26281
27216
  const code = err?.code;
26282
27217
  if (code === "ENOENT") {
@@ -26286,26 +27221,167 @@ function streamFile(res, absPath, logger) {
26286
27221
  }
26287
27222
  return;
26288
27223
  }
26289
- if (!stat.isFile()) {
26290
- sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
26291
- return;
27224
+ if (!stat.isFile()) {
27225
+ sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
27226
+ return;
27227
+ }
27228
+ const mime = lookupMime(absPath);
27229
+ const basename = import_node_path15.default.basename(absPath);
27230
+ res.writeHead(200, {
27231
+ "Content-Type": mime,
27232
+ "Content-Length": String(stat.size),
27233
+ "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
27234
+ // 防止浏览器把任意 mime 当 html 渲染
27235
+ "X-Content-Type-Options": "nosniff"
27236
+ });
27237
+ const stream = import_node_fs13.default.createReadStream(absPath);
27238
+ stream.on("error", (err) => {
27239
+ logger?.warn("streamFile read error", { absPath, err: err.message });
27240
+ res.destroy();
27241
+ });
27242
+ stream.pipe(res);
27243
+ }
27244
+
27245
+ // src/attachment/group.ts
27246
+ var import_node_fs14 = __toESM(require("fs"), 1);
27247
+ var import_node_path16 = __toESM(require("path"), 1);
27248
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
27249
+ init_protocol();
27250
+ var GroupFileStore = class {
27251
+ dataDir;
27252
+ logger;
27253
+ cache = /* @__PURE__ */ new Map();
27254
+ constructor(opts) {
27255
+ this.dataDir = opts.dataDir;
27256
+ this.logger = opts.logger;
27257
+ }
27258
+ rootForScope(scope) {
27259
+ return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27260
+ }
27261
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
27262
+ filePath(scope, sessionId) {
27263
+ return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27264
+ }
27265
+ cacheKey(scope, sessionId) {
27266
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
27267
+ }
27268
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
27269
+ readFile(scope, sessionId) {
27270
+ const file = this.filePath(scope, sessionId);
27271
+ try {
27272
+ const raw = import_node_fs14.default.readFileSync(file, "utf8");
27273
+ const parsed = JSON.parse(raw);
27274
+ if (!Array.isArray(parsed)) {
27275
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
27276
+ file
27277
+ });
27278
+ return [];
27279
+ }
27280
+ const out = [];
27281
+ for (const entry of parsed) {
27282
+ const r = GroupFileEntrySchema.safeParse(entry);
27283
+ if (r.success) out.push(r.data);
27284
+ }
27285
+ return out;
27286
+ } catch (err) {
27287
+ const code = err?.code;
27288
+ if (code === "ENOENT") return [];
27289
+ this.logger?.warn("GroupFileStore.readFile failed", {
27290
+ file,
27291
+ err: err.message
27292
+ });
27293
+ return [];
27294
+ }
27295
+ }
27296
+ writeFile(scope, sessionId, entries) {
27297
+ const file = this.filePath(scope, sessionId);
27298
+ import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
27299
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27300
+ import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27301
+ import_node_fs14.default.renameSync(tmp, file);
27302
+ }
27303
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
27304
+ list(scope, sessionId) {
27305
+ const key = this.cacheKey(scope, sessionId);
27306
+ const cached = this.cache.get(key);
27307
+ if (cached) return cached.entries;
27308
+ const entries = this.readFile(scope, sessionId);
27309
+ this.cache.set(key, { entries });
27310
+ return entries;
27311
+ }
27312
+ /**
27313
+ * upsert:
27314
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
27315
+ * (保留首次入群人 / 入群时刻)
27316
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
27317
+ *
27318
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
27319
+ */
27320
+ upsert(scope, sessionId, input, now = Date.now()) {
27321
+ const entries = this.list(scope, sessionId).slice();
27322
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
27323
+ let next;
27324
+ if (idx >= 0) {
27325
+ const prev = entries[idx];
27326
+ next = {
27327
+ ...prev,
27328
+ size: input.size,
27329
+ mime: input.mime,
27330
+ lastEditedAt: now,
27331
+ stale: false
27332
+ // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
27333
+ };
27334
+ entries[idx] = next;
27335
+ } else {
27336
+ next = {
27337
+ id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
27338
+ relPath: input.relPath,
27339
+ from: input.from,
27340
+ label: input.label,
27341
+ size: input.size,
27342
+ mime: input.mime,
27343
+ addedAt: now
27344
+ // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
27345
+ };
27346
+ entries.push(next);
27347
+ }
27348
+ this.writeFile(scope, sessionId, entries);
27349
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27350
+ return next;
27351
+ }
27352
+ /**
27353
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
27354
+ * - 命中 → stale=true,UI 灰显
27355
+ * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
27356
+ *
27357
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
27358
+ */
27359
+ markStale(scope, sessionId, relPath) {
27360
+ const entries = this.list(scope, sessionId).slice();
27361
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27362
+ if (idx < 0) return;
27363
+ if (entries[idx].stale) return;
27364
+ entries[idx] = { ...entries[idx], stale: true };
27365
+ this.writeFile(scope, sessionId, entries);
27366
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27367
+ }
27368
+ /**
27369
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
27370
+ * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
27371
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
27372
+ *
27373
+ * 返回值:true=命中并删除;false=relPath 不在群里。
27374
+ */
27375
+ remove(scope, sessionId, relPath) {
27376
+ const entries = this.list(scope, sessionId).slice();
27377
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27378
+ if (idx < 0) return false;
27379
+ entries.splice(idx, 1);
27380
+ this.writeFile(scope, sessionId, entries);
27381
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27382
+ return true;
26292
27383
  }
26293
- const mime = lookupMime(absPath);
26294
- const basename = import_node_path16.default.basename(absPath);
26295
- res.writeHead(200, {
26296
- "Content-Type": mime,
26297
- "Content-Length": String(stat.size),
26298
- "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
26299
- // 防止浏览器把任意 mime 当 html 渲染
26300
- "X-Content-Type-Options": "nosniff"
26301
- });
26302
- const stream = import_node_fs14.default.createReadStream(absPath);
26303
- stream.on("error", (err) => {
26304
- logger?.warn("streamFile read error", { absPath, err: err.message });
26305
- res.destroy();
26306
- });
26307
- stream.pipe(res);
26308
- }
27384
+ };
26309
27385
 
26310
27386
  // src/discovery/state-file.ts
26311
27387
  var import_node_fs15 = __toESM(require("fs"), 1);
@@ -26985,26 +28061,32 @@ function authFilePath(dataDir) {
26985
28061
  }
26986
28062
  function loadOrCreateAuthFile(opts) {
26987
28063
  const file = authFilePath(opts.dataDir);
26988
- const generate = opts.generate ?? defaultGenerate;
28064
+ const generate = opts.generate ?? defaultGenerateToken2;
28065
+ const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
26989
28066
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
26990
28067
  const existing = readAuthFile(file);
26991
- if (existing && existing.token && existing.signSecret) {
28068
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
26992
28069
  return {
26993
28070
  token: existing.token,
26994
28071
  signSecret: existing.signSecret,
28072
+ ownerPrincipalId: existing.ownerPrincipalId,
26995
28073
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
26996
28074
  };
26997
28075
  }
26998
28076
  const token = existing?.token || generate();
26999
28077
  const signSecret = existing?.signSecret || generate();
28078
+ const ownerPrincipalId = existing?.ownerPrincipalId || genId();
27000
28079
  const createdAt = existing?.createdAt || now().toISOString();
27001
- const next = { token, signSecret, createdAt };
28080
+ const next = { token, signSecret, ownerPrincipalId, createdAt };
27002
28081
  writeAuthFile(file, next);
27003
28082
  return next;
27004
28083
  }
27005
- function defaultGenerate() {
28084
+ function defaultGenerateToken2() {
27006
28085
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
27007
28086
  }
28087
+ function defaultGenerateOwnerPrincipalId() {
28088
+ return `owner-${import_node_crypto8.default.randomUUID()}`;
28089
+ }
27008
28090
  function readAuthFile(file) {
27009
28091
  try {
27010
28092
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
@@ -27015,6 +28097,7 @@ function readAuthFile(file) {
27015
28097
  return {
27016
28098
  token: parsed.token,
27017
28099
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
28100
+ ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
27018
28101
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
27019
28102
  };
27020
28103
  } catch (err) {
@@ -27128,10 +28211,63 @@ function forkSession(input) {
27128
28211
  return { forkedToolSessionId, forkedFilePath };
27129
28212
  }
27130
28213
 
28214
+ // src/permission/capability.ts
28215
+ function matchResource(grant, target) {
28216
+ if (grant.type === "*") return true;
28217
+ if (grant.type !== target.type) return false;
28218
+ if (grant.id === "*") return true;
28219
+ return grant.id === target.id;
28220
+ }
28221
+ function assertGrant(grants, resource, action) {
28222
+ for (const g2 of grants) {
28223
+ if (!matchResource(g2.resource, resource)) continue;
28224
+ if (g2.actions.includes(action)) return true;
28225
+ if (g2.actions.includes("admin")) return true;
28226
+ }
28227
+ return false;
28228
+ }
28229
+
28230
+ // src/permission/session-access.ts
28231
+ function canAccessSession(ctx, sessionId, action, deps) {
28232
+ if (ctx.principal.kind === "owner") return true;
28233
+ const file = deps.readSession(sessionId);
28234
+ if (!file) return true;
28235
+ return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
28236
+ }
28237
+ function canAccessPersona(grants, personaId, action) {
28238
+ if (!personaId) return false;
28239
+ const resource = { type: "persona", id: personaId };
28240
+ return assertGrant(grants, resource, action);
28241
+ }
28242
+
27131
28243
  // src/handlers/session.ts
28244
+ init_protocol();
27132
28245
  function buildSessionHandlers(deps) {
27133
- const { manager, observer, getAdapter: getAdapter2 } = deps;
27134
- const create = async (frame) => {
28246
+ const { manager, observer, getAdapter: getAdapter2, store, ownerPrincipalId } = deps;
28247
+ const ensureSessionAccess = (ctx, sessionId, action) => {
28248
+ if (!ctx) return;
28249
+ const ok = canAccessSession(ctx, sessionId, action, {
28250
+ readSession: (sid) => store.read(sid)
28251
+ });
28252
+ if (!ok) {
28253
+ throw new ClawdError(
28254
+ ERROR_CODES.UNAUTHORIZED,
28255
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} session ${sessionId}`
28256
+ );
28257
+ }
28258
+ };
28259
+ const ensurePersonaAccess = (ctx, personaId, action) => {
28260
+ if (!ctx) return;
28261
+ if (ctx.principal.kind === "owner") return;
28262
+ const ok = canAccessPersona(ctx.grants, personaId, action);
28263
+ if (!ok) {
28264
+ throw new ClawdError(
28265
+ ERROR_CODES.UNAUTHORIZED,
28266
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} on persona:${personaId ?? "<none>"}`
28267
+ );
28268
+ }
28269
+ };
28270
+ const create = async (frame, _client, ctx) => {
27135
28271
  const args = SessionCreateArgs.parse(frame);
27136
28272
  if (args.ownerPersonaId) {
27137
28273
  const persona = deps.personaRegistry.get(args.ownerPersonaId);
@@ -27139,58 +28275,75 @@ function buildSessionHandlers(deps) {
27139
28275
  throw new Error(`persona not found: ${args.ownerPersonaId}`);
27140
28276
  }
27141
28277
  }
27142
- const { response, broadcast } = manager.create(args);
28278
+ ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
28279
+ const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
28280
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
27143
28281
  return { response: { type: "session:info", ...response }, broadcast };
27144
28282
  };
27145
- const list = async () => {
28283
+ const list = async (_frame, _client, ctx) => {
27146
28284
  const { response } = manager.list();
28285
+ if (ctx && ctx.principal.kind === "guest") {
28286
+ const sessions = response.sessions ?? [];
28287
+ const capId = ctx.capabilityId;
28288
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === capId);
28289
+ return { response: { type: "session:list", sessions: filtered } };
28290
+ }
27147
28291
  return { response: { type: "session:list", ...response } };
27148
28292
  };
27149
- const get = async (frame) => {
28293
+ const get = async (frame, _client, ctx) => {
27150
28294
  const args = SessionIdArgs.parse(frame);
28295
+ ensureSessionAccess(ctx, args.sessionId, "read");
27151
28296
  const { response } = manager.get(args);
27152
28297
  return { response: { type: "session:info", ...response } };
27153
28298
  };
27154
- const update = async (frame) => {
28299
+ const update = async (frame, _client, ctx) => {
27155
28300
  const args = SessionUpdateArgs.parse(frame);
28301
+ ensureSessionAccess(ctx, args.sessionId, "send");
27156
28302
  const { response, broadcast } = manager.update(args);
27157
28303
  return { response: { type: "session:info", ...response }, broadcast };
27158
28304
  };
27159
- const del = async (frame) => {
28305
+ const del = async (frame, _client, ctx) => {
27160
28306
  const args = SessionIdArgs.parse(frame);
28307
+ ensureSessionAccess(ctx, args.sessionId, "send");
27161
28308
  const { broadcast } = manager.delete(args);
27162
28309
  return {
27163
28310
  response: { type: "session:deleted", sessionId: args.sessionId },
27164
28311
  broadcast
27165
28312
  };
27166
28313
  };
27167
- const send = async (frame) => {
28314
+ const send = async (frame, _client, ctx) => {
27168
28315
  const args = SessionSendArgs.parse(frame);
28316
+ ensureSessionAccess(ctx, args.sessionId, "send");
27169
28317
  const { broadcast } = manager.send(args);
27170
28318
  return { response: { type: "session:send", ok: true }, broadcast };
27171
28319
  };
27172
- const stop = async (frame) => {
28320
+ const stop = async (frame, _client, ctx) => {
27173
28321
  const args = SessionIdArgs.parse(frame);
28322
+ ensureSessionAccess(ctx, args.sessionId, "send");
27174
28323
  const { broadcast } = await manager.stop(args);
27175
28324
  return { response: { type: "session:stop", ok: true }, broadcast };
27176
28325
  };
27177
- const interrupt = async (frame) => {
28326
+ const interrupt = async (frame, _client, ctx) => {
27178
28327
  const args = SessionIdArgs.parse(frame);
28328
+ ensureSessionAccess(ctx, args.sessionId, "send");
27179
28329
  const { broadcast } = await manager.interrupt(args);
27180
28330
  return { response: { type: "session:interrupt", ok: true }, broadcast };
27181
28331
  };
27182
- const rewind = async (frame) => {
28332
+ const rewind = async (frame, _client, ctx) => {
27183
28333
  const args = SessionRewindArgs.parse(frame);
28334
+ ensureSessionAccess(ctx, args.sessionId, "send");
27184
28335
  const { response, broadcast } = await manager.rewind(args);
27185
28336
  return { response: { type: "session:rewind", ...response }, broadcast };
27186
28337
  };
27187
- const rewindDiff = async (frame) => {
28338
+ const rewindDiff = async (frame, _client, ctx) => {
27188
28339
  const args = SessionRewindDiffArgs.parse(frame);
28340
+ ensureSessionAccess(ctx, args.sessionId, "read");
27189
28341
  const { response } = await manager.rewindDiff(args);
27190
28342
  return { response: { type: "session:rewind-diff", ...response } };
27191
28343
  };
27192
- const rewindableMessageIds = async (frame) => {
28344
+ const rewindableMessageIds = async (frame, _client, ctx) => {
27193
28345
  const args = SessionRewindableMessageIdsArgs.parse(frame);
28346
+ ensureSessionAccess(ctx, args.sessionId, "read");
27194
28347
  const { response } = manager.rewindableMessageIds(args);
27195
28348
  return {
27196
28349
  response: { type: "session:rewindable-message-ids", ...response }
@@ -27201,19 +28354,22 @@ function buildSessionHandlers(deps) {
27201
28354
  const result = forkSession(args);
27202
28355
  return { response: { type: "session:fork", ...result } };
27203
28356
  };
27204
- const newSession = async (frame) => {
28357
+ const newSession = async (frame, _client, ctx) => {
27205
28358
  const args = SessionIdArgs.parse(frame);
28359
+ ensureSessionAccess(ctx, args.sessionId, "send");
27206
28360
  observer.stop(args.sessionId);
27207
28361
  const { response, broadcast } = manager.newSession(args);
27208
28362
  return { response: { type: "session:info", ...response }, broadcast };
27209
28363
  };
27210
- const resume = async (frame) => {
28364
+ const resume = async (frame, _client, ctx) => {
27211
28365
  const args = SessionResumeArgs.parse(frame);
28366
+ ensureSessionAccess(ctx, args.sessionId, "send");
27212
28367
  const { response, broadcast } = manager.resume(args);
27213
28368
  return { response: { type: "session:info", ...response }, broadcast };
27214
28369
  };
27215
- const observe = async (frame) => {
28370
+ const observe = async (frame, _client, ctx) => {
27216
28371
  const args = SessionObserveArgs.parse(frame);
28372
+ ensureSessionAccess(ctx, args.sessionId, "read");
27217
28373
  const { response: file } = manager.get({ sessionId: args.sessionId });
27218
28374
  const sessionFile = file;
27219
28375
  manager.ensureSession(sessionFile);
@@ -27227,14 +28383,17 @@ function buildSessionHandlers(deps) {
27227
28383
  });
27228
28384
  return { response: { type: "session:observe", ok: true } };
27229
28385
  };
27230
- const events = async (frame) => {
28386
+ const events = async (frame, _client, ctx) => {
27231
28387
  const args = SessionEventsArgs.parse(frame);
28388
+ ensureSessionAccess(ctx, args.sessionId, "read");
27232
28389
  const { response } = manager.getEvents(args);
27233
28390
  return { response: { type: "session:events", ...response } };
27234
28391
  };
27235
- const subscribe = async (frame, client) => {
27236
- if (typeof frame.sessionId === "string")
28392
+ const subscribe = async (frame, client, ctx) => {
28393
+ if (typeof frame.sessionId === "string") {
28394
+ ensureSessionAccess(ctx, frame.sessionId, "read");
27237
28395
  addSubscription(client, frame.sessionId);
28396
+ }
27238
28397
  return {
27239
28398
  response: { type: "subscribed", sessionId: frame.sessionId }
27240
28399
  };
@@ -27246,8 +28405,9 @@ function buildSessionHandlers(deps) {
27246
28405
  response: { type: "unsubscribed", sessionId: frame.sessionId }
27247
28406
  };
27248
28407
  };
27249
- const pin = async (frame) => {
28408
+ const pin = async (frame, _client, ctx) => {
27250
28409
  const args = SessionPinArgs.parse(frame);
28410
+ ensureSessionAccess(ctx, args.sessionId, "send");
27251
28411
  const { response, broadcast } = manager.pin(args);
27252
28412
  return { response: { type: "session:info", ...response }, broadcast };
27253
28413
  };
@@ -27256,13 +28416,15 @@ function buildSessionHandlers(deps) {
27256
28416
  const { response, broadcast } = manager.reorderPins(args);
27257
28417
  return { response: { type: "session:reorderPins", ...response }, broadcast };
27258
28418
  };
27259
- const answerQuestion = async (frame) => {
28419
+ const answerQuestion = async (frame, _client, ctx) => {
27260
28420
  const args = AnswerQuestionArgs.parse(frame);
28421
+ ensureSessionAccess(ctx, args.sessionId, "send");
27261
28422
  const { response, broadcast } = manager.answerQuestion(args);
27262
28423
  return { response: { type: "session:answerQuestion", ...response }, broadcast };
27263
28424
  };
27264
- const cancelQuestion = async (frame) => {
28425
+ const cancelQuestion = async (frame, _client, ctx) => {
27265
28426
  const args = CancelQuestionArgs.parse(frame);
28427
+ ensureSessionAccess(ctx, args.sessionId, "send");
27266
28428
  const { response, broadcast } = await manager.cancelQuestion(args);
27267
28429
  return { response: { type: "session:cancelQuestion", ...response }, broadcast };
27268
28430
  };
@@ -27357,6 +28519,7 @@ function listRecentDirs(store, limit = 50) {
27357
28519
  }
27358
28520
 
27359
28521
  // src/handlers/history.ts
28522
+ init_protocol();
27360
28523
  function buildHistoryHandlers(deps) {
27361
28524
  const { manager, history, store } = deps;
27362
28525
  const projects = async () => {
@@ -27368,17 +28531,27 @@ function buildHistoryHandlers(deps) {
27368
28531
  const sessions = await history.listSessions(args);
27369
28532
  return { response: { type: "history:list", sessions } };
27370
28533
  };
27371
- const read = async (frame) => {
28534
+ const read = async (frame, _client, ctx) => {
27372
28535
  const args = HistoryReadArgs.parse(frame);
27373
28536
  const { response: file } = manager.get({ sessionId: args.sessionId });
27374
- if (!file.toolSessionId) {
28537
+ const f = file;
28538
+ if (ctx && ctx.principal.kind === "guest") {
28539
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
28540
+ if (!ok) {
28541
+ throw new ClawdError(
28542
+ ERROR_CODES.UNAUTHORIZED,
28543
+ `principal guest:${ctx.principal.id} cannot read history of session ${args.sessionId}`
28544
+ );
28545
+ }
28546
+ }
28547
+ if (!f.toolSessionId) {
27375
28548
  return {
27376
28549
  response: { type: "history:read", messages: [], total: 0, offset: 0 }
27377
28550
  };
27378
28551
  }
27379
28552
  const res = await history.read({
27380
- cwd: file.cwd,
27381
- toolSessionId: file.toolSessionId,
28553
+ cwd: f.cwd,
28554
+ toolSessionId: f.toolSessionId,
27382
28555
  limit: args.limit,
27383
28556
  offset: args.offset
27384
28557
  });
@@ -27560,6 +28733,345 @@ function buildCapabilitiesHandlers(deps) {
27560
28733
  };
27561
28734
  }
27562
28735
 
28736
+ // src/handlers/capability.ts
28737
+ init_zod();
28738
+ init_protocol();
28739
+ var DeleteArgsSchema = external_exports.object({
28740
+ capabilityId: external_exports.string().min(1)
28741
+ }).strict();
28742
+ function buildCapabilityHandlers(deps) {
28743
+ const { manager, getShareBaseUrl } = deps;
28744
+ const issue = async (frame) => {
28745
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28746
+ const args = CapabilityIssueArgsSchema.parse(rest);
28747
+ const { token, capability } = manager.issue({
28748
+ displayName: args.displayName,
28749
+ grants: args.grants,
28750
+ ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
28751
+ ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
28752
+ });
28753
+ const base = getShareBaseUrl().replace(/\/$/, "");
28754
+ const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
28755
+ return {
28756
+ response: {
28757
+ type: "capability:issued",
28758
+ token,
28759
+ shareUrl,
28760
+ capability: stripSecretHash(capability)
28761
+ }
28762
+ };
28763
+ };
28764
+ const list = async () => {
28765
+ return {
28766
+ response: {
28767
+ type: "capability:list",
28768
+ capabilities: manager.list().map(stripSecretHash)
28769
+ }
28770
+ };
28771
+ };
28772
+ const del = async (frame) => {
28773
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28774
+ const args = DeleteArgsSchema.parse(rest);
28775
+ const result = manager.delete(args.capabilityId);
28776
+ if (!result) {
28777
+ throw new ClawdError(
28778
+ ERROR_CODES.VALIDATION_ERROR,
28779
+ `capability not found: ${args.capabilityId}`
28780
+ );
28781
+ }
28782
+ return {
28783
+ response: {
28784
+ type: "capability:deleted",
28785
+ capabilityId: args.capabilityId
28786
+ }
28787
+ };
28788
+ };
28789
+ const bindPeer = async (frame) => {
28790
+ const { type: _type, requestId: _requestId, ...rest } = frame;
28791
+ const args = CapabilityBindPeerArgsSchema.parse(rest);
28792
+ try {
28793
+ manager.recordPeerHello(args.capabilityId, {
28794
+ ownerPrincipalId: args.peerOwnerId,
28795
+ displayName: args.peerDisplayName,
28796
+ now: Date.now()
28797
+ });
28798
+ } catch (e) {
28799
+ throw new ClawdError(
28800
+ ERROR_CODES.VALIDATION_ERROR,
28801
+ `capability:bindPeer: ${e.message}`
28802
+ );
28803
+ }
28804
+ return {
28805
+ response: { type: "capability:bindPeer:ok", capabilityId: args.capabilityId }
28806
+ };
28807
+ };
28808
+ return {
28809
+ "capability:issue": issue,
28810
+ "capability:list": list,
28811
+ "capability:delete": del,
28812
+ "capability:bindPeer": bindPeer
28813
+ };
28814
+ }
28815
+
28816
+ // src/handlers/inbox.ts
28817
+ init_protocol();
28818
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
28819
+ if (ctx.principal.kind === "owner") {
28820
+ if (!argsPeerOwnerId) {
28821
+ throw new ClawdError(
28822
+ ERROR_CODES.VALIDATION_ERROR,
28823
+ "inbox: owner ctx requires peerOwnerId"
28824
+ );
28825
+ }
28826
+ return argsPeerOwnerId;
28827
+ }
28828
+ if (!ctx.capabilityId) {
28829
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
28830
+ }
28831
+ const cap = capManager.findById(ctx.capabilityId);
28832
+ if (!cap?.peerOwnerId) {
28833
+ throw new ClawdError(
28834
+ ERROR_CODES.UNAUTHORIZED,
28835
+ `inbox: guest cap ${ctx.capabilityId} has no peerOwnerId (recordPeerHello not yet)`
28836
+ );
28837
+ }
28838
+ if (argsPeerOwnerId && argsPeerOwnerId !== cap.peerOwnerId) {
28839
+ throw new ClawdError(
28840
+ ERROR_CODES.UNAUTHORIZED,
28841
+ `inbox: guest args.peerOwnerId mismatch (cap.peerOwnerId=${cap.peerOwnerId}, args=${argsPeerOwnerId})`
28842
+ );
28843
+ }
28844
+ return cap.peerOwnerId;
28845
+ }
28846
+ function buildInboxHandlers(deps) {
28847
+ const { manager, capManager } = deps;
28848
+ const postMessage = async (frame, _client, ctx) => {
28849
+ const { type: _t, requestId: _r, ...rest } = frame;
28850
+ const args = InboxPostMessageArgsSchema.parse(rest);
28851
+ if (!ctx) {
28852
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
28853
+ }
28854
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28855
+ const message = manager.postMessage({
28856
+ peerOwnerId,
28857
+ senderPrincipalId: ctx.principal.id,
28858
+ text: args.text,
28859
+ id: args.id,
28860
+ createdAt: args.createdAt
28861
+ });
28862
+ return {
28863
+ response: { type: "inbox:postMessage:ok", message }
28864
+ };
28865
+ };
28866
+ const list = async (frame, _client, ctx) => {
28867
+ const { type: _t, requestId: _r, ...rest } = frame;
28868
+ const args = InboxListArgsSchema.parse(rest);
28869
+ if (!ctx) {
28870
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
28871
+ }
28872
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28873
+ const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
28874
+ return {
28875
+ response: { type: "inbox:list:ok", peerOwnerId, messages }
28876
+ };
28877
+ };
28878
+ const markRead = async (frame, _client, ctx) => {
28879
+ const { type: _t, requestId: _r, ...rest } = frame;
28880
+ const args = InboxMarkReadArgsSchema.parse(rest);
28881
+ if (!ctx) {
28882
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
28883
+ }
28884
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
28885
+ const updated = manager.markRead({
28886
+ peerOwnerId,
28887
+ principalId: ctx.principal.id,
28888
+ upToCreatedAt: args.upToCreatedAt
28889
+ });
28890
+ return {
28891
+ response: {
28892
+ type: "inbox:markRead:ok",
28893
+ peerOwnerId,
28894
+ upToCreatedAt: args.upToCreatedAt,
28895
+ updated
28896
+ }
28897
+ };
28898
+ };
28899
+ return {
28900
+ "inbox:postMessage": postMessage,
28901
+ "inbox:list": list,
28902
+ "inbox:markRead": markRead
28903
+ };
28904
+ }
28905
+
28906
+ // src/handlers/received-capability.ts
28907
+ init_protocol();
28908
+ function ensureOwner(ctx) {
28909
+ if (!ctx || ctx.principal.kind !== "owner") {
28910
+ throw new ClawdError(
28911
+ ERROR_CODES.UNAUTHORIZED,
28912
+ "UNAUTHORIZED: received-capability:* requires owner ctx"
28913
+ );
28914
+ }
28915
+ }
28916
+ function ensureGuestCtx(ctx) {
28917
+ if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
28918
+ throw new ClawdError(
28919
+ ERROR_CODES.UNAUTHORIZED,
28920
+ "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
28921
+ );
28922
+ }
28923
+ }
28924
+ function buildReceivedCapabilityHandlers(deps) {
28925
+ const now = deps.now ?? Date.now;
28926
+ const list = async (_frame, _client, ctx) => {
28927
+ ensureOwner(ctx);
28928
+ return {
28929
+ response: {
28930
+ type: "received-capability:list:ok",
28931
+ receivedCaps: deps.store.list()
28932
+ }
28933
+ };
28934
+ };
28935
+ const add = async (frame, _client, ctx) => {
28936
+ ensureOwner(ctx);
28937
+ const { type: _t, requestId: _r, ...rest } = frame;
28938
+ const args = ReceivedCapabilityAddArgsSchema.parse(rest);
28939
+ let conn;
28940
+ try {
28941
+ conn = await deps.connectRemote({
28942
+ url: args.remoteUrl,
28943
+ token: args.token,
28944
+ selfPrincipalId: deps.selfPrincipalId(),
28945
+ selfDisplayName: deps.selfDisplayName()
28946
+ });
28947
+ } catch (e) {
28948
+ throw new ClawdError(
28949
+ ERROR_CODES.VALIDATION_ERROR,
28950
+ `INVITE_REMOTE_UNREACHABLE: ${e.message}`
28951
+ );
28952
+ }
28953
+ try {
28954
+ const wh = await conn.whoami();
28955
+ const rc = {
28956
+ ownerPrincipalId: wh.ownerId,
28957
+ ownerDisplayName: wh.displayName,
28958
+ remoteUrl: args.remoteUrl,
28959
+ capabilityId: wh.capabilityId,
28960
+ capabilityToken: args.token,
28961
+ grants: wh.grants,
28962
+ receivedAt: now()
28963
+ };
28964
+ deps.store.upsert(rc);
28965
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
28966
+ return {
28967
+ response: { type: "received-capability:add:ok", receivedCap: rc }
28968
+ };
28969
+ } finally {
28970
+ try {
28971
+ conn.close();
28972
+ } catch {
28973
+ }
28974
+ }
28975
+ };
28976
+ const remove = async (frame, _client, ctx) => {
28977
+ ensureOwner(ctx);
28978
+ const { type: _t, requestId: _r, ...rest } = frame;
28979
+ const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
28980
+ deps.store.remove(args.ownerPrincipalId);
28981
+ deps.broadcast({
28982
+ type: "received-capability:removed",
28983
+ ownerPrincipalId: args.ownerPrincipalId
28984
+ });
28985
+ return {
28986
+ response: {
28987
+ type: "received-capability:remove:ok",
28988
+ ownerPrincipalId: args.ownerPrincipalId
28989
+ }
28990
+ };
28991
+ };
28992
+ const autoAttach = async (frame, _client, ctx) => {
28993
+ ensureGuestCtx(ctx);
28994
+ const { type: _t, requestId: _r, ...rest } = frame;
28995
+ const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
28996
+ const rc = {
28997
+ ownerPrincipalId: args.ownerPrincipalId,
28998
+ ownerDisplayName: args.ownerDisplayName,
28999
+ remoteUrl: args.remoteUrl,
29000
+ capabilityId: args.capabilityId,
29001
+ capabilityToken: args.token,
29002
+ grants: args.grants,
29003
+ receivedAt: now()
29004
+ };
29005
+ deps.store.upsert(rc);
29006
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
29007
+ return {
29008
+ response: { type: "received-capability:autoAttach:ok" }
29009
+ };
29010
+ };
29011
+ return {
29012
+ "received-capability:list": list,
29013
+ "received-capability:add": add,
29014
+ "received-capability:remove": remove,
29015
+ "received-capability:autoAttach": autoAttach
29016
+ };
29017
+ }
29018
+
29019
+ // src/handlers/whoami.ts
29020
+ init_protocol();
29021
+ function buildWhoamiHandler(deps) {
29022
+ return async (_frame, _client, ctx) => {
29023
+ if (!ctx) {
29024
+ throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
29025
+ }
29026
+ const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
29027
+ let capability;
29028
+ if (ctx.principal.kind === "owner") {
29029
+ capability = {
29030
+ id: deps.ownerPrincipalId,
29031
+ displayName: deps.ownerDisplayName,
29032
+ grants: ctx.grants,
29033
+ issuedAt: 0,
29034
+ usedCount: 0
29035
+ };
29036
+ } else {
29037
+ if (!ctx.capabilityId) {
29038
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
29039
+ }
29040
+ const cap = deps.capabilityManager.findById(ctx.capabilityId);
29041
+ if (!cap) {
29042
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
29043
+ }
29044
+ capability = stripSecretHash(cap);
29045
+ }
29046
+ const grantedPersonas = [];
29047
+ const isGuest = ctx.principal.kind === "guest";
29048
+ const hasWildcard = capability.grants.some((g2) => g2.resource.type === "*");
29049
+ if (hasWildcard) {
29050
+ for (const id of deps.personaStore.list()) {
29051
+ const file = deps.personaStore.read(id);
29052
+ if (!file) continue;
29053
+ if (isGuest && !file.public) continue;
29054
+ grantedPersonas.push({ id: file.personaId, displayName: file.label });
29055
+ }
29056
+ } else {
29057
+ for (const g2 of capability.grants) {
29058
+ if (g2.resource.type !== "persona") continue;
29059
+ const file = deps.personaStore.read(g2.resource.id);
29060
+ if (!file) continue;
29061
+ grantedPersonas.push({ id: file.personaId, displayName: file.label });
29062
+ }
29063
+ }
29064
+ return {
29065
+ response: {
29066
+ type: "whoami:ok",
29067
+ owner,
29068
+ capability,
29069
+ grantedPersonas
29070
+ }
29071
+ };
29072
+ };
29073
+ }
29074
+
27563
29075
  // src/handlers/meta.ts
27564
29076
  var import_node_os13 = __toESM(require("os"), 1);
27565
29077
  init_protocol();
@@ -27623,9 +29135,14 @@ function buildPersonaHandlers(deps) {
27623
29135
  const persona = personaManager.create(args);
27624
29136
  return { response: { type: "persona:info", ...persona } };
27625
29137
  };
27626
- const list = async () => {
29138
+ const list = async (_frame, _client, ctx) => {
29139
+ const all = personaRegistry.list();
29140
+ const isGuest = ctx?.principal.kind === "guest";
29141
+ const personas = isGuest ? all.filter(
29142
+ (p2) => p2.public && assertGrant(ctx.grants, { type: "persona", id: p2.personaId }, "read")
29143
+ ) : all;
27627
29144
  return {
27628
- response: { type: "persona:list", personas: personaRegistry.list() }
29145
+ response: { type: "persona:list", personas }
27629
29146
  };
27630
29147
  };
27631
29148
  const get = async (frame) => {
@@ -27663,26 +29180,12 @@ function buildPersonaHandlers(deps) {
27663
29180
  response: { type: "persona:deleted", personaId: args.personaId }
27664
29181
  };
27665
29182
  };
27666
- const issueToken = async (frame) => {
27667
- const args = PersonaIssueTokenArgsSchema.parse(frame);
27668
- const { token, persona } = personaManager.issueToken(args.personaId, args.label);
27669
- return {
27670
- response: { type: "persona:tokenIssued", token, persona }
27671
- };
27672
- };
27673
- const revokeToken = async (frame) => {
27674
- const args = PersonaRevokeTokenArgsSchema.parse(frame);
27675
- const persona = personaManager.revokeToken(args.personaId, args.token);
27676
- return { response: { type: "persona:info", ...persona } };
27677
- };
27678
29183
  return {
27679
29184
  "persona:create": create,
27680
29185
  "persona:list": list,
27681
29186
  "persona:get": get,
27682
29187
  "persona:update": update,
27683
- "persona:delete": del,
27684
- "persona:issueToken": issueToken,
27685
- "persona:revokeToken": revokeToken
29188
+ "persona:delete": del
27686
29189
  };
27687
29190
  }
27688
29191
 
@@ -27813,23 +29316,11 @@ function buildAttachmentHandlers(deps) {
27813
29316
  const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
27814
29317
  return { response: { type: "attachment.groupList", entries } };
27815
29318
  };
27816
- const groupListPersona = async (frame) => {
27817
- if (!deps.groupFileStore) {
27818
- throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
27819
- }
27820
- const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
27821
- if (!parsed.success) {
27822
- throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
27823
- }
27824
- const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
27825
- return { response: { type: "attachment.groupListPersona", perSession } };
27826
- };
27827
29319
  return {
27828
29320
  "attachment.signUrl": signUrl,
27829
29321
  "attachment.groupAdd": groupAdd,
27830
29322
  "attachment.groupRemove": groupRemove,
27831
- "attachment.groupList": groupList,
27832
- "attachment.groupListPersona": groupListPersona
29323
+ "attachment.groupList": groupList
27833
29324
  };
27834
29325
  }
27835
29326
 
@@ -27847,10 +29338,136 @@ function buildMethodHandlers(deps) {
27847
29338
  personaManager: deps.personaManager,
27848
29339
  personaRegistry: deps.personaRegistry
27849
29340
  }),
29341
+ ...buildCapabilityHandlers({
29342
+ manager: deps.capabilityManager,
29343
+ getShareBaseUrl: deps.getShareBaseUrl
29344
+ }),
29345
+ ...buildInboxHandlers({
29346
+ manager: deps.inboxManager,
29347
+ capManager: deps.capabilityManager
29348
+ }),
29349
+ ...buildReceivedCapabilityHandlers({
29350
+ store: deps.receivedCapabilityStore,
29351
+ connectRemote: deps.connectRemote,
29352
+ broadcast: deps.broadcastToOwners,
29353
+ selfPrincipalId: () => deps.ownerPrincipalId,
29354
+ selfDisplayName: () => deps.ownerDisplayName
29355
+ }),
29356
+ whoami: buildWhoamiHandler({
29357
+ ownerDisplayName: deps.ownerDisplayName,
29358
+ ownerPrincipalId: deps.ownerPrincipalId,
29359
+ personaStore: deps.personaStore,
29360
+ capabilityManager: deps.capabilityManager
29361
+ }),
27850
29362
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
27851
29363
  };
27852
29364
  }
27853
29365
 
29366
+ // src/handlers/method-grants.ts
29367
+ var ADMIN_ANY = {
29368
+ kind: "fixed",
29369
+ resource: { type: "*" },
29370
+ action: "admin"
29371
+ };
29372
+ var CAPABILITY_SCOPED = { kind: "capability-scoped" };
29373
+ var METHOD_GRANT_MAP = {
29374
+ // ---- public(meta-only,guest 也能调) ----
29375
+ "info": { kind: "public" },
29376
+ "ping": { kind: "public" },
29377
+ // v2 Phase 6: whoami 是身份反查,任何 authed connection (owner / guest) 都能调;
29378
+ // handler 内部按 ctx.principal.kind 决定返回 "合成 owner self capability" 还是
29379
+ // "查 capabilityManager 拿 caller 的 guest capability"。
29380
+ "whoami": { kind: "public" },
29381
+ // ---- capability platform(admin-only,本 PR 新增) ----
29382
+ "capability:issue": ADMIN_ANY,
29383
+ "capability:list": ADMIN_ANY,
29384
+ "capability:delete": ADMIN_ANY,
29385
+ "capability:bindPeer": ADMIN_ANY,
29386
+ // ---- inbox: channel-based IM (capability platform v3) ----
29387
+ // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
29388
+ // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
29389
+ "inbox:list": { kind: "public" },
29390
+ "inbox:markRead": { kind: "public" },
29391
+ "inbox:postMessage": { kind: "public" },
29392
+ // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
29393
+ // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
29394
+ "received-capability:list": ADMIN_ANY,
29395
+ "received-capability:add": ADMIN_ANY,
29396
+ "received-capability:remove": ADMIN_ANY,
29397
+ "received-capability:autoAttach": { kind: "public" },
29398
+ // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
29399
+ // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
29400
+ // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
29401
+ "session:create": CAPABILITY_SCOPED,
29402
+ "session:list": CAPABILITY_SCOPED,
29403
+ "session:get": CAPABILITY_SCOPED,
29404
+ "session:update": CAPABILITY_SCOPED,
29405
+ "session:delete": CAPABILITY_SCOPED,
29406
+ "session:send": CAPABILITY_SCOPED,
29407
+ "session:stop": CAPABILITY_SCOPED,
29408
+ "session:interrupt": CAPABILITY_SCOPED,
29409
+ "session:rewind": CAPABILITY_SCOPED,
29410
+ "session:rewind-diff": CAPABILITY_SCOPED,
29411
+ "session:rewindable-message-ids": CAPABILITY_SCOPED,
29412
+ "session:fork": CAPABILITY_SCOPED,
29413
+ "session:new": CAPABILITY_SCOPED,
29414
+ "session:resume": CAPABILITY_SCOPED,
29415
+ "session:observe": CAPABILITY_SCOPED,
29416
+ "session:events": CAPABILITY_SCOPED,
29417
+ "session:subscribe": CAPABILITY_SCOPED,
29418
+ "session:unsubscribe": CAPABILITY_SCOPED,
29419
+ "session:pin": CAPABILITY_SCOPED,
29420
+ "session:reorderPins": ADMIN_ANY,
29421
+ // owner 全局操作,无 personaId 维度
29422
+ "permission:respond": CAPABILITY_SCOPED,
29423
+ "session:answerQuestion": CAPABILITY_SCOPED,
29424
+ "session:cancelQuestion": CAPABILITY_SCOPED,
29425
+ "history:projects": ADMIN_ANY,
29426
+ "history:list": ADMIN_ANY,
29427
+ // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
29428
+ // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
29429
+ // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
29430
+ "history:read": CAPABILITY_SCOPED,
29431
+ "history:subagents": ADMIN_ANY,
29432
+ "history:subagent-read": ADMIN_ANY,
29433
+ "history:recentDirs": ADMIN_ANY,
29434
+ "workspace:list": ADMIN_ANY,
29435
+ "workspace:read": ADMIN_ANY,
29436
+ "skills:list": ADMIN_ANY,
29437
+ "agents:list": ADMIN_ANY,
29438
+ "git:root": ADMIN_ANY,
29439
+ "git:branch": ADMIN_ANY,
29440
+ "git:branches": ADMIN_ANY,
29441
+ "capabilities:get": ADMIN_ANY,
29442
+ "persona:create": ADMIN_ANY,
29443
+ // persona:list 下沉到 CAPABILITY_SCOPED:guest 调用合法(用于 People 面板展开
29444
+ // 对方分享的 personas),handler 端按 ctx + persona.public + assertGrant 过滤。
29445
+ "persona:list": CAPABILITY_SCOPED,
29446
+ "persona:get": ADMIN_ANY,
29447
+ "persona:update": ADMIN_ANY,
29448
+ "persona:delete": ADMIN_ANY,
29449
+ "session:pty:input": ADMIN_ANY,
29450
+ "session:pty:resize": ADMIN_ANY,
29451
+ // file-sharing attachment.* RPC:dispatcher 用 admin-only 兜底(wire-level 拦),
29452
+ // 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
29453
+ "attachment.signUrl": ADMIN_ANY,
29454
+ "attachment.groupAdd": ADMIN_ANY,
29455
+ "attachment.groupRemove": ADMIN_ANY,
29456
+ "attachment.groupList": ADMIN_ANY
29457
+ };
29458
+ function computeGrantForFrame(method, frame) {
29459
+ const rule = METHOD_GRANT_MAP[method];
29460
+ if (!rule) return { kind: "public" };
29461
+ if (rule.kind === "public") return { kind: "public" };
29462
+ if (rule.kind === "capability-scoped") return { kind: "public" };
29463
+ if (rule.kind === "fixed") {
29464
+ return { kind: "check", resource: rule.resource, action: rule.action };
29465
+ }
29466
+ const picked = rule.pick(frame);
29467
+ if (!picked) return { kind: "public" };
29468
+ return { kind: "check", resource: picked.resource, action: picked.action };
29469
+ }
29470
+
27854
29471
  // src/index.ts
27855
29472
  async function startDaemon(config) {
27856
29473
  const logger = createLogger({
@@ -27866,24 +29483,90 @@ async function startDaemon(config) {
27866
29483
  if (pre.status === "stale") {
27867
29484
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
27868
29485
  }
29486
+ const authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27869
29487
  let resolvedAuthToken = null;
27870
- let authFile = null;
27871
29488
  if (config.authToken && config.authToken.trim()) {
27872
29489
  resolvedAuthToken = config.authToken.trim();
27873
29490
  } else if (config.tunnel) {
27874
- authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27875
29491
  resolvedAuthToken = authFile.token;
27876
29492
  }
29493
+ const ownerPrincipalId = authFile.ownerPrincipalId;
29494
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
27877
29495
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
29496
+ migrateLegacyFiles(config.dataDir);
29497
+ const inboxStore = new InboxStore(config.dataDir);
29498
+ const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
29499
+ wsServer?.broadcastToOwners(frame);
29500
+ });
29501
+ const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
29502
+ receivedCapabilityStore.load();
29503
+ const capabilityStore = new CapabilityStore(config.dataDir);
29504
+ const capabilityRegistry = new CapabilityRegistry(capabilityStore);
29505
+ const capabilityManager = new CapabilityManager(capabilityRegistry, {
29506
+ onIssued: (cap, token) => {
29507
+ wsServer?.broadcastToOwners({
29508
+ type: "capability:tokenIssued",
29509
+ capability: stripSecretHash(cap),
29510
+ token
29511
+ });
29512
+ },
29513
+ onDeleted: (cap) => {
29514
+ const deletedAt = Date.now();
29515
+ wsServer?.broadcastToOwners({
29516
+ type: "capability:tokenDeleted",
29517
+ capabilityId: cap.id,
29518
+ deletedAt
29519
+ });
29520
+ wsServer?.closeConnectionsByCapability(cap.id);
29521
+ const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
29522
+ if (cleanup.removed.length > 0) {
29523
+ logger.info("capability delete cascade: guest sessions removed", {
29524
+ capabilityId: cap.id,
29525
+ removedDirs: cleanup.removed
29526
+ });
29527
+ }
29528
+ }
29529
+ });
29530
+ migrateDropPersonsDir(config.dataDir);
27878
29531
  let wsServer = null;
27879
29532
  const authGate = authMode === "first-message" ? new AuthGate({
27880
29533
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
29534
+ // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
29535
+ // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
27881
29536
  expectedToken: resolvedAuthToken,
29537
+ authenticate: (t, selfPrincipalId, selfDisplayName) => {
29538
+ return authenticate(t, {
29539
+ isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
29540
+ ownerPrincipalId,
29541
+ ownerDisplayName,
29542
+ capabilityRegistry,
29543
+ selfPrincipalId,
29544
+ selfDisplayName,
29545
+ onGuestHello: (capId, ownerId, name) => {
29546
+ capabilityManager.recordPeerHello(capId, {
29547
+ ownerPrincipalId: ownerId,
29548
+ displayName: name,
29549
+ now: Date.now()
29550
+ });
29551
+ }
29552
+ });
29553
+ },
29554
+ onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
29555
+ buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
27882
29556
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
27883
29557
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
27884
29558
  }) : null;
27885
29559
  resetRegistry();
27886
- const store = new SessionStore({ dataDir: config.dataDir });
29560
+ const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
29561
+ if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
29562
+ logger.info("sessions migration applied", {
29563
+ movedBare: migrateResult.movedBare,
29564
+ movedVmOwner: migrateResult.movedVmOwner,
29565
+ archivedListener: migrateResult.archivedListener
29566
+ });
29567
+ }
29568
+ const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
29569
+ const store = sessionStoreFactory.forBare();
27887
29570
  const workspace = new WorkspaceBrowser();
27888
29571
  const skills = new SkillsScanner();
27889
29572
  const agents = new AgentsScanner();
@@ -27896,10 +29579,12 @@ async function startDaemon(config) {
27896
29579
  } else {
27897
29580
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
27898
29581
  }
27899
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
27900
29582
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
27901
29583
  const manager = new SessionManager({
27902
29584
  store,
29585
+ // Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
29586
+ // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
29587
+ storeFactory: sessionStoreFactory,
27903
29588
  logger,
27904
29589
  getAdapter,
27905
29590
  historyReader: history,
@@ -27907,6 +29592,7 @@ async function startDaemon(config) {
27907
29592
  personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
27908
29593
  personaStore,
27909
29594
  ownerDisplayName,
29595
+ ownerPrincipalId,
27910
29596
  mode: config.mode,
27911
29597
  broadcastFrame: (frame, target) => {
27912
29598
  if (target === "all") {
@@ -28042,18 +29728,37 @@ async function startDaemon(config) {
28042
29728
  // group RPC + sign 都用:根据 sessionId 反查 scope。owner-mode persona session 走
28043
29729
  // 'persona/<pid>/owner',default 走 'default'。
28044
29730
  getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
28045
- }
29731
+ },
29732
+ // Task 1.9: capability:issue/list/revoke handler 依赖
29733
+ capabilityManager,
29734
+ // v2 Phase 5: capability:issue 返回的 shareUrl 用此 base URL 拼接。
29735
+ // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
29736
+ getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
29737
+ // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
29738
+ ownerDisplayName,
29739
+ // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
29740
+ ownerPrincipalId,
29741
+ personaStore,
29742
+ // capability handler 也用 (capability:issue 走 registry / capability:list)
29743
+ capabilityRegistry,
29744
+ // capability platform v3 inbox: handler 接 manager (post/list/markRead),
29745
+ // cascade 接 store (list 统计 deletedInboxEvents).
29746
+ inboxManager,
29747
+ inboxStore,
29748
+ // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
29749
+ receivedCapabilityStore,
29750
+ // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
29751
+ broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
29752
+ // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
29753
+ connectRemote
28046
29754
  });
28047
29755
  const authResolver = new AuthContextResolver({
28048
- ownerToken: resolvedAuthToken,
28049
- personaRegistry
29756
+ ownerToken: resolvedAuthToken
28050
29757
  });
28051
29758
  const httpRouter = createHttpRouter({
28052
29759
  authResolver,
28053
29760
  daemonVersion: version,
28054
29761
  logger,
28055
- personaStore,
28056
- groupFileStore,
28057
29762
  sessionStore: store,
28058
29763
  // /files HMAC verify 用 auth.json 的 signSecret 字段(与 attachment.signUrl 同源)。
28059
29764
  // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
@@ -28063,6 +29768,8 @@ async function startDaemon(config) {
28063
29768
  host: config.host,
28064
29769
  port: config.port,
28065
29770
  logger,
29771
+ // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
29772
+ ownerPrincipalId,
28066
29773
  readyFrameBuilder: (ctx) => buildReadyFrame(
28067
29774
  {
28068
29775
  manager,
@@ -28078,6 +29785,29 @@ async function startDaemon(config) {
28078
29785
  ),
28079
29786
  protocolVersion: PROTOCOL_VERSION,
28080
29787
  authGate: authGate ?? void 0,
29788
+ // noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
29789
+ // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
29790
+ // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
29791
+ // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
29792
+ tryVerifyCapabilityToken: (token, selfPrincipalId, selfDisplayName) => {
29793
+ const v2 = capabilityRegistry.verifyToken(token);
29794
+ if (!v2.ok) return null;
29795
+ if (selfPrincipalId && selfDisplayName) {
29796
+ try {
29797
+ capabilityManager.recordPeerHello(v2.capability.id, {
29798
+ ownerPrincipalId: selfPrincipalId,
29799
+ displayName: selfDisplayName,
29800
+ now: Date.now()
29801
+ });
29802
+ } catch {
29803
+ }
29804
+ }
29805
+ return {
29806
+ principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
29807
+ grants: v2.capability.grants,
29808
+ capabilityId: v2.capability.id
29809
+ };
29810
+ },
28081
29811
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
28082
29812
  httpRequestHandler: httpRouter,
28083
29813
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
@@ -28130,7 +29860,18 @@ async function startDaemon(config) {
28130
29860
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
28131
29861
  const handler = handlers[type];
28132
29862
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
28133
- const result = await handler(frame, client);
29863
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
29864
+ const verdict = computeGrantForFrame(type, frame);
29865
+ if (verdict.kind === "check") {
29866
+ const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
29867
+ if (!ok) {
29868
+ throw new ClawdError(
29869
+ ERROR_CODES.UNAUTHORIZED,
29870
+ `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
29871
+ );
29872
+ }
29873
+ }
29874
+ const result = await handler(frame, client, ctx);
28134
29875
  if (requestId && result.response) {
28135
29876
  client.send({ ...result.response, requestId });
28136
29877
  }
@@ -28253,6 +29994,13 @@ ${bar}
28253
29994
  manager
28254
29995
  };
28255
29996
  }
29997
+ function migrateDropPersonsDir(dataDir) {
29998
+ const dir = import_node_path27.default.join(dataDir, "persons");
29999
+ try {
30000
+ import_node_fs24.default.rmSync(dir, { recursive: true, force: true });
30001
+ } catch {
30002
+ }
30003
+ }
28256
30004
 
28257
30005
  // src/cli.ts
28258
30006
  async function main() {