@clawos-dev/clawd 0.2.71-beta.131.d934b0e → 0.2.71-beta.134.b4f990a

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 +1289 -837
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -92,10 +92,6 @@ var init_methods = __esm({
92
92
  "persona:get",
93
93
  "persona:update",
94
94
  "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
95
  // ---- session:pty 双向透传(仅 CLAWD_CC_MODE=tui 才生效,对应 session:pty push 帧的上行) ----
100
96
  // session:pty:input — UI 把用户键盘字节(base64 UTF-8)发给 daemon,daemon 直写 pty stdin。
101
97
  // 高频帧(每次按键 1 帧),不入 reducer / 不广播。response 是 fire-and-forget 风格 ack。
@@ -106,21 +102,21 @@ var init_methods = __esm({
106
102
  // ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
107
103
  // 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
108
104
  // (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
109
- // 全部管理类 RPC handler 入口 requireOwnerpersonal token 调任意一个都 403);
110
- // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,签名验证),不在白名单内。
105
+ // 全部管理类 RPC dispatcher grant 限 ownercapability admin-only);
106
+ // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,HMAC 签名自包含,不在白名单内)。
111
107
  "attachment.signUrl",
112
108
  "attachment.groupAdd",
113
109
  "attachment.groupRemove",
114
110
  "attachment.groupList",
115
- // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
116
- "attachment.groupListPersona",
117
111
  // ---- capability:* (capability platform 鉴权底座) ----
118
- // owner 颁发 / 列出 / 撤销给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
112
+ // owner 颁发 / 列出 / 删除给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
119
113
  // 在 daemon 端固定为 `{ resource: '*', action: 'admin' }`,owner 自动满足)。
120
- // 颁发后 daemon 推 'capability:tokenIssued' 帧;撤销推 'capability:revoked' 帧。
114
+ // 颁发后 daemon 推 'capability:tokenIssued' 帧;删除推 'capability:tokenDeleted' 帧。
115
+ // 删除是 hard delete:CapabilityStore 物理移除 + 关该 cap 的所有活跃 ws + rm
116
+ // personas/<pid>/.clawd/sessions/guests/<capId>/ guest sessions 目录。
121
117
  "capability:issue",
122
118
  "capability:list",
123
- "capability:revoke",
119
+ "capability:delete",
124
120
  // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
125
121
  // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
126
122
  // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
@@ -139,7 +135,20 @@ var init_methods = __esm({
139
135
  // 的 preview 一次性临时 client 用它判定身份和可用 persona。
140
136
  "whoami",
141
137
  "info",
142
- "ping"
138
+ "ping",
139
+ // ---- person:* (Person identity Phase 1, spec v5) ----
140
+ // Person 抽象 = People panel 顶层 entity(一个 Person 聚合 N 个 cap + M 个 remote
141
+ // alias)。owner admin-only。daemon 维护 PersonStore + PersonAliasStore。
142
+ // - list 返回 PersonWithLinks[](含派生 linkedCapabilityIds / linkedRemoteAliases)
143
+ // - create / update / delete 操作 Person 实体(delete 触发 cascade:revoke 全部
144
+ // linked cap + remove subscribed alias + clear linked inbox events)
145
+ // - link 重新指向某 principal 到目标 personId(手动合并 / 重链入口;自动 link
146
+ // 在 capability:issue / remote-persona:add handler 内部直接写 PersonAliasStore)
147
+ "person:list",
148
+ "person:create",
149
+ "person:update",
150
+ "person:delete",
151
+ "person:link"
143
152
  ];
144
153
  }
145
154
  });
@@ -629,8 +638,8 @@ var init_parseUtil = __esm({
629
638
  init_errors2();
630
639
  init_en();
631
640
  makeIssue = (params) => {
632
- const { data, path: path37, errorMaps, issueData } = params;
633
- const fullPath = [...path37, ...issueData.path || []];
641
+ const { data, path: path39, errorMaps, issueData } = params;
642
+ const fullPath = [...path39, ...issueData.path || []];
634
643
  const fullIssue = {
635
644
  ...issueData,
636
645
  path: fullPath
@@ -941,11 +950,11 @@ var init_types = __esm({
941
950
  init_parseUtil();
942
951
  init_util();
943
952
  ParseInputLazyPath = class {
944
- constructor(parent, value, path37, key) {
953
+ constructor(parent, value, path39, key) {
945
954
  this._cachedPath = [];
946
955
  this.parent = parent;
947
956
  this.data = value;
948
- this._path = path37;
957
+ this._path = path39;
949
958
  this._key = key;
950
959
  }
951
960
  get path() {
@@ -4329,12 +4338,12 @@ var init_zod = __esm({
4329
4338
  });
4330
4339
 
4331
4340
  // ../protocol/src/attachment-schemas.ts
4332
- var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4341
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
4333
4342
  var init_attachment_schemas = __esm({
4334
4343
  "../protocol/src/attachment-schemas.ts"() {
4335
4344
  "use strict";
4336
4345
  init_zod();
4337
- TOKEN_ROLES = ["owner", "personal"];
4346
+ TOKEN_ROLES = ["owner"];
4338
4347
  GROUP_FILE_SOURCES = ["agent", "owner"];
4339
4348
  GroupFileEntrySchema = external_exports.object({
4340
4349
  /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
@@ -4388,31 +4397,15 @@ var init_attachment_schemas = __esm({
4388
4397
  AttachmentGroupListResponseSchema = external_exports.object({
4389
4398
  entries: external_exports.array(GroupFileEntrySchema)
4390
4399
  });
4391
- AttachmentGroupListPersonaArgs = external_exports.object({
4392
- personaId: external_exports.string().min(1)
4393
- });
4394
- AttachmentGroupListPersonaResponseSchema = external_exports.object({
4395
- perSession: external_exports.array(
4396
- external_exports.object({
4397
- sessionId: external_exports.string().min(1),
4398
- entries: external_exports.array(GroupFileEntrySchema)
4399
- })
4400
- )
4401
- });
4402
4400
  }
4403
4401
  });
4404
4402
 
4405
4403
  // ../protocol/src/persona-schemas.ts
4406
- var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema;
4404
+ var PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
4407
4405
  var init_persona_schemas = __esm({
4408
4406
  "../protocol/src/persona-schemas.ts"() {
4409
4407
  "use strict";
4410
4408
  init_zod();
4411
- PersonaTokenEntrySchema = external_exports.object({
4412
- label: external_exports.string(),
4413
- issuedAt: external_exports.number(),
4414
- revoked: external_exports.boolean()
4415
- });
4416
4409
  PersonaFileSchema = external_exports.object({
4417
4410
  personaId: external_exports.string(),
4418
4411
  label: external_exports.string(),
@@ -4421,13 +4414,6 @@ var init_persona_schemas = __esm({
4421
4414
  // 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
4422
4415
  // for forward compatibility, consumer fallbacks to default when key not found.
4423
4416
  iconKey: external_exports.string().optional(),
4424
- /**
4425
- * Persona token 列表:file-sharing HTTP Bearer 鉴权用。
4426
- * - 写:`PersonaManager.issueToken` / `revokeToken`
4427
- * - 读:`PersonaRegistry.findByToken` 反向查表(需 persona.public === true)
4428
- * - optional:兼容历史 persona.json 不含此字段的情形(旧 daemon 写文件时省略)
4429
- */
4430
- tokenMap: external_exports.record(external_exports.string(), PersonaTokenEntrySchema).optional(),
4431
4417
  createdAt: external_exports.number(),
4432
4418
  updatedAt: external_exports.number()
4433
4419
  }).strict();
@@ -4480,14 +4466,6 @@ var init_persona_schemas = __esm({
4480
4466
  iconKey: external_exports.string().nullable().optional()
4481
4467
  }).strict()
4482
4468
  }).strict();
4483
- PersonaIssueTokenArgsSchema = external_exports.object({
4484
- personaId: external_exports.string().min(1),
4485
- label: external_exports.string().min(1)
4486
- });
4487
- PersonaRevokeTokenArgsSchema = external_exports.object({
4488
- personaId: external_exports.string().min(1),
4489
- token: external_exports.string().min(1)
4490
- });
4491
4469
  }
4492
4470
  });
4493
4471
 
@@ -4594,6 +4572,15 @@ var init_schemas = __esm({
4594
4572
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
4595
4573
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4596
4574
  chatId: external_exports.string().min(1).optional(),
4575
+ /**
4576
+ * 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
4577
+ * 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4578
+ * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4579
+ * 后者再通过 PersonAlias 表反查 personId 二级分组。
4580
+ *
4581
+ * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4582
+ */
4583
+ creatorPrincipalId: external_exports.string().min(1).optional(),
4597
4584
  createdAt: external_exports.string().min(1),
4598
4585
  updatedAt: external_exports.string().min(1)
4599
4586
  });
@@ -5003,7 +4990,18 @@ var init_schemas = __esm({
5003
4990
  AuthRequestFrameSchema = external_exports.object({
5004
4991
  type: external_exports.literal("auth"),
5005
4992
  token: external_exports.string().min(1),
5006
- scheme: external_exports.literal("bearer").optional()
4993
+ scheme: external_exports.literal("bearer").optional(),
4994
+ /**
4995
+ * Person identity Phase 1: 如果 guest 自己也是 daemon owner,把自己的稳定
4996
+ * ownerPrincipalId 主动上报,让对端 daemon 反查 PersonAliasStore 自动 link 到
4997
+ * 已有 Person(避免出现"两个 Bob")。纯访客 / 老 daemon 客户端可省略。
4998
+ *
4999
+ * daemon transport/auth.ts 在 verify token 成功后:
5000
+ * - 若 cap.linkedPrincipalId 未设 → 更新 cap.linkedPrincipalId = selfPrincipalId
5001
+ * + PersonAliasStore.rekey(cap.id, selfPrincipalId)
5002
+ * - 若已设但与本次不一致 → 仅 warn log(潜在协议异常但不阻断)
5003
+ */
5004
+ selfPrincipalId: external_exports.string().min(1).optional()
5007
5005
  });
5008
5006
  AuthOkFrameSchema = external_exports.object({
5009
5007
  type: external_exports.literal("auth:ok"),
@@ -5036,10 +5034,9 @@ var init_schemas = __esm({
5036
5034
  // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5037
5035
  // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5038
5036
  // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5039
- /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5037
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
5038
+ * 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
5040
5039
  tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5041
- /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5042
- tokenPersonaId: external_exports.string().min(1).optional(),
5043
5040
  /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5044
5041
  isLoopback: external_exports.boolean().optional(),
5045
5042
  /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
@@ -5067,7 +5064,10 @@ var init_persona_mode = __esm({
5067
5064
  });
5068
5065
 
5069
5066
  // ../protocol/src/principal.ts
5070
- var PrincipalKindSchema, PrincipalSchema, OWNER_PRINCIPAL;
5067
+ function makeOwnerPrincipal(id, displayName) {
5068
+ return PrincipalSchema.parse({ id, kind: "owner", displayName });
5069
+ }
5070
+ var PrincipalKindSchema, PrincipalSchema;
5071
5071
  var init_principal = __esm({
5072
5072
  "../protocol/src/principal.ts"() {
5073
5073
  "use strict";
@@ -5078,11 +5078,6 @@ var init_principal = __esm({
5078
5078
  kind: PrincipalKindSchema,
5079
5079
  displayName: external_exports.string()
5080
5080
  }).strict();
5081
- OWNER_PRINCIPAL = {
5082
- id: "owner",
5083
- kind: "owner",
5084
- displayName: "owner"
5085
- };
5086
5081
  }
5087
5082
  });
5088
5083
 
@@ -5091,7 +5086,7 @@ function stripSecretHash(cap) {
5091
5086
  const { secretHash: _hash, ...wire } = cap;
5092
5087
  return wire;
5093
5088
  }
5094
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema;
5089
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema;
5095
5090
  var init_capability = __esm({
5096
5091
  "../protocol/src/capability.ts"() {
5097
5092
  "use strict";
@@ -5117,7 +5112,13 @@ var init_capability = __esm({
5117
5112
  expiresAt: external_exports.number().int().positive().optional(),
5118
5113
  maxUses: external_exports.number().int().positive().optional(),
5119
5114
  usedCount: external_exports.number().int().nonnegative(),
5120
- revokedAt: external_exports.number().int().positive().optional()
5115
+ /**
5116
+ * Person identity Phase 1: guest 真实身份。
5117
+ * - 纯访客 / 尚未 authenticate 时 undefined
5118
+ * - guest authenticate 上报 selfPrincipalId 后 daemon 落入此字段(immutable,不允许 owner 修改)
5119
+ * - PersonAliasStore 的 alias key 来源优先 linkedPrincipalId,否则 fallback cap.id
5120
+ */
5121
+ linkedPrincipalId: external_exports.string().min(1).optional()
5121
5122
  }).strict();
5122
5123
  CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
5123
5124
  CapabilityErrorCodeSchema = external_exports.enum([
@@ -5141,43 +5142,41 @@ var init_capability = __esm({
5141
5142
  }).strict()
5142
5143
  )
5143
5144
  }).strict();
5145
+ CapabilityIssueArgsSchema = external_exports.object({
5146
+ displayName: external_exports.string().min(1),
5147
+ grants: external_exports.array(GrantSchema),
5148
+ personId: external_exports.string().min(1),
5149
+ expiresAt: external_exports.number().int().positive().optional(),
5150
+ maxUses: external_exports.number().int().positive().optional()
5151
+ }).strict();
5144
5152
  }
5145
5153
  });
5146
5154
 
5147
5155
  // ../protocol/src/inbox.ts
5148
- var INBOX_EVENT_KIND_VALUES, InboxEventKindSchema, INBOX_PREVIEW_MAX_LENGTH, InboxEventSchema, InboxListArgsSchema, InboxMarkReadArgsSchema, InboxPostMessageArgsSchema;
5156
+ var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
5149
5157
  var init_inbox = __esm({
5150
5158
  "../protocol/src/inbox.ts"() {
5151
5159
  "use strict";
5152
5160
  init_zod();
5153
- init_principal();
5154
- init_capability();
5155
- INBOX_EVENT_KIND_VALUES = ["persona-mention", "direct-message"];
5156
- InboxEventKindSchema = external_exports.enum(INBOX_EVENT_KIND_VALUES);
5157
- INBOX_PREVIEW_MAX_LENGTH = 140;
5158
- InboxEventSchema = external_exports.object({
5161
+ InboxMessageSchema = external_exports.object({
5159
5162
  id: external_exports.string().min(1),
5160
- kind: InboxEventKindSchema,
5161
- fromPrincipal: PrincipalSchema,
5162
- toPrincipal: PrincipalSchema,
5163
- // persona-mention 才有, direct-message 不带
5164
- resource: ResourceSchema.optional(),
5165
- preview: external_exports.string().max(INBOX_PREVIEW_MAX_LENGTH),
5163
+ capabilityId: external_exports.string().min(1),
5164
+ senderPrincipalId: external_exports.string().min(1),
5165
+ text: external_exports.string().min(1),
5166
5166
  createdAt: external_exports.number().int().nonnegative(),
5167
- readAt: external_exports.number().int().positive().optional(),
5168
- // direct-message 才有, sha256 hex 64; 派生规则: sha256(min(A,B) + '/' + max(A,B))
5169
- threadId: external_exports.string().regex(/^[a-f0-9]{64}$/).optional()
5167
+ readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5168
+ }).strict();
5169
+ InboxPostMessageArgsSchema = external_exports.object({
5170
+ capabilityId: external_exports.string().min(1),
5171
+ text: external_exports.string().min(1)
5170
5172
  }).strict();
5171
5173
  InboxListArgsSchema = external_exports.object({
5172
- // false / 缺省 = 只返回未读; true = 含已读 (历史回溯用)
5173
- includeRead: external_exports.boolean().optional()
5174
+ capabilityId: external_exports.string().min(1),
5175
+ sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5174
5176
  }).strict();
5175
5177
  InboxMarkReadArgsSchema = external_exports.object({
5176
- eventId: external_exports.string().min(1)
5177
- }).strict();
5178
- InboxPostMessageArgsSchema = external_exports.object({
5179
- peerPrincipalId: external_exports.string().min(1),
5180
- text: external_exports.string().min(1).max(INBOX_PREVIEW_MAX_LENGTH)
5178
+ capabilityId: external_exports.string().min(1),
5179
+ upToCreatedAt: external_exports.number().int().nonnegative()
5181
5180
  }).strict();
5182
5181
  }
5183
5182
  });
@@ -5200,6 +5199,11 @@ var init_remote_persona = __esm({
5200
5199
  // capability.id 直接存进来。旧 v1 持久化文件没此字段 → schema parse fail →
5201
5200
  // RemotePersonaStore.list 静默跳过 (alpha 阶段可接受, 老板重新 add 即可)。
5202
5201
  myCapabilityId: external_exports.string().min(1),
5202
+ // owner-id stabilization: 远端 daemon 的稳定 ownerPrincipalId(whoami.owner.id)。
5203
+ // UI DM 时作为 peerPrincipalId 传给远端 daemon(取代字面量 'owner' sentinel),
5204
+ // 让 inbox event from/to 路由 + UI useDm fromOwner 判定都用真实 id。
5205
+ // 旧持久化文件缺该字段 → schema parse fail → store.list 静默跳过(同 myCapabilityId)。
5206
+ ownerPrincipalId: external_exports.string().min(1),
5203
5207
  remotePersonaId: external_exports.string().min(1),
5204
5208
  remoteDisplayName: external_exports.string(),
5205
5209
  ownerDisplayName: external_exports.string().optional(),
@@ -5213,9 +5217,17 @@ var init_remote_persona = __esm({
5213
5217
  capabilityToken: external_exports.string().min(1),
5214
5218
  // v2 Phase 7: UI 调 preview 后从 whoami response 取 capability.id 传入
5215
5219
  myCapabilityId: external_exports.string().min(1),
5220
+ // owner-id stabilization: UI 从 whoami.owner.id 取,远端 daemon 稳定身份
5221
+ ownerPrincipalId: external_exports.string().min(1),
5216
5222
  remotePersonaId: external_exports.string().min(1),
5217
5223
  remoteDisplayName: external_exports.string(),
5218
- ownerDisplayName: external_exports.string().optional()
5224
+ ownerDisplayName: external_exports.string().optional(),
5225
+ /**
5226
+ * Person identity Phase 1: add 时必须关联到一个 Person(owner 视角的"对方是谁")。
5227
+ * daemon handler 内 atomic 写 RemotePersona + 写 PersonAliasStore
5228
+ * (alias key = ownerPrincipalId)。
5229
+ */
5230
+ personId: external_exports.string().min(1)
5219
5231
  }).strict();
5220
5232
  RemotePersonaRemoveArgsSchema = external_exports.object({
5221
5233
  alias: external_exports.string().min(1)
@@ -5223,6 +5235,75 @@ var init_remote_persona = __esm({
5223
5235
  }
5224
5236
  });
5225
5237
 
5238
+ // ../protocol/src/person.ts
5239
+ var PersonSchema, PersonAliasSchema, PersonWithLinksSchema, PersonCreateArgsSchema, PersonUpdateArgsSchema, PersonDeleteArgsSchema, PersonLinkArgsSchema, PersonListResponseSchema, PersonCreateResponseSchema, PersonUpdateResponseSchema, PersonDeleteResponseSchema, PersonLinkResponseSchema;
5240
+ var init_person = __esm({
5241
+ "../protocol/src/person.ts"() {
5242
+ "use strict";
5243
+ init_zod();
5244
+ PersonSchema = external_exports.object({
5245
+ id: external_exports.string().min(1),
5246
+ displayName: external_exports.string().min(1),
5247
+ notes: external_exports.string().optional(),
5248
+ /** 默认 true(owner 可关闭以阻断该 Person 的入站 DM) */
5249
+ dmEnabled: external_exports.boolean(),
5250
+ createdAt: external_exports.number().int().nonnegative(),
5251
+ updatedAt: external_exports.number().int().nonnegative()
5252
+ }).strict();
5253
+ PersonAliasSchema = external_exports.object({
5254
+ principalId: external_exports.string().min(1),
5255
+ personId: external_exports.string().min(1)
5256
+ }).strict();
5257
+ PersonWithLinksSchema = PersonSchema.extend({
5258
+ linkedCapabilityIds: external_exports.array(external_exports.string()),
5259
+ linkedRemoteAliases: external_exports.array(external_exports.string())
5260
+ }).strict();
5261
+ PersonCreateArgsSchema = external_exports.object({
5262
+ displayName: external_exports.string().min(1),
5263
+ notes: external_exports.string().optional(),
5264
+ /** 缺省 true(在 daemon handler 内 default) */
5265
+ dmEnabled: external_exports.boolean().optional()
5266
+ }).strict();
5267
+ PersonUpdateArgsSchema = external_exports.object({
5268
+ id: external_exports.string().min(1),
5269
+ displayName: external_exports.string().min(1).optional(),
5270
+ notes: external_exports.string().optional(),
5271
+ dmEnabled: external_exports.boolean().optional()
5272
+ }).strict();
5273
+ PersonDeleteArgsSchema = external_exports.object({
5274
+ id: external_exports.string().min(1)
5275
+ }).strict();
5276
+ PersonLinkArgsSchema = external_exports.object({
5277
+ principalId: external_exports.string().min(1),
5278
+ personId: external_exports.string().min(1)
5279
+ }).strict();
5280
+ PersonListResponseSchema = external_exports.object({
5281
+ type: external_exports.literal("person:list:ok"),
5282
+ persons: external_exports.array(PersonWithLinksSchema)
5283
+ }).strict();
5284
+ PersonCreateResponseSchema = external_exports.object({
5285
+ type: external_exports.literal("person:create:ok"),
5286
+ person: PersonSchema
5287
+ }).strict();
5288
+ PersonUpdateResponseSchema = external_exports.object({
5289
+ type: external_exports.literal("person:update:ok"),
5290
+ person: PersonSchema
5291
+ }).strict();
5292
+ PersonDeleteResponseSchema = external_exports.object({
5293
+ type: external_exports.literal("person:delete:ok"),
5294
+ /** cascade 删了哪些 capability id */
5295
+ deletedCapabilityIds: external_exports.array(external_exports.string()),
5296
+ /** cascade 移除了哪些 remote alias */
5297
+ removedRemoteAliases: external_exports.array(external_exports.string()),
5298
+ /** cascade 清掉了多少条 inbox 事件 */
5299
+ deletedInboxEvents: external_exports.number().int().nonnegative()
5300
+ }).strict();
5301
+ PersonLinkResponseSchema = external_exports.object({
5302
+ type: external_exports.literal("person:link:ok")
5303
+ }).strict();
5304
+ }
5305
+ });
5306
+
5226
5307
  // ../protocol/src/runtime.ts
5227
5308
  var init_runtime = __esm({
5228
5309
  "../protocol/src/runtime.ts"() {
@@ -5239,6 +5320,7 @@ var init_runtime = __esm({
5239
5320
  init_capability();
5240
5321
  init_inbox();
5241
5322
  init_remote_persona();
5323
+ init_person();
5242
5324
  }
5243
5325
  });
5244
5326
 
@@ -5513,8 +5595,8 @@ var require_req = __commonJS({
5513
5595
  if (req.originalUrl) {
5514
5596
  _req.url = req.originalUrl;
5515
5597
  } else {
5516
- const path37 = req.path;
5517
- _req.url = typeof path37 === "string" ? path37 : req.url ? req.url.path || req.url : void 0;
5598
+ const path39 = req.path;
5599
+ _req.url = typeof path39 === "string" ? path39 : req.url ? req.url.path || req.url : void 0;
5518
5600
  }
5519
5601
  if (req.query) {
5520
5602
  _req.query = req.query;
@@ -5679,14 +5761,14 @@ var require_redact = __commonJS({
5679
5761
  }
5680
5762
  return obj;
5681
5763
  }
5682
- function parsePath(path37) {
5764
+ function parsePath(path39) {
5683
5765
  const parts = [];
5684
5766
  let current = "";
5685
5767
  let inBrackets = false;
5686
5768
  let inQuotes = false;
5687
5769
  let quoteChar = "";
5688
- for (let i = 0; i < path37.length; i++) {
5689
- const char = path37[i];
5770
+ for (let i = 0; i < path39.length; i++) {
5771
+ const char = path39[i];
5690
5772
  if (!inBrackets && char === ".") {
5691
5773
  if (current) {
5692
5774
  parts.push(current);
@@ -5817,10 +5899,10 @@ var require_redact = __commonJS({
5817
5899
  return current;
5818
5900
  }
5819
5901
  function redactPaths(obj, paths, censor, remove = false) {
5820
- for (const path37 of paths) {
5821
- const parts = parsePath(path37);
5902
+ for (const path39 of paths) {
5903
+ const parts = parsePath(path39);
5822
5904
  if (parts.includes("*")) {
5823
- redactWildcardPath(obj, parts, censor, path37, remove);
5905
+ redactWildcardPath(obj, parts, censor, path39, remove);
5824
5906
  } else {
5825
5907
  if (remove) {
5826
5908
  removeKey(obj, parts);
@@ -5905,8 +5987,8 @@ var require_redact = __commonJS({
5905
5987
  }
5906
5988
  } else {
5907
5989
  if (afterWildcard.includes("*")) {
5908
- const wrappedCensor = typeof censor === "function" ? (value, path37) => {
5909
- const fullPath = [...pathArray.slice(0, pathLength), ...path37];
5990
+ const wrappedCensor = typeof censor === "function" ? (value, path39) => {
5991
+ const fullPath = [...pathArray.slice(0, pathLength), ...path39];
5910
5992
  return censor(value, fullPath);
5911
5993
  } : censor;
5912
5994
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -5941,8 +6023,8 @@ var require_redact = __commonJS({
5941
6023
  return null;
5942
6024
  }
5943
6025
  const pathStructure = /* @__PURE__ */ new Map();
5944
- for (const path37 of pathsToClone) {
5945
- const parts = parsePath(path37);
6026
+ for (const path39 of pathsToClone) {
6027
+ const parts = parsePath(path39);
5946
6028
  let current = pathStructure;
5947
6029
  for (let i = 0; i < parts.length; i++) {
5948
6030
  const part = parts[i];
@@ -5994,24 +6076,24 @@ var require_redact = __commonJS({
5994
6076
  }
5995
6077
  return cloneSelectively(obj, pathStructure);
5996
6078
  }
5997
- function validatePath(path37) {
5998
- if (typeof path37 !== "string") {
6079
+ function validatePath(path39) {
6080
+ if (typeof path39 !== "string") {
5999
6081
  throw new Error("Paths must be (non-empty) strings");
6000
6082
  }
6001
- if (path37 === "") {
6083
+ if (path39 === "") {
6002
6084
  throw new Error("Invalid redaction path ()");
6003
6085
  }
6004
- if (path37.includes("..")) {
6005
- throw new Error(`Invalid redaction path (${path37})`);
6086
+ if (path39.includes("..")) {
6087
+ throw new Error(`Invalid redaction path (${path39})`);
6006
6088
  }
6007
- if (path37.includes(",")) {
6008
- throw new Error(`Invalid redaction path (${path37})`);
6089
+ if (path39.includes(",")) {
6090
+ throw new Error(`Invalid redaction path (${path39})`);
6009
6091
  }
6010
6092
  let bracketCount = 0;
6011
6093
  let inQuotes = false;
6012
6094
  let quoteChar = "";
6013
- for (let i = 0; i < path37.length; i++) {
6014
- const char = path37[i];
6095
+ for (let i = 0; i < path39.length; i++) {
6096
+ const char = path39[i];
6015
6097
  if ((char === '"' || char === "'") && bracketCount > 0) {
6016
6098
  if (!inQuotes) {
6017
6099
  inQuotes = true;
@@ -6025,20 +6107,20 @@ var require_redact = __commonJS({
6025
6107
  } else if (char === "]" && !inQuotes) {
6026
6108
  bracketCount--;
6027
6109
  if (bracketCount < 0) {
6028
- throw new Error(`Invalid redaction path (${path37})`);
6110
+ throw new Error(`Invalid redaction path (${path39})`);
6029
6111
  }
6030
6112
  }
6031
6113
  }
6032
6114
  if (bracketCount !== 0) {
6033
- throw new Error(`Invalid redaction path (${path37})`);
6115
+ throw new Error(`Invalid redaction path (${path39})`);
6034
6116
  }
6035
6117
  }
6036
6118
  function validatePaths(paths) {
6037
6119
  if (!Array.isArray(paths)) {
6038
6120
  throw new TypeError("paths must be an array");
6039
6121
  }
6040
- for (const path37 of paths) {
6041
- validatePath(path37);
6122
+ for (const path39 of paths) {
6123
+ validatePath(path39);
6042
6124
  }
6043
6125
  }
6044
6126
  function slowRedact(options = {}) {
@@ -6206,8 +6288,8 @@ var require_redaction = __commonJS({
6206
6288
  if (shape[k2] === null) {
6207
6289
  o[k2] = (value) => topCensor(value, [k2]);
6208
6290
  } else {
6209
- const wrappedCensor = typeof censor === "function" ? (value, path37) => {
6210
- return censor(value, [k2, ...path37]);
6291
+ const wrappedCensor = typeof censor === "function" ? (value, path39) => {
6292
+ return censor(value, [k2, ...path39]);
6211
6293
  } : censor;
6212
6294
  o[k2] = Redact({
6213
6295
  paths: shape[k2],
@@ -6425,10 +6507,10 @@ var require_atomic_sleep = __commonJS({
6425
6507
  var require_sonic_boom = __commonJS({
6426
6508
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6427
6509
  "use strict";
6428
- var fs33 = require("fs");
6510
+ var fs35 = require("fs");
6429
6511
  var EventEmitter2 = require("events");
6430
6512
  var inherits = require("util").inherits;
6431
- var path37 = require("path");
6513
+ var path39 = require("path");
6432
6514
  var sleep = require_atomic_sleep();
6433
6515
  var assert = require("assert");
6434
6516
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6482,20 +6564,20 @@ var require_sonic_boom = __commonJS({
6482
6564
  const mode = sonic.mode;
6483
6565
  if (sonic.sync) {
6484
6566
  try {
6485
- if (sonic.mkdir) fs33.mkdirSync(path37.dirname(file), { recursive: true });
6486
- const fd = fs33.openSync(file, flags, mode);
6567
+ if (sonic.mkdir) fs35.mkdirSync(path39.dirname(file), { recursive: true });
6568
+ const fd = fs35.openSync(file, flags, mode);
6487
6569
  fileOpened(null, fd);
6488
6570
  } catch (err) {
6489
6571
  fileOpened(err);
6490
6572
  throw err;
6491
6573
  }
6492
6574
  } else if (sonic.mkdir) {
6493
- fs33.mkdir(path37.dirname(file), { recursive: true }, (err) => {
6575
+ fs35.mkdir(path39.dirname(file), { recursive: true }, (err) => {
6494
6576
  if (err) return fileOpened(err);
6495
- fs33.open(file, flags, mode, fileOpened);
6577
+ fs35.open(file, flags, mode, fileOpened);
6496
6578
  });
6497
6579
  } else {
6498
- fs33.open(file, flags, mode, fileOpened);
6580
+ fs35.open(file, flags, mode, fileOpened);
6499
6581
  }
6500
6582
  }
6501
6583
  function SonicBoom(opts) {
@@ -6536,8 +6618,8 @@ var require_sonic_boom = __commonJS({
6536
6618
  this.flush = flushBuffer;
6537
6619
  this.flushSync = flushBufferSync;
6538
6620
  this._actualWrite = actualWriteBuffer;
6539
- fsWriteSync = () => fs33.writeSync(this.fd, this._writingBuf);
6540
- fsWrite = () => fs33.write(this.fd, this._writingBuf, this.release);
6621
+ fsWriteSync = () => fs35.writeSync(this.fd, this._writingBuf);
6622
+ fsWrite = () => fs35.write(this.fd, this._writingBuf, this.release);
6541
6623
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6542
6624
  this._writingBuf = "";
6543
6625
  this.write = write;
@@ -6546,15 +6628,15 @@ var require_sonic_boom = __commonJS({
6546
6628
  this._actualWrite = actualWrite;
6547
6629
  fsWriteSync = () => {
6548
6630
  if (Buffer.isBuffer(this._writingBuf)) {
6549
- return fs33.writeSync(this.fd, this._writingBuf);
6631
+ return fs35.writeSync(this.fd, this._writingBuf);
6550
6632
  }
6551
- return fs33.writeSync(this.fd, this._writingBuf, "utf8");
6633
+ return fs35.writeSync(this.fd, this._writingBuf, "utf8");
6552
6634
  };
6553
6635
  fsWrite = () => {
6554
6636
  if (Buffer.isBuffer(this._writingBuf)) {
6555
- return fs33.write(this.fd, this._writingBuf, this.release);
6637
+ return fs35.write(this.fd, this._writingBuf, this.release);
6556
6638
  }
6557
- return fs33.write(this.fd, this._writingBuf, "utf8", this.release);
6639
+ return fs35.write(this.fd, this._writingBuf, "utf8", this.release);
6558
6640
  };
6559
6641
  } else {
6560
6642
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6611,7 +6693,7 @@ var require_sonic_boom = __commonJS({
6611
6693
  }
6612
6694
  }
6613
6695
  if (this._fsync) {
6614
- fs33.fsyncSync(this.fd);
6696
+ fs35.fsyncSync(this.fd);
6615
6697
  }
6616
6698
  const len = this._len;
6617
6699
  if (this._reopening) {
@@ -6725,7 +6807,7 @@ var require_sonic_boom = __commonJS({
6725
6807
  const onDrain = () => {
6726
6808
  if (!this._fsync) {
6727
6809
  try {
6728
- fs33.fsync(this.fd, (err) => {
6810
+ fs35.fsync(this.fd, (err) => {
6729
6811
  this._flushPending = false;
6730
6812
  cb(err);
6731
6813
  });
@@ -6827,7 +6909,7 @@ var require_sonic_boom = __commonJS({
6827
6909
  const fd = this.fd;
6828
6910
  this.once("ready", () => {
6829
6911
  if (fd !== this.fd) {
6830
- fs33.close(fd, (err) => {
6912
+ fs35.close(fd, (err) => {
6831
6913
  if (err) {
6832
6914
  return this.emit("error", err);
6833
6915
  }
@@ -6876,7 +6958,7 @@ var require_sonic_boom = __commonJS({
6876
6958
  buf = this._bufs[0];
6877
6959
  }
6878
6960
  try {
6879
- const n = Buffer.isBuffer(buf) ? fs33.writeSync(this.fd, buf) : fs33.writeSync(this.fd, buf, "utf8");
6961
+ const n = Buffer.isBuffer(buf) ? fs35.writeSync(this.fd, buf) : fs35.writeSync(this.fd, buf, "utf8");
6880
6962
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6881
6963
  buf = releasedBufObj.writingBuf;
6882
6964
  this._len = releasedBufObj.len;
@@ -6892,7 +6974,7 @@ var require_sonic_boom = __commonJS({
6892
6974
  }
6893
6975
  }
6894
6976
  try {
6895
- fs33.fsyncSync(this.fd);
6977
+ fs35.fsyncSync(this.fd);
6896
6978
  } catch {
6897
6979
  }
6898
6980
  }
@@ -6913,7 +6995,7 @@ var require_sonic_boom = __commonJS({
6913
6995
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6914
6996
  }
6915
6997
  try {
6916
- const n = fs33.writeSync(this.fd, buf);
6998
+ const n = fs35.writeSync(this.fd, buf);
6917
6999
  buf = buf.subarray(n);
6918
7000
  this._len = Math.max(this._len - n, 0);
6919
7001
  if (buf.length <= 0) {
@@ -6941,13 +7023,13 @@ var require_sonic_boom = __commonJS({
6941
7023
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
6942
7024
  if (this.sync) {
6943
7025
  try {
6944
- const written = Buffer.isBuffer(this._writingBuf) ? fs33.writeSync(this.fd, this._writingBuf) : fs33.writeSync(this.fd, this._writingBuf, "utf8");
7026
+ const written = Buffer.isBuffer(this._writingBuf) ? fs35.writeSync(this.fd, this._writingBuf) : fs35.writeSync(this.fd, this._writingBuf, "utf8");
6945
7027
  release(null, written);
6946
7028
  } catch (err) {
6947
7029
  release(err);
6948
7030
  }
6949
7031
  } else {
6950
- fs33.write(this.fd, this._writingBuf, release);
7032
+ fs35.write(this.fd, this._writingBuf, release);
6951
7033
  }
6952
7034
  }
6953
7035
  function actualWriteBuffer() {
@@ -6956,7 +7038,7 @@ var require_sonic_boom = __commonJS({
6956
7038
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
6957
7039
  if (this.sync) {
6958
7040
  try {
6959
- const written = fs33.writeSync(this.fd, this._writingBuf);
7041
+ const written = fs35.writeSync(this.fd, this._writingBuf);
6960
7042
  release(null, written);
6961
7043
  } catch (err) {
6962
7044
  release(err);
@@ -6965,7 +7047,7 @@ var require_sonic_boom = __commonJS({
6965
7047
  if (kCopyBuffer) {
6966
7048
  this._writingBuf = Buffer.from(this._writingBuf);
6967
7049
  }
6968
- fs33.write(this.fd, this._writingBuf, release);
7050
+ fs35.write(this.fd, this._writingBuf, release);
6969
7051
  }
6970
7052
  }
6971
7053
  function actualClose(sonic) {
@@ -6981,12 +7063,12 @@ var require_sonic_boom = __commonJS({
6981
7063
  sonic._lens = [];
6982
7064
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
6983
7065
  try {
6984
- fs33.fsync(sonic.fd, closeWrapped);
7066
+ fs35.fsync(sonic.fd, closeWrapped);
6985
7067
  } catch {
6986
7068
  }
6987
7069
  function closeWrapped() {
6988
7070
  if (sonic.fd !== 1 && sonic.fd !== 2) {
6989
- fs33.close(sonic.fd, done);
7071
+ fs35.close(sonic.fd, done);
6990
7072
  } else {
6991
7073
  done();
6992
7074
  }
@@ -7243,7 +7325,7 @@ var require_thread_stream = __commonJS({
7243
7325
  var { version: version2 } = require_package();
7244
7326
  var { EventEmitter: EventEmitter2 } = require("events");
7245
7327
  var { Worker } = require("worker_threads");
7246
- var { join: join10 } = require("path");
7328
+ var { join: join12 } = require("path");
7247
7329
  var { pathToFileURL } = require("url");
7248
7330
  var { wait } = require_wait();
7249
7331
  var {
@@ -7279,7 +7361,7 @@ var require_thread_stream = __commonJS({
7279
7361
  function createWorker(stream, opts) {
7280
7362
  const { filename, workerData } = opts;
7281
7363
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7282
- const toExecute = bundlerOverrides["thread-stream-worker"] || join10(__dirname, "lib", "worker.js");
7364
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7283
7365
  const worker = new Worker(toExecute, {
7284
7366
  ...opts.workerOpts,
7285
7367
  trackUnmanagedFds: false,
@@ -7665,7 +7747,7 @@ var require_transport = __commonJS({
7665
7747
  "use strict";
7666
7748
  var { createRequire } = require("module");
7667
7749
  var getCallers = require_caller();
7668
- var { join: join10, isAbsolute, sep: sep2 } = require("path");
7750
+ var { join: join12, isAbsolute, sep: sep2 } = require("path");
7669
7751
  var sleep = require_atomic_sleep();
7670
7752
  var onExit = require_on_exit_leak_free();
7671
7753
  var ThreadStream = require_thread_stream();
@@ -7728,7 +7810,7 @@ var require_transport = __commonJS({
7728
7810
  throw new Error("only one of target or targets can be specified");
7729
7811
  }
7730
7812
  if (targets) {
7731
- target = bundlerOverrides["pino-worker"] || join10(__dirname, "worker.js");
7813
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
7732
7814
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
7733
7815
  return {
7734
7816
  ...dest,
@@ -7746,7 +7828,7 @@ var require_transport = __commonJS({
7746
7828
  });
7747
7829
  });
7748
7830
  } else if (pipeline2) {
7749
- target = bundlerOverrides["pino-worker"] || join10(__dirname, "worker.js");
7831
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
7750
7832
  options.pipelines = [pipeline2.map((dest) => {
7751
7833
  return {
7752
7834
  ...dest,
@@ -7768,7 +7850,7 @@ var require_transport = __commonJS({
7768
7850
  return origin;
7769
7851
  }
7770
7852
  if (origin === "pino/file") {
7771
- return join10(__dirname, "..", "file.js");
7853
+ return join12(__dirname, "..", "file.js");
7772
7854
  }
7773
7855
  let fixTarget2;
7774
7856
  for (const filePath of callers) {
@@ -8758,7 +8840,7 @@ var require_safe_stable_stringify = __commonJS({
8758
8840
  return circularValue;
8759
8841
  }
8760
8842
  let res = "";
8761
- let join10 = ",";
8843
+ let join12 = ",";
8762
8844
  const originalIndentation = indentation;
8763
8845
  if (Array.isArray(value)) {
8764
8846
  if (value.length === 0) {
@@ -8772,7 +8854,7 @@ var require_safe_stable_stringify = __commonJS({
8772
8854
  indentation += spacer;
8773
8855
  res += `
8774
8856
  ${indentation}`;
8775
- join10 = `,
8857
+ join12 = `,
8776
8858
  ${indentation}`;
8777
8859
  }
8778
8860
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8780,13 +8862,13 @@ ${indentation}`;
8780
8862
  for (; i < maximumValuesToStringify - 1; i++) {
8781
8863
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8782
8864
  res += tmp2 !== void 0 ? tmp2 : "null";
8783
- res += join10;
8865
+ res += join12;
8784
8866
  }
8785
8867
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8786
8868
  res += tmp !== void 0 ? tmp : "null";
8787
8869
  if (value.length - 1 > maximumBreadth) {
8788
8870
  const removedKeys = value.length - maximumBreadth - 1;
8789
- res += `${join10}"... ${getItemCount(removedKeys)} not stringified"`;
8871
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
8790
8872
  }
8791
8873
  if (spacer !== "") {
8792
8874
  res += `
@@ -8807,7 +8889,7 @@ ${originalIndentation}`;
8807
8889
  let separator = "";
8808
8890
  if (spacer !== "") {
8809
8891
  indentation += spacer;
8810
- join10 = `,
8892
+ join12 = `,
8811
8893
  ${indentation}`;
8812
8894
  whitespace = " ";
8813
8895
  }
@@ -8821,13 +8903,13 @@ ${indentation}`;
8821
8903
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
8822
8904
  if (tmp !== void 0) {
8823
8905
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8824
- separator = join10;
8906
+ separator = join12;
8825
8907
  }
8826
8908
  }
8827
8909
  if (keyLength > maximumBreadth) {
8828
8910
  const removedKeys = keyLength - maximumBreadth;
8829
8911
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
8830
- separator = join10;
8912
+ separator = join12;
8831
8913
  }
8832
8914
  if (spacer !== "" && separator.length > 1) {
8833
8915
  res = `
@@ -8868,7 +8950,7 @@ ${originalIndentation}`;
8868
8950
  }
8869
8951
  const originalIndentation = indentation;
8870
8952
  let res = "";
8871
- let join10 = ",";
8953
+ let join12 = ",";
8872
8954
  if (Array.isArray(value)) {
8873
8955
  if (value.length === 0) {
8874
8956
  return "[]";
@@ -8881,7 +8963,7 @@ ${originalIndentation}`;
8881
8963
  indentation += spacer;
8882
8964
  res += `
8883
8965
  ${indentation}`;
8884
- join10 = `,
8966
+ join12 = `,
8885
8967
  ${indentation}`;
8886
8968
  }
8887
8969
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8889,13 +8971,13 @@ ${indentation}`;
8889
8971
  for (; i < maximumValuesToStringify - 1; i++) {
8890
8972
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8891
8973
  res += tmp2 !== void 0 ? tmp2 : "null";
8892
- res += join10;
8974
+ res += join12;
8893
8975
  }
8894
8976
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8895
8977
  res += tmp !== void 0 ? tmp : "null";
8896
8978
  if (value.length - 1 > maximumBreadth) {
8897
8979
  const removedKeys = value.length - maximumBreadth - 1;
8898
- res += `${join10}"... ${getItemCount(removedKeys)} not stringified"`;
8980
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
8899
8981
  }
8900
8982
  if (spacer !== "") {
8901
8983
  res += `
@@ -8908,7 +8990,7 @@ ${originalIndentation}`;
8908
8990
  let whitespace = "";
8909
8991
  if (spacer !== "") {
8910
8992
  indentation += spacer;
8911
- join10 = `,
8993
+ join12 = `,
8912
8994
  ${indentation}`;
8913
8995
  whitespace = " ";
8914
8996
  }
@@ -8917,7 +8999,7 @@ ${indentation}`;
8917
8999
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
8918
9000
  if (tmp !== void 0) {
8919
9001
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8920
- separator = join10;
9002
+ separator = join12;
8921
9003
  }
8922
9004
  }
8923
9005
  if (spacer !== "" && separator.length > 1) {
@@ -8975,20 +9057,20 @@ ${originalIndentation}`;
8975
9057
  indentation += spacer;
8976
9058
  let res2 = `
8977
9059
  ${indentation}`;
8978
- const join11 = `,
9060
+ const join13 = `,
8979
9061
  ${indentation}`;
8980
9062
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
8981
9063
  let i = 0;
8982
9064
  for (; i < maximumValuesToStringify - 1; i++) {
8983
9065
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8984
9066
  res2 += tmp2 !== void 0 ? tmp2 : "null";
8985
- res2 += join11;
9067
+ res2 += join13;
8986
9068
  }
8987
9069
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
8988
9070
  res2 += tmp !== void 0 ? tmp : "null";
8989
9071
  if (value.length - 1 > maximumBreadth) {
8990
9072
  const removedKeys = value.length - maximumBreadth - 1;
8991
- res2 += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
9073
+ res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
8992
9074
  }
8993
9075
  res2 += `
8994
9076
  ${originalIndentation}`;
@@ -9004,16 +9086,16 @@ ${originalIndentation}`;
9004
9086
  return '"[Object]"';
9005
9087
  }
9006
9088
  indentation += spacer;
9007
- const join10 = `,
9089
+ const join12 = `,
9008
9090
  ${indentation}`;
9009
9091
  let res = "";
9010
9092
  let separator = "";
9011
9093
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9012
9094
  if (isTypedArrayWithEntries(value)) {
9013
- res += stringifyTypedArray(value, join10, maximumBreadth);
9095
+ res += stringifyTypedArray(value, join12, maximumBreadth);
9014
9096
  keys = keys.slice(value.length);
9015
9097
  maximumPropertiesToStringify -= value.length;
9016
- separator = join10;
9098
+ separator = join12;
9017
9099
  }
9018
9100
  if (deterministic) {
9019
9101
  keys = sort(keys, comparator);
@@ -9024,13 +9106,13 @@ ${indentation}`;
9024
9106
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9025
9107
  if (tmp !== void 0) {
9026
9108
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9027
- separator = join10;
9109
+ separator = join12;
9028
9110
  }
9029
9111
  }
9030
9112
  if (keyLength > maximumBreadth) {
9031
9113
  const removedKeys = keyLength - maximumBreadth;
9032
9114
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9033
- separator = join10;
9115
+ separator = join12;
9034
9116
  }
9035
9117
  if (separator !== "") {
9036
9118
  res = `
@@ -10121,11 +10203,11 @@ var init_lib = __esm({
10121
10203
  }
10122
10204
  }
10123
10205
  },
10124
- addToPath: function addToPath(path37, added, removed, oldPosInc, options) {
10125
- var last = path37.lastComponent;
10206
+ addToPath: function addToPath(path39, added, removed, oldPosInc, options) {
10207
+ var last = path39.lastComponent;
10126
10208
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10127
10209
  return {
10128
- oldPos: path37.oldPos + oldPosInc,
10210
+ oldPos: path39.oldPos + oldPosInc,
10129
10211
  lastComponent: {
10130
10212
  count: last.count + 1,
10131
10213
  added,
@@ -10135,7 +10217,7 @@ var init_lib = __esm({
10135
10217
  };
10136
10218
  } else {
10137
10219
  return {
10138
- oldPos: path37.oldPos + oldPosInc,
10220
+ oldPos: path39.oldPos + oldPosInc,
10139
10221
  lastComponent: {
10140
10222
  count: 1,
10141
10223
  added,
@@ -10566,10 +10648,10 @@ function attachmentToHistoryMessage(o, ts) {
10566
10648
  const memories = raw.map((m2) => {
10567
10649
  if (!m2 || typeof m2 !== "object") return null;
10568
10650
  const rec = m2;
10569
- const path37 = typeof rec.path === "string" ? rec.path : null;
10651
+ const path39 = typeof rec.path === "string" ? rec.path : null;
10570
10652
  const content = typeof rec.content === "string" ? rec.content : null;
10571
- if (!path37 || content == null) return null;
10572
- const entry = { path: path37, content };
10653
+ if (!path39 || content == null) return null;
10654
+ const entry = { path: path39, content };
10573
10655
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10574
10656
  return entry;
10575
10657
  }).filter((m2) => m2 !== null);
@@ -11395,10 +11477,10 @@ function parseAttachment(obj) {
11395
11477
  const memories = raw.map((m2) => {
11396
11478
  if (!m2 || typeof m2 !== "object") return null;
11397
11479
  const rec = m2;
11398
- const path37 = typeof rec.path === "string" ? rec.path : null;
11480
+ const path39 = typeof rec.path === "string" ? rec.path : null;
11399
11481
  const content = typeof rec.content === "string" ? rec.content : null;
11400
- if (!path37 || content == null) return null;
11401
- const out = { path: path37, content };
11482
+ if (!path39 || content == null) return null;
11483
+ const out = { path: path39, content };
11402
11484
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11403
11485
  return out;
11404
11486
  }).filter((m2) => m2 !== null);
@@ -18897,7 +18979,7 @@ var require_websocket = __commonJS({
18897
18979
  var http2 = require("http");
18898
18980
  var net = require("net");
18899
18981
  var tls = require("tls");
18900
- var { randomBytes: randomBytes3, createHash: createHash4 } = require("crypto");
18982
+ var { randomBytes: randomBytes3, createHash: createHash3 } = require("crypto");
18901
18983
  var { Duplex, Readable: Readable3 } = require("stream");
18902
18984
  var { URL: URL2 } = require("url");
18903
18985
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19557,7 +19639,7 @@ var require_websocket = __commonJS({
19557
19639
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19558
19640
  return;
19559
19641
  }
19560
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
19642
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
19561
19643
  if (res.headers["sec-websocket-accept"] !== digest) {
19562
19644
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19563
19645
  return;
@@ -19924,7 +20006,7 @@ var require_websocket_server = __commonJS({
19924
20006
  var EventEmitter2 = require("events");
19925
20007
  var http2 = require("http");
19926
20008
  var { Duplex } = require("stream");
19927
- var { createHash: createHash4 } = require("crypto");
20009
+ var { createHash: createHash3 } = require("crypto");
19928
20010
  var extension2 = require_extension();
19929
20011
  var PerMessageDeflate2 = require_permessage_deflate();
19930
20012
  var subprotocol2 = require_subprotocol();
@@ -20225,7 +20307,7 @@ var require_websocket_server = __commonJS({
20225
20307
  );
20226
20308
  }
20227
20309
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20228
- const digest = createHash4("sha1").update(key + GUID).digest("base64");
20310
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
20229
20311
  const headers = [
20230
20312
  "HTTP/1.1 101 Switching Protocols",
20231
20313
  "Upgrade: websocket",
@@ -22551,7 +22633,7 @@ var SessionManager = class {
22551
22633
  }
22552
22634
  }
22553
22635
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
22554
- create(args) {
22636
+ create(args, creatorPrincipalId) {
22555
22637
  let cwd;
22556
22638
  if (args.ownerPersonaId) {
22557
22639
  cwd = derivePersonaSpawnCwd(
@@ -22592,6 +22674,7 @@ var SessionManager = class {
22592
22674
  iconKey: args.iconKey,
22593
22675
  forkedFromSessionId: args.forkedFromSessionId,
22594
22676
  ownerPersonaId: args.ownerPersonaId,
22677
+ creatorPrincipalId,
22595
22678
  createdAt: iso,
22596
22679
  updatedAt: iso
22597
22680
  };
@@ -23536,6 +23619,10 @@ var PersonaStore = class {
23536
23619
  const p2 = this.metaPath(personaId);
23537
23620
  if (!fs6.existsSync(p2)) return null;
23538
23621
  const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23622
+ if (raw && typeof raw === "object" && "tokenMap" in raw) {
23623
+ delete raw.tokenMap;
23624
+ this.atomicWrite(p2, JSON.stringify(raw, null, 2));
23625
+ }
23539
23626
  return PersonaFileSchema.parse(raw);
23540
23627
  }
23541
23628
  has(personaId) {
@@ -23591,12 +23678,19 @@ var PersonaRegistry = class {
23591
23678
  }
23592
23679
  store;
23593
23680
  cache = /* @__PURE__ */ new Map();
23594
- /** 从 store 全量重建缓存(boot 时 + 测试 setup 时) */
23681
+ /**
23682
+ * 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
23683
+ * 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
23684
+ * try/catch 隔离,让其它 persona 仍能加载。
23685
+ */
23595
23686
  reload() {
23596
23687
  this.cache.clear();
23597
23688
  for (const id of this.store.list()) {
23598
- const p2 = this.store.read(id);
23599
- if (p2) this.cache.set(p2.personaId, p2);
23689
+ try {
23690
+ const p2 = this.store.read(id);
23691
+ if (p2) this.cache.set(p2.personaId, p2);
23692
+ } catch {
23693
+ }
23600
23694
  }
23601
23695
  }
23602
23696
  get(personaId) {
@@ -23612,21 +23706,6 @@ var PersonaRegistry = class {
23612
23706
  list() {
23613
23707
  return Array.from(this.cache.values());
23614
23708
  }
23615
- /**
23616
- * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23617
- * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23618
- * persona 数量通常 < 100,性能可忽略。
23619
- */
23620
- findByToken(token) {
23621
- if (!token) return null;
23622
- for (const persona of this.cache.values()) {
23623
- if (!persona.public) continue;
23624
- const entry = persona.tokenMap?.[token];
23625
- if (!entry || entry.revoked) continue;
23626
- return { personaId: persona.personaId, label: entry.label };
23627
- }
23628
- return null;
23629
- }
23630
23709
  };
23631
23710
 
23632
23711
  // src/persona/manager.ts
@@ -23920,9 +23999,6 @@ var PersonaManager = class {
23920
23999
  model: args.model,
23921
24000
  public: args.public ?? false,
23922
24001
  iconKey: args.iconKey,
23923
- // 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
23924
- // file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
23925
- tokenMap: {},
23926
24002
  createdAt: now,
23927
24003
  updatedAt: now
23928
24004
  };
@@ -23981,46 +24057,6 @@ var PersonaManager = class {
23981
24057
  this.deps.store.remove(personaId);
23982
24058
  this.deps.registry.remove(personaId);
23983
24059
  }
23984
- /**
23985
- * 生成 32-char base64url token 并写入 tokenMap,给 file-sharing HTTP Bearer
23986
- * 鉴权用(PersonaRegistry.findByToken 反查 personaId/label)。
23987
- */
23988
- issueToken(personaId, label) {
23989
- const existing = this.deps.registry.get(personaId);
23990
- if (!existing) throw new Error(`persona not found: ${personaId}`);
23991
- const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
23992
- const entry = {
23993
- label,
23994
- issuedAt: Date.now(),
23995
- revoked: false
23996
- };
23997
- const updated = {
23998
- ...existing,
23999
- tokenMap: { ...existing.tokenMap ?? {}, [token]: entry },
24000
- updatedAt: Date.now()
24001
- };
24002
- this.deps.store.writeMeta(updated);
24003
- this.deps.registry.set(updated);
24004
- return { token, persona: updated };
24005
- }
24006
- /** 标记 token 为 revoked(保留条目用于审计);不存在的 token 抛错。 */
24007
- revokeToken(personaId, token) {
24008
- const existing = this.deps.registry.get(personaId);
24009
- if (!existing) throw new Error(`persona not found: ${personaId}`);
24010
- const tokenEntry = existing.tokenMap?.[token];
24011
- if (!tokenEntry) throw new Error(`token not found in persona ${personaId}`);
24012
- const updated = {
24013
- ...existing,
24014
- tokenMap: {
24015
- ...existing.tokenMap ?? {},
24016
- [token]: { ...tokenEntry, revoked: true }
24017
- },
24018
- updatedAt: Date.now()
24019
- };
24020
- this.deps.store.writeMeta(updated);
24021
- this.deps.registry.set(updated);
24022
- return updated;
24023
- }
24024
24060
  /**
24025
24061
  * label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
24026
24062
  * base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
@@ -24126,7 +24162,6 @@ function seedDefaultPersonas(args) {
24126
24162
  model: entry.model,
24127
24163
  public: entry.public,
24128
24164
  iconKey: entry.iconKey,
24129
- tokenMap: {},
24130
24165
  createdAt: now,
24131
24166
  updatedAt: now
24132
24167
  };
@@ -25615,7 +25650,7 @@ var LocalWsServer = class {
25615
25650
  }
25616
25651
  }
25617
25652
  // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25618
- // 用于 capability:tokenIssued / capability:revoked 等 owner-only push frame——
25653
+ // 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
25619
25654
  // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25620
25655
  broadcastToOwners(frame) {
25621
25656
  const gate = this.opts.authGate;
@@ -25625,12 +25660,27 @@ var LocalWsServer = class {
25625
25660
  this.safeSend(c.ws, frame);
25626
25661
  }
25627
25662
  }
25663
+ // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
25664
+ // - 所有 owner ws (owner 看自家所有 channel)
25665
+ // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
25666
+ // 一次调用同时推两端, inbox:event push 帧由此走.
25667
+ broadcastToCapabilityChannel(capabilityId, frame) {
25668
+ this.broadcastToOwners(frame);
25669
+ const set = this.capabilityIdToClients.get(capabilityId);
25670
+ if (!set) return;
25671
+ for (const id of set) {
25672
+ const c = this.clients.get(id);
25673
+ if (!c) continue;
25674
+ this.safeSend(c.ws, frame);
25675
+ }
25676
+ }
25628
25677
  // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
25629
- // principalId === 'owner' → 同 broadcastToOwners (所有 owner ws)
25630
- // principalId === 'cap_xxx' capability 的所有 guest ws (多端 / tab)
25678
+ // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
25679
+ // principalId === 'owner' sentinel 同上(向后兼容遗留 caller / 协议层 sentinel)
25680
+ // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
25631
25681
  // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
25632
25682
  broadcastToPrincipal(principalId, frame) {
25633
- if (principalId === "owner") {
25683
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
25634
25684
  this.broadcastToOwners(frame);
25635
25685
  return;
25636
25686
  }
@@ -25793,6 +25843,11 @@ var LocalWsServer = class {
25793
25843
  return;
25794
25844
  }
25795
25845
  } else if (frame.type === "auth") {
25846
+ const token = typeof frame.token === "string" ? frame.token ?? "" : "";
25847
+ if (token && this.opts.tryVerifyCapabilityToken) {
25848
+ const guestCtx = this.opts.tryVerifyCapabilityToken(token);
25849
+ if (guestCtx) this.attachClientContext(client.id, guestCtx);
25850
+ }
25796
25851
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
25797
25852
  return;
25798
25853
  }
@@ -25911,7 +25966,7 @@ var AuthGate = class {
25911
25966
  }
25912
25967
  let ctx = null;
25913
25968
  if (this.opts.authenticate) {
25914
- const r = this.opts.authenticate(parsed.data.token);
25969
+ const r = this.opts.authenticate(parsed.data.token, parsed.data.selfPrincipalId);
25915
25970
  if (!r.ok) {
25916
25971
  this.opts.closeConnection(handle, 4401, r.code);
25917
25972
  this.markFailed(handle.id);
@@ -25973,7 +26028,7 @@ var AuthContextResolver = class {
25973
26028
  }
25974
26029
  opts;
25975
26030
  /**
25976
- * 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26031
+ * 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
25977
26032
  * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
25978
26033
  */
25979
26034
  resolveFromHeader(authHeader, remoteAddress) {
@@ -25983,15 +26038,6 @@ var AuthContextResolver = class {
25983
26038
  if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
25984
26039
  return { role: "owner", isLoopback };
25985
26040
  }
25986
- const personalHit = this.opts.personaRegistry.findByToken(token);
25987
- if (personalHit) {
25988
- return {
25989
- role: "personal",
25990
- personaId: personalHit.personaId,
25991
- label: personalHit.label,
25992
- isLoopback
25993
- };
25994
- }
25995
26041
  return null;
25996
26042
  }
25997
26043
  };
@@ -26017,9 +26063,9 @@ function constantTimeEqual2(a, b2) {
26017
26063
  init_runtime();
26018
26064
 
26019
26065
  // src/transport/connection-context.ts
26020
- function ownerContext() {
26066
+ function ownerContext(ownerPrincipalId, displayName) {
26021
26067
  return {
26022
- principal: OWNER_PRINCIPAL,
26068
+ principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
26023
26069
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
26024
26070
  };
26025
26071
  }
@@ -26032,7 +26078,9 @@ function guestContext(cap) {
26032
26078
  }
26033
26079
  function authenticate(token, deps) {
26034
26080
  if (!token) return { ok: false, code: "NO_TOKEN" };
26035
- if (deps.isOwnerToken(token)) return { ok: true, context: ownerContext() };
26081
+ if (deps.isOwnerToken(token)) {
26082
+ return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
26083
+ }
26036
26084
  if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
26037
26085
  const v2 = deps.capabilityRegistry.verifyToken(token);
26038
26086
  if (v2.ok) return { ok: true, context: guestContext(v2.capability) };
@@ -26137,7 +26185,6 @@ var CapabilityRegistry = class {
26137
26185
  const hash = sha256Hex(token);
26138
26186
  const cap = this.bySecretHash.get(hash);
26139
26187
  if (!cap) return { ok: false, code: "TOKEN_INVALID" };
26140
- if (cap.revokedAt) return { ok: false, code: "TOKEN_REVOKED" };
26141
26188
  if (cap.expiresAt && this.now() >= cap.expiresAt) {
26142
26189
  return { ok: false, code: "TOKEN_EXPIRED" };
26143
26190
  }
@@ -26153,18 +26200,37 @@ var CapabilityRegistry = class {
26153
26200
  this.bySecretHash.set(cap.secretHash, cap);
26154
26201
  this.store.upsert(cap);
26155
26202
  }
26156
- markRevoked(id, revokedAt) {
26203
+ /**
26204
+ * Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
26205
+ * 后续 verifyToken 该 token 走 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
26206
+ * idempotent:不存在 → null。
26207
+ */
26208
+ delete(id) {
26157
26209
  const current = this.store.list().find((c) => c.id === id);
26158
26210
  if (!current) return null;
26159
- if (current.revokedAt) return current;
26160
- const updated = { ...current, revokedAt };
26161
- this.bySecretHash.set(updated.secretHash, updated);
26162
- this.store.upsert(updated);
26163
- return updated;
26211
+ this.bySecretHash.delete(current.secretHash);
26212
+ this.store.remove(id);
26213
+ return current;
26164
26214
  }
26165
26215
  findById(id) {
26166
26216
  return this.store.list().find((c) => c.id === id) ?? null;
26167
26217
  }
26218
+ /**
26219
+ * Person identity Phase 1 (B11): guest authenticate 上报 selfPrincipalId 时
26220
+ * daemon 把它落到 cap.linkedPrincipalId(之前 undefined)。设计上 immutable:
26221
+ * 只允许 undefined → set;已设置后再调本方法 noop(reject 留给 caller 判断)。
26222
+ *
26223
+ * 返回 true = 实际写入;false = 不存在 / 已设置过(caller 用此区分用户行为)。
26224
+ */
26225
+ updateLinkedPrincipalId(id, principalId) {
26226
+ const current = this.store.list().find((c) => c.id === id);
26227
+ if (!current) return false;
26228
+ if (current.linkedPrincipalId !== void 0) return false;
26229
+ const updated = { ...current, linkedPrincipalId: principalId };
26230
+ this.bySecretHash.set(updated.secretHash, updated);
26231
+ this.store.upsert(updated);
26232
+ return true;
26233
+ }
26168
26234
  };
26169
26235
  function sha256Hex(s) {
26170
26236
  return crypto4.createHash("sha256").update(s).digest("hex");
@@ -26205,20 +26271,38 @@ var CapabilityManager = class {
26205
26271
  ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
26206
26272
  };
26207
26273
  this.registry.upsertCapability(cap);
26274
+ if (args.atomicAfterPersist) {
26275
+ try {
26276
+ args.atomicAfterPersist(cap);
26277
+ } catch (err) {
26278
+ try {
26279
+ this.registry.delete(cap.id);
26280
+ } catch {
26281
+ }
26282
+ throw err;
26283
+ }
26284
+ }
26208
26285
  this.hooks.onIssued?.(cap, token);
26209
26286
  return { token, capability: cap };
26210
26287
  }
26211
- revoke(id) {
26212
- const existing = this.registry.findById(id);
26213
- if (!existing) return null;
26214
- const now = (this.hooks.now ?? Date.now)();
26215
- if (existing.revokedAt) {
26216
- return { revokedAt: existing.revokedAt, capability: existing };
26217
- }
26218
- const updated = this.registry.markRevoked(id, now);
26219
- if (!updated) return null;
26220
- this.hooks.onRevoked?.(updated);
26221
- return { revokedAt: now, capability: updated };
26288
+ /**
26289
+ * Hard delete:从 registry / store 物理移除 capability。
26290
+ * idempotent:不存在 null(handler 翻译成 VALIDATION_ERROR)
26291
+ * onDeleted hook 在 daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
26292
+ */
26293
+ delete(id) {
26294
+ const removed = this.registry.delete(id);
26295
+ if (!removed) return null;
26296
+ this.hooks.onDeleted?.(removed);
26297
+ return { capability: removed };
26298
+ }
26299
+ /**
26300
+ * Person identity Phase 1 (B11) passthrough:guest authenticate 上报
26301
+ * selfPrincipalId 时升级 cap.linkedPrincipalId(一次性,undefined → set)。
26302
+ * 详见 CapabilityRegistry.updateLinkedPrincipalId JSDoc。
26303
+ */
26304
+ updateLinkedPrincipalId(id, principalId) {
26305
+ return this.registry.updateLinkedPrincipalId(id, principalId);
26222
26306
  }
26223
26307
  };
26224
26308
  function defaultGenerateToken() {
@@ -26250,15 +26334,20 @@ function cleanupGuestSessionsForCapability(cap, factory) {
26250
26334
  // src/inbox/inbox-store.ts
26251
26335
  var fs17 = __toESM(require("fs"), 1);
26252
26336
  var path19 = __toESM(require("path"), 1);
26253
- var INBOX_FILE_NAME = "inbox.jsonl";
26337
+ var INBOX_SUBDIR = "inbox";
26254
26338
  var InboxStore = class {
26255
26339
  constructor(dataDir) {
26256
26340
  this.dataDir = dataDir;
26257
- fs17.mkdirSync(dataDir, { recursive: true });
26341
+ fs17.mkdirSync(this.dirPath(), { recursive: true });
26258
26342
  }
26259
26343
  dataDir;
26260
- list() {
26261
- const file = this.filePath();
26344
+ /**
26345
+ * 列出某条 channel 的消息, 按 createdAt 升序.
26346
+ * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
26347
+ * channel 文件不存在时返回 [] (不抛, 不创建).
26348
+ */
26349
+ list(capabilityId, sinceCreatedAt) {
26350
+ const file = this.filePath(capabilityId);
26262
26351
  let raw;
26263
26352
  try {
26264
26353
  raw = fs17.readFileSync(file, "utf8");
@@ -26266,11 +26355,28 @@ var InboxStore = class {
26266
26355
  if (err?.code === "ENOENT") return [];
26267
26356
  return [];
26268
26357
  }
26269
- return parseAllLines(raw);
26358
+ const all = parseAllLines(raw);
26359
+ if (sinceCreatedAt === void 0) return all;
26360
+ return all.filter((m2) => m2.createdAt > sinceCreatedAt);
26270
26361
  }
26271
- append(ev) {
26272
- const file = this.filePath();
26273
- const line = JSON.stringify(ev) + "\n";
26362
+ /**
26363
+ * 列出所有 channel 的 capabilityId (扫目录所有 *.jsonl 文件名).
26364
+ * 用于 daemon 启动时构建 index ( cap 聚合 unread 计数等).
26365
+ */
26366
+ listAllCapabilityIds() {
26367
+ const dir = this.dirPath();
26368
+ let entries;
26369
+ try {
26370
+ entries = fs17.readdirSync(dir);
26371
+ } catch (err) {
26372
+ if (err?.code === "ENOENT") return [];
26373
+ return [];
26374
+ }
26375
+ return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
26376
+ }
26377
+ append(message) {
26378
+ const file = this.filePath(message.capabilityId);
26379
+ const line = JSON.stringify(message) + "\n";
26274
26380
  fs17.appendFileSync(file, line, { mode: 384 });
26275
26381
  try {
26276
26382
  fs17.chmodSync(file, 384);
@@ -26278,25 +26384,40 @@ var InboxStore = class {
26278
26384
  }
26279
26385
  }
26280
26386
  /**
26281
- * 标记某条 event 为已读. 已有 readAt 时不覆盖 (idempotent: 第二次调用维持第一次时间).
26282
- * 未知 id 静默 no-op (不抛, 不写文件).
26387
+ * 把该 channel 所有 createdAt<=upToCreatedAt sender!=principalId 的消息
26388
+ * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个 channel 文件.
26389
+ * 返写入了几条 (用于测试断言 / index 维护).
26283
26390
  */
26284
- markRead(id, at) {
26285
- const events = this.list();
26286
- let changed = false;
26287
- const next = events.map((e) => {
26288
- if (e.id !== id) return e;
26289
- if (e.readAt !== void 0) return e;
26290
- changed = true;
26291
- return { ...e, readAt: at };
26391
+ markRead(capabilityId, principalId, upToCreatedAt) {
26392
+ const messages = this.list(capabilityId);
26393
+ let changed = 0;
26394
+ const next = messages.map((m2) => {
26395
+ if (m2.senderPrincipalId === principalId) return m2;
26396
+ if (m2.createdAt > upToCreatedAt) return m2;
26397
+ const cur = m2.readBy[principalId];
26398
+ if (cur !== void 0 && cur >= upToCreatedAt) return m2;
26399
+ changed++;
26400
+ return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
26292
26401
  });
26293
- if (!changed) return;
26294
- this.rewrite(next);
26402
+ if (changed === 0) return 0;
26403
+ this.rewriteChannel(capabilityId, next);
26404
+ return changed;
26295
26405
  }
26296
- rewrite(events) {
26297
- const file = this.filePath();
26406
+ /**
26407
+ * 删整条 channel (capability:delete cascade). 文件不存在 no-op.
26408
+ */
26409
+ removeByCapabilityId(capabilityId) {
26410
+ const file = this.filePath(capabilityId);
26411
+ try {
26412
+ fs17.unlinkSync(file);
26413
+ } catch (err) {
26414
+ if (err?.code === "ENOENT") return;
26415
+ }
26416
+ }
26417
+ rewriteChannel(capabilityId, messages) {
26418
+ const file = this.filePath(capabilityId);
26298
26419
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26299
- const content = events.map((e) => JSON.stringify(e)).join("\n") + (events.length > 0 ? "\n" : "");
26420
+ const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
26300
26421
  fs17.writeFileSync(tmp, content, { mode: 384 });
26301
26422
  fs17.renameSync(tmp, file);
26302
26423
  try {
@@ -26304,8 +26425,11 @@ var InboxStore = class {
26304
26425
  } catch {
26305
26426
  }
26306
26427
  }
26307
- filePath() {
26308
- return path19.join(this.dataDir, INBOX_FILE_NAME);
26428
+ dirPath() {
26429
+ return path19.join(this.dataDir, INBOX_SUBDIR);
26430
+ }
26431
+ filePath(capabilityId) {
26432
+ return path19.join(this.dirPath(), `${capabilityId}.jsonl`);
26309
26433
  }
26310
26434
  };
26311
26435
  function parseAllLines(raw) {
@@ -26319,23 +26443,14 @@ function parseAllLines(raw) {
26319
26443
  } catch {
26320
26444
  continue;
26321
26445
  }
26322
- const r = InboxEventSchema.safeParse(parsed);
26446
+ const r = InboxMessageSchema.safeParse(parsed);
26323
26447
  if (r.success) out.push(r.data);
26324
26448
  }
26325
26449
  return out;
26326
26450
  }
26327
26451
 
26328
26452
  // src/inbox/inbox-manager.ts
26329
- var crypto7 = __toESM(require("crypto"), 1);
26330
-
26331
- // src/inbox/dm.ts
26332
26453
  var crypto6 = __toESM(require("crypto"), 1);
26333
- function deriveDmThreadId(idA, idB) {
26334
- const [low, high] = idA < idB ? [idA, idB] : [idB, idA];
26335
- return crypto6.createHash("sha256").update(`${low}/${high}`).digest("hex");
26336
- }
26337
-
26338
- // src/inbox/inbox-manager.ts
26339
26454
  var InboxManager = class {
26340
26455
  constructor(store, broadcast, opts = {}) {
26341
26456
  this.store = store;
@@ -26348,64 +26463,51 @@ var InboxManager = class {
26348
26463
  now;
26349
26464
  genId;
26350
26465
  /**
26351
- * persona VM 内某 sender CC 发消息 → 跨 principal 时记 inbox event.
26352
- * 返回 null = owner 自己 (no-op); 否则返回写入的 event (含派生 id / createdAt).
26353
- */
26354
- recordPersonaMention(args) {
26355
- if (args.sender.kind === "owner") return null;
26356
- const resource = { type: "persona", id: args.personaId };
26357
- const ev = {
26358
- id: this.genId(),
26359
- kind: "persona-mention",
26360
- fromPrincipal: args.sender,
26361
- toPrincipal: OWNER_PRINCIPAL,
26362
- resource,
26363
- preview: truncatePreview(args.preview),
26364
- createdAt: this.now()
26365
- };
26366
- this.store.append(ev);
26367
- this.broadcast({ type: "inbox:event", event: ev });
26368
- return ev;
26369
- }
26370
- /**
26371
- * Phase 4 Task 4.2: DM 私聊事件. 与 recordPersonaMention 区别:
26372
- * - kind = 'direct-message' (不是 persona-mention)
26373
- * - 不带 resource (DM 不挂某个 persona)
26374
- * - threadId 由 from.id + to.id 派生 (sha256), 双向消息共享同一 thread
26375
- * - 调用方 (handler) 已校验 sender = ctx.principal (防伪造)
26376
- *
26377
- * 返回值是 InboxEvent (含派生 threadId), daemon/index.ts 装配 broadcast 路由:
26378
- * broadcastToPrincipal(from.id) + broadcastToPrincipal(to.id) (双侧 ws 都收).
26466
+ * 写入一条消息到 channel + broadcast 给该 channel 两端 ws.
26467
+ * caller 必须已通过 handler 鉴权 (guest ctx.capabilityId === args.capabilityId, 否则拦截).
26379
26468
  */
26380
- recordDirectMessage(args) {
26381
- const ev = {
26469
+ postMessage(args) {
26470
+ const msg = {
26382
26471
  id: this.genId(),
26383
- kind: "direct-message",
26384
- fromPrincipal: args.from,
26385
- toPrincipal: args.to,
26386
- preview: truncatePreview(args.text),
26472
+ capabilityId: args.capabilityId,
26473
+ senderPrincipalId: args.senderPrincipalId,
26474
+ text: args.text,
26387
26475
  createdAt: this.now(),
26388
- threadId: deriveDmThreadId(args.from.id, args.to.id)
26476
+ readBy: {}
26389
26477
  };
26390
- this.store.append(ev);
26391
- this.broadcast({ type: "inbox:event", event: ev });
26392
- return ev;
26478
+ this.store.append(msg);
26479
+ this.broadcast(args.capabilityId, { type: "inbox:event", message: msg });
26480
+ return msg;
26393
26481
  }
26394
- list(opts = {}) {
26395
- const all = this.store.list();
26396
- if (opts.includeRead) return all;
26397
- return all.filter((e) => e.readAt === void 0);
26482
+ list(capabilityId, sinceCreatedAt) {
26483
+ return this.store.list(capabilityId, sinceCreatedAt);
26398
26484
  }
26399
- markRead(eventId) {
26400
- this.store.markRead(eventId, this.now());
26485
+ /**
26486
+ * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
26487
+ * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道),
26488
+ * 让对端 UI 实时看到 "对方已读到哪了".
26489
+ */
26490
+ markRead(args) {
26491
+ const before = this.store.list(args.capabilityId);
26492
+ const changed = this.store.markRead(args.capabilityId, args.principalId, args.upToCreatedAt);
26493
+ if (changed === 0) return 0;
26494
+ const after = this.store.list(args.capabilityId);
26495
+ const beforeById = new Map(before.map((m2) => [m2.id, m2]));
26496
+ for (const m2 of after) {
26497
+ const prev = beforeById.get(m2.id);
26498
+ if (!prev) continue;
26499
+ if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
26500
+ this.broadcast(args.capabilityId, { type: "inbox:event", message: m2 });
26501
+ }
26502
+ return changed;
26503
+ }
26504
+ /** capability:delete cascade. 删整个 channel 文件. */
26505
+ removeChannel(capabilityId) {
26506
+ this.store.removeByCapabilityId(capabilityId);
26401
26507
  }
26402
26508
  };
26403
- function truncatePreview(s) {
26404
- if (s.length <= INBOX_PREVIEW_MAX_LENGTH) return s;
26405
- return s.slice(0, INBOX_PREVIEW_MAX_LENGTH);
26406
- }
26407
26509
  function defaultGenId() {
26408
- return "inb_" + crypto7.randomBytes(6).toString("base64url");
26510
+ return "m_" + crypto6.randomBytes(6).toString("base64url");
26409
26511
  }
26410
26512
 
26411
26513
  // src/remote-persona/store.ts
@@ -26508,299 +26610,300 @@ var RemotePersonaStore = class {
26508
26610
  }
26509
26611
  };
26510
26612
 
26511
- // src/migrations/2026-05-20-flatten-sessions.ts
26613
+ // src/person/store.ts
26512
26614
  var fs19 = __toESM(require("fs"), 1);
26513
26615
  var path21 = __toESM(require("path"), 1);
26514
- var MIGRATION_FLAG_NAME = ".migration.v1.done";
26515
- function migrateFlattenSessions(opts) {
26516
- const dataDir = opts.dataDir;
26517
- const now = opts.now ?? Date.now;
26518
- const sessionsDir = path21.join(dataDir, "sessions");
26519
- const flagPath = path21.join(sessionsDir, MIGRATION_FLAG_NAME);
26520
- if (existsSync4(flagPath)) {
26521
- return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26616
+ var PERSONS_DIR = "persons";
26617
+ var PersonStore = class {
26618
+ constructor(dataDir) {
26619
+ this.dataDir = dataDir;
26620
+ fs19.mkdirSync(this.rootDir(), { recursive: true });
26522
26621
  }
26523
- let movedBare = 0;
26524
- let movedVmOwner = 0;
26525
- let archivedListener = 0;
26526
- const defaultDir = path21.join(sessionsDir, "default");
26527
- if (existsSync4(defaultDir)) {
26528
- for (const entry of readdirSafe(defaultDir)) {
26529
- if (!entry.endsWith(".json")) continue;
26530
- const src = path21.join(defaultDir, entry);
26531
- const dst = path21.join(sessionsDir, entry);
26532
- fs19.renameSync(src, dst);
26533
- movedBare += 1;
26622
+ dataDir;
26623
+ list() {
26624
+ let entries;
26625
+ try {
26626
+ entries = fs19.readdirSync(this.rootDir());
26627
+ } catch (err) {
26628
+ if (err?.code === "ENOENT") return [];
26629
+ return [];
26534
26630
  }
26535
- rmdirIfEmpty(defaultDir);
26536
- }
26537
- for (const pid of readdirSafe(sessionsDir)) {
26538
- const personaDir = path21.join(sessionsDir, pid);
26539
- if (!isDir(personaDir)) continue;
26540
- if (pid === "default") continue;
26541
- const ownerSrc = path21.join(personaDir, "owner");
26542
- if (existsSync4(ownerSrc) && isDir(ownerSrc)) {
26543
- const ownerDst = path21.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26544
- fs19.mkdirSync(ownerDst, { recursive: true });
26545
- for (const file of readdirSafe(ownerSrc)) {
26546
- if (!file.endsWith(".json")) continue;
26547
- fs19.renameSync(path21.join(ownerSrc, file), path21.join(ownerDst, file));
26548
- movedVmOwner += 1;
26631
+ const out = [];
26632
+ for (const name of entries) {
26633
+ if (!name.endsWith(".json")) continue;
26634
+ if (name.includes(".tmp-")) continue;
26635
+ const file = path21.join(this.rootDir(), name);
26636
+ let raw;
26637
+ try {
26638
+ raw = fs19.readFileSync(file, "utf8");
26639
+ } catch {
26640
+ continue;
26549
26641
  }
26550
- rmdirIfEmpty(ownerSrc);
26551
- }
26552
- const listenerSrc = path21.join(personaDir, "listener");
26553
- if (existsSync4(listenerSrc) && isDir(listenerSrc)) {
26554
- const archiveDst = path21.join(dataDir, ".legacy", `listener-${pid}`);
26555
- fs19.mkdirSync(archiveDst, { recursive: true });
26556
- for (const file of readdirSafe(listenerSrc)) {
26557
- if (!file.endsWith(".json")) continue;
26558
- fs19.renameSync(path21.join(listenerSrc, file), path21.join(archiveDst, file));
26559
- archivedListener += 1;
26642
+ let parsed;
26643
+ try {
26644
+ parsed = JSON.parse(raw);
26645
+ } catch {
26646
+ continue;
26560
26647
  }
26561
- rmdirIfEmpty(listenerSrc);
26648
+ const r = PersonSchema.safeParse(parsed);
26649
+ if (r.success) out.push(r.data);
26562
26650
  }
26563
- rmdirIfEmpty(personaDir);
26564
- }
26565
- fs19.mkdirSync(sessionsDir, { recursive: true });
26566
- fs19.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26567
- return {
26568
- skipped: false,
26569
- flagWritten: true,
26570
- movedBare,
26571
- movedVmOwner,
26572
- archivedListener
26573
- };
26574
- }
26575
- function existsSync4(p2) {
26576
- try {
26577
- fs19.statSync(p2);
26578
- return true;
26579
- } catch {
26580
- return false;
26651
+ return out.sort((a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0);
26581
26652
  }
26582
- }
26583
- function isDir(p2) {
26584
- try {
26585
- return fs19.statSync(p2).isDirectory();
26586
- } catch {
26587
- return false;
26653
+ get(id) {
26654
+ const file = this.filePath(id);
26655
+ let raw;
26656
+ try {
26657
+ raw = fs19.readFileSync(file, "utf8");
26658
+ } catch {
26659
+ return null;
26660
+ }
26661
+ try {
26662
+ const parsed = JSON.parse(raw);
26663
+ const r = PersonSchema.safeParse(parsed);
26664
+ return r.success ? r.data : null;
26665
+ } catch {
26666
+ return null;
26667
+ }
26588
26668
  }
26589
- }
26590
- function readdirSafe(p2) {
26591
- try {
26592
- return fs19.readdirSync(p2);
26593
- } catch {
26594
- return [];
26669
+ add(person) {
26670
+ const file = this.filePath(person.id);
26671
+ if (fs19.existsSync(file)) {
26672
+ throw new Error(`PersonStore.add: person id already exists: ${person.id}`);
26673
+ }
26674
+ this.atomicWrite(file, person);
26595
26675
  }
26596
- }
26597
- function rmdirIfEmpty(p2) {
26598
- try {
26599
- fs19.rmdirSync(p2);
26600
- } catch {
26676
+ /**
26677
+ * Merge patch into existing person + bump updatedAt to `now`.
26678
+ * 不存在 id → no-op(caller 应预先 get 校验)。
26679
+ * 只允许 patch displayName / notes / dmEnabled(id / createdAt 不可改)。
26680
+ */
26681
+ update(id, patch, now) {
26682
+ const cur = this.get(id);
26683
+ if (!cur) return;
26684
+ const next = {
26685
+ ...cur,
26686
+ ...patch.displayName !== void 0 ? { displayName: patch.displayName } : {},
26687
+ ...patch.notes !== void 0 ? { notes: patch.notes } : {},
26688
+ ...patch.dmEnabled !== void 0 ? { dmEnabled: patch.dmEnabled } : {},
26689
+ updatedAt: now
26690
+ };
26691
+ this.atomicWrite(this.filePath(id), next);
26601
26692
  }
26602
- }
26603
-
26604
- // src/transport/http-router.ts
26605
- var import_node_fs14 = __toESM(require("fs"), 1);
26606
- var import_node_path16 = __toESM(require("path"), 1);
26607
-
26608
- // src/attachment/group.ts
26609
- var import_node_fs13 = __toESM(require("fs"), 1);
26610
- var import_node_path14 = __toESM(require("path"), 1);
26611
- var import_node_crypto4 = __toESM(require("crypto"), 1);
26612
- init_protocol();
26613
- var GroupFileStore = class {
26614
- dataDir;
26615
- logger;
26616
- cache = /* @__PURE__ */ new Map();
26617
- constructor(opts) {
26618
- this.dataDir = opts.dataDir;
26619
- this.logger = opts.logger;
26693
+ remove(id) {
26694
+ const file = this.filePath(id);
26695
+ try {
26696
+ fs19.unlinkSync(file);
26697
+ return true;
26698
+ } catch (err) {
26699
+ if (err?.code === "ENOENT") return false;
26700
+ throw err;
26701
+ }
26620
26702
  }
26621
- rootForScope(scope) {
26622
- return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26703
+ rootDir() {
26704
+ return path21.join(this.dataDir, PERSONS_DIR);
26623
26705
  }
26624
- /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
26625
- filePath(scope, sessionId) {
26626
- return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26706
+ filePath(id) {
26707
+ return path21.join(this.rootDir(), `${safeFileName(id)}.json`);
26627
26708
  }
26628
- cacheKey(scope, sessionId) {
26629
- return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26709
+ atomicWrite(file, content) {
26710
+ fs19.mkdirSync(this.rootDir(), { recursive: true });
26711
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26712
+ fs19.writeFileSync(tmp, JSON.stringify(content, null, 2), { mode: 384 });
26713
+ fs19.renameSync(tmp, file);
26714
+ try {
26715
+ fs19.chmodSync(file, 384);
26716
+ } catch {
26717
+ }
26630
26718
  }
26631
- /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
26632
- readFile(scope, sessionId) {
26633
- const file = this.filePath(scope, sessionId);
26719
+ };
26720
+
26721
+ // src/person/alias-store.ts
26722
+ var fs20 = __toESM(require("fs"), 1);
26723
+ var path22 = __toESM(require("path"), 1);
26724
+ var PERSON_ALIASES_SUBDIR = "persons-meta";
26725
+ var PERSON_ALIASES_FILE = "aliases.json";
26726
+ var PersonAliasStore = class {
26727
+ constructor(dataDir) {
26728
+ this.dataDir = dataDir;
26729
+ fs20.mkdirSync(path22.join(this.dataDir, PERSON_ALIASES_SUBDIR), { recursive: true });
26730
+ }
26731
+ dataDir;
26732
+ get filePath() {
26733
+ return path22.join(this.dataDir, PERSON_ALIASES_SUBDIR, PERSON_ALIASES_FILE);
26734
+ }
26735
+ list() {
26736
+ let raw;
26634
26737
  try {
26635
- const raw = import_node_fs13.default.readFileSync(file, "utf8");
26636
- const parsed = JSON.parse(raw);
26637
- if (!Array.isArray(parsed)) {
26638
- this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
26639
- file
26640
- });
26641
- return [];
26642
- }
26643
- const out = [];
26644
- for (const entry of parsed) {
26645
- const r = GroupFileEntrySchema.safeParse(entry);
26646
- if (r.success) out.push(r.data);
26647
- }
26648
- return out;
26738
+ raw = fs20.readFileSync(this.filePath, "utf8");
26649
26739
  } catch (err) {
26650
- const code = err?.code;
26651
- if (code === "ENOENT") return [];
26652
- this.logger?.warn("GroupFileStore.readFile failed", {
26653
- file,
26654
- err: err.message
26655
- });
26740
+ if (err?.code === "ENOENT") return [];
26741
+ return [];
26742
+ }
26743
+ let parsed;
26744
+ try {
26745
+ parsed = JSON.parse(raw);
26746
+ } catch {
26656
26747
  return [];
26657
26748
  }
26749
+ if (!Array.isArray(parsed)) return [];
26750
+ const out = [];
26751
+ for (const item of parsed) {
26752
+ const r = PersonAliasSchema.safeParse(item);
26753
+ if (r.success) out.push(r.data);
26754
+ }
26755
+ return out;
26658
26756
  }
26659
- writeFile(scope, sessionId, entries) {
26660
- const file = this.filePath(scope, sessionId);
26661
- import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
26662
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
26663
- import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
26664
- import_node_fs13.default.renameSync(tmp, file);
26757
+ get(principalId) {
26758
+ return this.list().find((a) => a.principalId === principalId)?.personId;
26665
26759
  }
26666
- /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
26667
- list(scope, sessionId) {
26668
- const key = this.cacheKey(scope, sessionId);
26669
- const cached = this.cache.get(key);
26670
- if (cached) return cached.entries;
26671
- const entries = this.readFile(scope, sessionId);
26672
- this.cache.set(key, { entries, scope });
26673
- return entries;
26760
+ set(principalId, personId) {
26761
+ const all = this.list().filter((a) => a.principalId !== principalId);
26762
+ all.push({ principalId, personId });
26763
+ this.write(all);
26764
+ }
26765
+ remove(principalId) {
26766
+ const all = this.list();
26767
+ const next = all.filter((a) => a.principalId !== principalId);
26768
+ if (next.length === all.length) return false;
26769
+ this.write(next);
26770
+ return true;
26771
+ }
26772
+ removeAllByPerson(personId) {
26773
+ const all = this.list();
26774
+ const next = all.filter((a) => a.personId !== personId);
26775
+ if (next.length === all.length) return;
26776
+ this.write(next);
26674
26777
  }
26675
26778
  /**
26676
- * upsert:
26677
- * - relPath 已存在 更新 lastEditedAt + clear stale;不动 from / addedAt / id
26678
- * (保留首次入群人 / 入群时刻)
26679
- * - 不存在 → append,id 派生稳定 uuid,addedAt = now
26779
+ * 把某条 alias 的 principalId 改为新 key,personId 不变。
26780
+ * 用于 guest 第一次 authenticate 上报 selfPrincipalId 时,把 alias key
26781
+ * cap.id(fallback)升级到真实 ownerPrincipalId。
26680
26782
  *
26681
- * 返回最新 entry(caller 可用来 broadcast 通知)。
26783
+ * oldKey 不存在 no-op。
26784
+ * 若 newKey 已存在另一条 entry → 用 oldKey 的 personId 覆盖("old wins"),并去重。
26682
26785
  */
26683
- upsert(scope, sessionId, input, now = Date.now()) {
26684
- const entries = this.list(scope, sessionId).slice();
26685
- const idx = entries.findIndex((e) => e.relPath === input.relPath);
26686
- let next;
26687
- if (idx >= 0) {
26688
- const prev = entries[idx];
26689
- next = {
26690
- ...prev,
26691
- size: input.size,
26692
- mime: input.mime,
26693
- lastEditedAt: now,
26694
- stale: false
26695
- // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
26696
- };
26697
- entries[idx] = next;
26698
- } else {
26699
- next = {
26700
- id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
26701
- relPath: input.relPath,
26702
- from: input.from,
26703
- label: input.label,
26704
- size: input.size,
26705
- mime: input.mime,
26706
- addedAt: now
26707
- // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
26708
- };
26709
- entries.push(next);
26786
+ rekey(oldPrincipalId, newPrincipalId) {
26787
+ const all = this.list();
26788
+ const old = all.find((a) => a.principalId === oldPrincipalId);
26789
+ if (!old) return;
26790
+ const filtered = all.filter(
26791
+ (a) => a.principalId !== oldPrincipalId && a.principalId !== newPrincipalId
26792
+ );
26793
+ filtered.push({ principalId: newPrincipalId, personId: old.personId });
26794
+ this.write(filtered);
26795
+ }
26796
+ write(entries) {
26797
+ fs20.mkdirSync(path22.join(this.dataDir, PERSON_ALIASES_SUBDIR), { recursive: true });
26798
+ const tmp = `${this.filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26799
+ fs20.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
26800
+ fs20.renameSync(tmp, this.filePath);
26801
+ try {
26802
+ fs20.chmodSync(this.filePath, 384);
26803
+ } catch {
26710
26804
  }
26711
- this.writeFile(scope, sessionId, entries);
26712
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26713
- return next;
26714
26805
  }
26715
- /**
26716
- * 标记一个 relPath stale(agent rm / mv 后文件不在)。
26717
- * - 命中 → stale=true,UI 灰显
26718
- * - 未命中 noop(不需要为不在群里的文件创建 stale 条目)
26719
- *
26720
- * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26721
- */
26722
- markStale(scope, sessionId, relPath) {
26723
- const entries = this.list(scope, sessionId).slice();
26724
- const idx = entries.findIndex((e) => e.relPath === relPath);
26725
- if (idx < 0) return;
26726
- if (entries[idx].stale) return;
26727
- entries[idx] = { ...entries[idx], stale: true };
26728
- this.writeFile(scope, sessionId, entries);
26729
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26806
+ };
26807
+
26808
+ // src/migrations/2026-05-20-flatten-sessions.ts
26809
+ var fs21 = __toESM(require("fs"), 1);
26810
+ var path23 = __toESM(require("path"), 1);
26811
+ var MIGRATION_FLAG_NAME = ".migration.v1.done";
26812
+ function migrateFlattenSessions(opts) {
26813
+ const dataDir = opts.dataDir;
26814
+ const now = opts.now ?? Date.now;
26815
+ const sessionsDir = path23.join(dataDir, "sessions");
26816
+ const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
26817
+ if (existsSync5(flagPath)) {
26818
+ return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26730
26819
  }
26731
- /**
26732
- * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
26733
- * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
26734
- * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
26735
- *
26736
- * 返回值:true=命中并删除;false=relPath 不在群里。
26737
- */
26738
- remove(scope, sessionId, relPath) {
26739
- const entries = this.list(scope, sessionId).slice();
26740
- const idx = entries.findIndex((e) => e.relPath === relPath);
26741
- if (idx < 0) return false;
26742
- entries.splice(idx, 1);
26743
- this.writeFile(scope, sessionId, entries);
26744
- this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26745
- return true;
26820
+ let movedBare = 0;
26821
+ let movedVmOwner = 0;
26822
+ let archivedListener = 0;
26823
+ const defaultDir = path23.join(sessionsDir, "default");
26824
+ if (existsSync5(defaultDir)) {
26825
+ for (const entry of readdirSafe(defaultDir)) {
26826
+ if (!entry.endsWith(".json")) continue;
26827
+ const src = path23.join(defaultDir, entry);
26828
+ const dst = path23.join(sessionsDir, entry);
26829
+ fs21.renameSync(src, dst);
26830
+ movedBare += 1;
26831
+ }
26832
+ rmdirIfEmpty(defaultDir);
26746
26833
  }
26747
- /**
26748
- * session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
26749
- *
26750
- * <dataDir>/sessions/<personaId>/owner/*.group-files.json
26751
- * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
26752
- *
26753
- * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26754
- */
26755
- listByPersona(personaId) {
26756
- const out = [];
26757
- for (const mode of ["owner", "listener"]) {
26758
- const scope = { kind: "persona", personaId, mode };
26759
- const root = this.rootForScope(scope);
26760
- let names;
26761
- try {
26762
- names = import_node_fs13.default.readdirSync(root);
26763
- } catch (err) {
26764
- const code = err?.code;
26765
- if (code === "ENOENT") continue;
26766
- continue;
26834
+ for (const pid of readdirSafe(sessionsDir)) {
26835
+ const personaDir = path23.join(sessionsDir, pid);
26836
+ if (!isDir(personaDir)) continue;
26837
+ if (pid === "default") continue;
26838
+ const ownerSrc = path23.join(personaDir, "owner");
26839
+ if (existsSync5(ownerSrc) && isDir(ownerSrc)) {
26840
+ const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26841
+ fs21.mkdirSync(ownerDst, { recursive: true });
26842
+ for (const file of readdirSafe(ownerSrc)) {
26843
+ if (!file.endsWith(".json")) continue;
26844
+ fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
26845
+ movedVmOwner += 1;
26767
26846
  }
26768
- for (const name of names) {
26769
- if (!name.endsWith(".group-files.json")) continue;
26770
- const sessionId = name.slice(0, -".group-files.json".length);
26771
- if (!sessionId) continue;
26772
- const entries = this.list(scope, sessionId);
26773
- out.push({ sessionId, entries });
26847
+ rmdirIfEmpty(ownerSrc);
26848
+ }
26849
+ const listenerSrc = path23.join(personaDir, "listener");
26850
+ if (existsSync5(listenerSrc) && isDir(listenerSrc)) {
26851
+ const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
26852
+ fs21.mkdirSync(archiveDst, { recursive: true });
26853
+ for (const file of readdirSafe(listenerSrc)) {
26854
+ if (!file.endsWith(".json")) continue;
26855
+ fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
26856
+ archivedListener += 1;
26774
26857
  }
26858
+ rmdirIfEmpty(listenerSrc);
26775
26859
  }
26776
- return out;
26860
+ rmdirIfEmpty(personaDir);
26777
26861
  }
26778
- };
26779
- function personalViewable(groupStore, personaDir, personaId, absPath) {
26780
- const realTarget = safeRealpath(absPath);
26781
- if (!realTarget) {
26862
+ fs21.mkdirSync(sessionsDir, { recursive: true });
26863
+ fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26864
+ return {
26865
+ skipped: false,
26866
+ flagWritten: true,
26867
+ movedBare,
26868
+ movedVmOwner,
26869
+ archivedListener
26870
+ };
26871
+ }
26872
+ function existsSync5(p2) {
26873
+ try {
26874
+ fs21.statSync(p2);
26875
+ return true;
26876
+ } catch {
26782
26877
  return false;
26783
26878
  }
26784
- const personasUnion = groupStore.listByPersona(personaId);
26785
- for (const { entries } of personasUnion) {
26786
- for (const e of entries) {
26787
- if (e.stale) continue;
26788
- const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
26789
- if (realEntry && realEntry === realTarget) return true;
26790
- }
26879
+ }
26880
+ function isDir(p2) {
26881
+ try {
26882
+ return fs21.statSync(p2).isDirectory();
26883
+ } catch {
26884
+ return false;
26791
26885
  }
26792
- return false;
26793
26886
  }
26794
- function safeRealpath(p2) {
26887
+ function readdirSafe(p2) {
26795
26888
  try {
26796
- return import_node_fs13.default.realpathSync(p2);
26889
+ return fs21.readdirSync(p2);
26890
+ } catch {
26891
+ return [];
26892
+ }
26893
+ }
26894
+ function rmdirIfEmpty(p2) {
26895
+ try {
26896
+ fs21.rmdirSync(p2);
26797
26897
  } catch {
26798
- return null;
26799
26898
  }
26800
26899
  }
26801
26900
 
26802
- // src/attachment/mime.ts
26901
+ // src/transport/http-router.ts
26902
+ var import_node_fs13 = __toESM(require("fs"), 1);
26803
26903
  var import_node_path15 = __toESM(require("path"), 1);
26904
+
26905
+ // src/attachment/mime.ts
26906
+ var import_node_path14 = __toESM(require("path"), 1);
26804
26907
  var TEXT_PLAIN = "text/plain; charset=utf-8";
26805
26908
  var EXT_TO_NATIVE_MIME = {
26806
26909
  // 图片
@@ -26907,14 +27010,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
26907
27010
  ".mk"
26908
27011
  ]);
26909
27012
  function lookupMime(filePathOrName) {
26910
- const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
27013
+ const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
26911
27014
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
26912
27015
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
26913
27016
  return "application/octet-stream";
26914
27017
  }
26915
27018
 
26916
27019
  // src/attachment/sign-url.ts
26917
- var import_node_crypto5 = __toESM(require("crypto"), 1);
27020
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
26918
27021
  var HMAC_ALGO = "sha256";
26919
27022
  function base64urlEncode(buf) {
26920
27023
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -26931,7 +27034,7 @@ function decodeAbsPathFromUrl(encoded) {
26931
27034
  }
26932
27035
  function computeSig(secret, absPath, e) {
26933
27036
  const msg = e === null ? absPath : `${absPath}|${e}`;
26934
- return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
27037
+ return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26935
27038
  }
26936
27039
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
26937
27040
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -26966,7 +27069,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
26966
27069
  if (provided.length !== expected.length) {
26967
27070
  return { ok: false, code: "BAD_SIG" };
26968
27071
  }
26969
- if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
27072
+ if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
26970
27073
  return { ok: false, code: "BAD_SIG" };
26971
27074
  }
26972
27075
  if (e !== null && now() / 1e3 > e) {
@@ -26987,7 +27090,7 @@ function createHttpRouter(deps) {
26987
27090
  sendJson(res, 200, { ok: true, version: deps.daemonVersion });
26988
27091
  return true;
26989
27092
  }
26990
- if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
27093
+ if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26991
27094
  return false;
26992
27095
  }
26993
27096
  if (url.pathname.startsWith("/files/") && req.method === "GET") {
@@ -27027,43 +27130,8 @@ function createHttpRouter(deps) {
27027
27130
  sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
27028
27131
  return true;
27029
27132
  }
27030
- const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
27031
- if (personaFilesMatch && req.method === "GET") {
27032
- const pid = personaFilesMatch[1];
27033
- const pathParam = url.searchParams.get("path");
27034
- if (!pathParam) {
27035
- sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
27036
- return true;
27037
- }
27038
- if (!deps.personaStore || !deps.groupFileStore) {
27039
- sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
27040
- return true;
27041
- }
27042
- const personaDir = deps.personaStore.personaDirPath(pid);
27043
- const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
27044
- if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
27045
- sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
27046
- return true;
27047
- }
27048
- if (ctx.role === "personal") {
27049
- if (ctx.personaId !== pid) {
27050
- sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
27051
- return true;
27052
- }
27053
- if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
27054
- sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
27055
- return true;
27056
- }
27057
- }
27058
- streamFile(res, absPath, deps.logger);
27059
- return true;
27060
- }
27061
27133
  const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
27062
27134
  if (sessionFilesMatch && req.method === "GET") {
27063
- if (ctx.role !== "owner") {
27064
- sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
27065
- return true;
27066
- }
27067
27135
  const sid = sessionFilesMatch[1];
27068
27136
  const pathParam = url.searchParams.get("path");
27069
27137
  if (!pathParam) {
@@ -27071,7 +27139,7 @@ function createHttpRouter(deps) {
27071
27139
  return true;
27072
27140
  }
27073
27141
  let absPath;
27074
- if (import_node_path16.default.isAbsolute(pathParam)) {
27142
+ if (import_node_path15.default.isAbsolute(pathParam)) {
27075
27143
  absPath = pathParam;
27076
27144
  } else if (deps.sessionStore) {
27077
27145
  const file = deps.sessionStore.read(sid);
@@ -27079,7 +27147,7 @@ function createHttpRouter(deps) {
27079
27147
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
27080
27148
  return true;
27081
27149
  }
27082
- absPath = import_node_path16.default.join(file.cwd, pathParam);
27150
+ absPath = import_node_path15.default.join(file.cwd, pathParam);
27083
27151
  } else {
27084
27152
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
27085
27153
  return true;
@@ -27098,53 +27166,188 @@ function parseUrl(rawUrl) {
27098
27166
  } catch {
27099
27167
  return null;
27100
27168
  }
27101
- }
27102
- function sendJson(res, status, body) {
27103
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
27104
- res.end(JSON.stringify(body));
27105
- }
27106
- function withCtx(ctx, body) {
27107
- return { ...body, role: ctx.role, personaId: ctx.personaId };
27108
- }
27109
- function isContainedIn(abs, root) {
27110
- const normalized = import_node_path16.default.resolve(abs);
27111
- const normalizedRoot = import_node_path16.default.resolve(root);
27112
- if (normalized === normalizedRoot) return true;
27113
- return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
27114
- }
27115
- function streamFile(res, absPath, logger) {
27116
- let stat;
27117
- try {
27118
- stat = import_node_fs14.default.statSync(absPath);
27119
- } catch (err) {
27120
- const code = err?.code;
27121
- if (code === "ENOENT") {
27122
- sendJson(res, 404, { code: "NOT_FOUND", message: "file not found" });
27169
+ }
27170
+ function sendJson(res, status, body) {
27171
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
27172
+ res.end(JSON.stringify(body));
27173
+ }
27174
+ function withCtx(ctx, body) {
27175
+ return { ...body, role: ctx.role };
27176
+ }
27177
+ function streamFile(res, absPath, logger) {
27178
+ let stat;
27179
+ try {
27180
+ stat = import_node_fs13.default.statSync(absPath);
27181
+ } catch (err) {
27182
+ const code = err?.code;
27183
+ if (code === "ENOENT") {
27184
+ sendJson(res, 404, { code: "NOT_FOUND", message: "file not found" });
27185
+ } else {
27186
+ sendJson(res, 500, { code: "STAT_FAILED", message: err.message });
27187
+ }
27188
+ return;
27189
+ }
27190
+ if (!stat.isFile()) {
27191
+ sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
27192
+ return;
27193
+ }
27194
+ const mime = lookupMime(absPath);
27195
+ const basename = import_node_path15.default.basename(absPath);
27196
+ res.writeHead(200, {
27197
+ "Content-Type": mime,
27198
+ "Content-Length": String(stat.size),
27199
+ "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
27200
+ // 防止浏览器把任意 mime 当 html 渲染
27201
+ "X-Content-Type-Options": "nosniff"
27202
+ });
27203
+ const stream = import_node_fs13.default.createReadStream(absPath);
27204
+ stream.on("error", (err) => {
27205
+ logger?.warn("streamFile read error", { absPath, err: err.message });
27206
+ res.destroy();
27207
+ });
27208
+ stream.pipe(res);
27209
+ }
27210
+
27211
+ // src/attachment/group.ts
27212
+ var import_node_fs14 = __toESM(require("fs"), 1);
27213
+ var import_node_path16 = __toESM(require("path"), 1);
27214
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
27215
+ init_protocol();
27216
+ var GroupFileStore = class {
27217
+ dataDir;
27218
+ logger;
27219
+ cache = /* @__PURE__ */ new Map();
27220
+ constructor(opts) {
27221
+ this.dataDir = opts.dataDir;
27222
+ this.logger = opts.logger;
27223
+ }
27224
+ rootForScope(scope) {
27225
+ return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27226
+ }
27227
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
27228
+ filePath(scope, sessionId) {
27229
+ return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27230
+ }
27231
+ cacheKey(scope, sessionId) {
27232
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
27233
+ }
27234
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
27235
+ readFile(scope, sessionId) {
27236
+ const file = this.filePath(scope, sessionId);
27237
+ try {
27238
+ const raw = import_node_fs14.default.readFileSync(file, "utf8");
27239
+ const parsed = JSON.parse(raw);
27240
+ if (!Array.isArray(parsed)) {
27241
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
27242
+ file
27243
+ });
27244
+ return [];
27245
+ }
27246
+ const out = [];
27247
+ for (const entry of parsed) {
27248
+ const r = GroupFileEntrySchema.safeParse(entry);
27249
+ if (r.success) out.push(r.data);
27250
+ }
27251
+ return out;
27252
+ } catch (err) {
27253
+ const code = err?.code;
27254
+ if (code === "ENOENT") return [];
27255
+ this.logger?.warn("GroupFileStore.readFile failed", {
27256
+ file,
27257
+ err: err.message
27258
+ });
27259
+ return [];
27260
+ }
27261
+ }
27262
+ writeFile(scope, sessionId, entries) {
27263
+ const file = this.filePath(scope, sessionId);
27264
+ import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
27265
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27266
+ import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27267
+ import_node_fs14.default.renameSync(tmp, file);
27268
+ }
27269
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
27270
+ list(scope, sessionId) {
27271
+ const key = this.cacheKey(scope, sessionId);
27272
+ const cached = this.cache.get(key);
27273
+ if (cached) return cached.entries;
27274
+ const entries = this.readFile(scope, sessionId);
27275
+ this.cache.set(key, { entries });
27276
+ return entries;
27277
+ }
27278
+ /**
27279
+ * upsert:
27280
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
27281
+ * (保留首次入群人 / 入群时刻)
27282
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
27283
+ *
27284
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
27285
+ */
27286
+ upsert(scope, sessionId, input, now = Date.now()) {
27287
+ const entries = this.list(scope, sessionId).slice();
27288
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
27289
+ let next;
27290
+ if (idx >= 0) {
27291
+ const prev = entries[idx];
27292
+ next = {
27293
+ ...prev,
27294
+ size: input.size,
27295
+ mime: input.mime,
27296
+ lastEditedAt: now,
27297
+ stale: false
27298
+ // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
27299
+ };
27300
+ entries[idx] = next;
27123
27301
  } else {
27124
- sendJson(res, 500, { code: "STAT_FAILED", message: err.message });
27302
+ next = {
27303
+ id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
27304
+ relPath: input.relPath,
27305
+ from: input.from,
27306
+ label: input.label,
27307
+ size: input.size,
27308
+ mime: input.mime,
27309
+ addedAt: now
27310
+ // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
27311
+ };
27312
+ entries.push(next);
27125
27313
  }
27126
- return;
27314
+ this.writeFile(scope, sessionId, entries);
27315
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27316
+ return next;
27127
27317
  }
27128
- if (!stat.isFile()) {
27129
- sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
27130
- return;
27318
+ /**
27319
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
27320
+ * - 命中 → stale=true,UI 灰显
27321
+ * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
27322
+ *
27323
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
27324
+ */
27325
+ markStale(scope, sessionId, relPath) {
27326
+ const entries = this.list(scope, sessionId).slice();
27327
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27328
+ if (idx < 0) return;
27329
+ if (entries[idx].stale) return;
27330
+ entries[idx] = { ...entries[idx], stale: true };
27331
+ this.writeFile(scope, sessionId, entries);
27332
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27131
27333
  }
27132
- const mime = lookupMime(absPath);
27133
- const basename = import_node_path16.default.basename(absPath);
27134
- res.writeHead(200, {
27135
- "Content-Type": mime,
27136
- "Content-Length": String(stat.size),
27137
- "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
27138
- // 防止浏览器把任意 mime 当 html 渲染
27139
- "X-Content-Type-Options": "nosniff"
27140
- });
27141
- const stream = import_node_fs14.default.createReadStream(absPath);
27142
- stream.on("error", (err) => {
27143
- logger?.warn("streamFile read error", { absPath, err: err.message });
27144
- res.destroy();
27145
- });
27146
- stream.pipe(res);
27147
- }
27334
+ /**
27335
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
27336
+ * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
27337
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
27338
+ *
27339
+ * 返回值:true=命中并删除;false=relPath 不在群里。
27340
+ */
27341
+ remove(scope, sessionId, relPath) {
27342
+ const entries = this.list(scope, sessionId).slice();
27343
+ const idx = entries.findIndex((e) => e.relPath === relPath);
27344
+ if (idx < 0) return false;
27345
+ entries.splice(idx, 1);
27346
+ this.writeFile(scope, sessionId, entries);
27347
+ this.cache.set(this.cacheKey(scope, sessionId), { entries });
27348
+ return true;
27349
+ }
27350
+ };
27148
27351
 
27149
27352
  // src/discovery/state-file.ts
27150
27353
  var import_node_fs15 = __toESM(require("fs"), 1);
@@ -27822,29 +28025,45 @@ var AUTH_FILE_NAME = "auth.json";
27822
28025
  function authFilePath(dataDir) {
27823
28026
  return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
27824
28027
  }
27825
- function loadOrCreateAuthToken(opts) {
28028
+ function loadOrCreateAuth(opts) {
27826
28029
  const file = authFilePath(opts.dataDir);
27827
28030
  const existing = readAuthFile(file);
27828
- if (existing && existing.token) return existing.token;
27829
- const token = (opts.generate ?? defaultGenerate)();
28031
+ const genToken = opts.generate ?? defaultGenerateToken2;
28032
+ const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
27830
28033
  const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
27831
- writeAuthFile(file, { token, createdAt: now.toISOString() });
27832
- return token;
28034
+ if (existing) {
28035
+ if (!existing.ownerPrincipalId) {
28036
+ const ownerPrincipalId2 = genId();
28037
+ writeAuthFile(file, {
28038
+ token: existing.token,
28039
+ ownerPrincipalId: ownerPrincipalId2,
28040
+ createdAt: existing.createdAt
28041
+ });
28042
+ return { token: existing.token, ownerPrincipalId: ownerPrincipalId2 };
28043
+ }
28044
+ return { token: existing.token, ownerPrincipalId: existing.ownerPrincipalId };
28045
+ }
28046
+ const token = genToken();
28047
+ const ownerPrincipalId = genId();
28048
+ writeAuthFile(file, { token, ownerPrincipalId, createdAt: now.toISOString() });
28049
+ return { token, ownerPrincipalId };
27833
28050
  }
27834
- function defaultGenerate() {
28051
+ function defaultGenerateToken2() {
27835
28052
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
27836
28053
  }
28054
+ function defaultGenerateOwnerPrincipalId() {
28055
+ return `owner-${import_node_crypto8.default.randomUUID()}`;
28056
+ }
27837
28057
  function readAuthFile(file) {
27838
28058
  try {
27839
28059
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
27840
28060
  const parsed = JSON.parse(raw);
27841
- if (typeof parsed?.token === "string" && parsed.token.length > 0) {
27842
- return {
27843
- token: parsed.token,
27844
- createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
27845
- };
27846
- }
27847
- return null;
28061
+ if (typeof parsed?.token !== "string" || parsed.token.length === 0) return null;
28062
+ return {
28063
+ token: parsed.token,
28064
+ ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : "",
28065
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
28066
+ };
27848
28067
  } catch (err) {
27849
28068
  const code = err?.code;
27850
28069
  if (code === "ENOENT") return null;
@@ -28020,7 +28239,8 @@ function buildSessionHandlers(deps) {
28020
28239
  }
28021
28240
  }
28022
28241
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
28023
- const { response, broadcast } = manager.create(args);
28242
+ const creatorPrincipalId = ctx?.principal.id ?? "";
28243
+ const { response, broadcast } = manager.create(args, creatorPrincipalId);
28024
28244
  return { response: { type: "session:info", ...response }, broadcast };
28025
28245
  };
28026
28246
  const list = async (_frame, _client, ctx) => {
@@ -28469,21 +28689,27 @@ function buildCapabilitiesHandlers(deps) {
28469
28689
  // src/handlers/capability.ts
28470
28690
  init_zod();
28471
28691
  init_protocol();
28472
- var IssueArgsSchema = external_exports.object({
28473
- displayName: external_exports.string().min(1),
28474
- grants: external_exports.array(GrantSchema),
28475
- expiresAt: external_exports.number().int().positive().optional(),
28476
- maxUses: external_exports.number().int().positive().optional()
28477
- }).strict();
28478
- var RevokeArgsSchema = external_exports.object({
28692
+ var DeleteArgsSchema = external_exports.object({
28479
28693
  capabilityId: external_exports.string().min(1)
28480
28694
  }).strict();
28481
28695
  function buildCapabilityHandlers(deps) {
28482
- const { manager, getShareBaseUrl } = deps;
28696
+ const { manager, getShareBaseUrl, personStore, aliasStore } = deps;
28483
28697
  const issue = async (frame) => {
28484
28698
  const { type: _type, requestId: _requestId, ...rest } = frame;
28485
- const args = IssueArgsSchema.parse(rest);
28486
- const { token, capability } = manager.issue(args);
28699
+ const args = CapabilityIssueArgsSchema.parse(rest);
28700
+ if (!personStore.get(args.personId)) {
28701
+ throw new ClawdError(
28702
+ ERROR_CODES.VALIDATION_ERROR,
28703
+ `Person not found: ${args.personId}`
28704
+ );
28705
+ }
28706
+ const { personId, ...managerArgs } = args;
28707
+ const { token, capability } = manager.issue({
28708
+ ...managerArgs,
28709
+ atomicAfterPersist: (cap) => {
28710
+ aliasStore.set(cap.id, personId);
28711
+ }
28712
+ });
28487
28713
  const base = getShareBaseUrl().replace(/\/$/, "");
28488
28714
  const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
28489
28715
  return {
@@ -28503,97 +28729,132 @@ function buildCapabilityHandlers(deps) {
28503
28729
  }
28504
28730
  };
28505
28731
  };
28506
- const revoke = async (frame) => {
28732
+ const del = async (frame) => {
28507
28733
  const { type: _type, requestId: _requestId, ...rest } = frame;
28508
- const args = RevokeArgsSchema.parse(rest);
28509
- const result = manager.revoke(args.capabilityId);
28734
+ const args = DeleteArgsSchema.parse(rest);
28735
+ const result = manager.delete(args.capabilityId);
28510
28736
  if (!result) {
28511
28737
  throw new ClawdError(
28512
28738
  ERROR_CODES.VALIDATION_ERROR,
28513
28739
  `capability not found: ${args.capabilityId}`
28514
28740
  );
28515
28741
  }
28742
+ aliasStore.remove(args.capabilityId);
28743
+ if (result.capability.linkedPrincipalId !== void 0) {
28744
+ aliasStore.remove(result.capability.linkedPrincipalId);
28745
+ }
28516
28746
  return {
28517
28747
  response: {
28518
- type: "capability:revoked",
28519
- capabilityId: args.capabilityId,
28520
- revokedAt: result.revokedAt
28748
+ type: "capability:deleted",
28749
+ capabilityId: args.capabilityId
28521
28750
  }
28522
28751
  };
28523
28752
  };
28524
28753
  return {
28525
28754
  "capability:issue": issue,
28526
28755
  "capability:list": list,
28527
- "capability:revoke": revoke
28756
+ "capability:delete": del
28528
28757
  };
28529
28758
  }
28530
28759
 
28531
28760
  // src/handlers/inbox.ts
28532
28761
  init_protocol();
28762
+ function assertChannelAccess(ctx, capabilityId) {
28763
+ if (ctx.principal.kind === "owner") return;
28764
+ if (ctx.capabilityId === capabilityId) return;
28765
+ throw new ClawdError(
28766
+ ERROR_CODES.UNAUTHORIZED,
28767
+ `inbox: guest can only access own channel (capabilityId=${capabilityId}, ctx.capabilityId=${ctx.capabilityId ?? "none"})`
28768
+ );
28769
+ }
28533
28770
  function buildInboxHandlers(deps) {
28534
- const { manager, capabilityRegistry } = deps;
28535
- function resolvePeerPrincipal(peerId) {
28536
- if (peerId === "owner") {
28537
- return { id: "owner", kind: "owner", displayName: "owner" };
28771
+ const { manager, personStore, aliasStore } = deps;
28772
+ const postMessage = async (frame, _client, ctx) => {
28773
+ const { type: _t, requestId: _r, ...rest } = frame;
28774
+ const args = InboxPostMessageArgsSchema.parse(rest);
28775
+ if (!ctx) {
28776
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
28538
28777
  }
28539
- const cap = capabilityRegistry.findById(peerId);
28540
- if (!cap) {
28541
- throw new ClawdError(
28542
- ERROR_CODES.VALIDATION_ERROR,
28543
- `peer principal not found: ${peerId}`
28544
- );
28778
+ assertChannelAccess(ctx, args.capabilityId);
28779
+ if (ctx.principal.kind === "guest") {
28780
+ const senderPrincipalId = ctx.principal.id;
28781
+ const personId = aliasStore.get(senderPrincipalId);
28782
+ if (personId !== void 0) {
28783
+ const person = personStore.get(personId);
28784
+ if (person && !person.dmEnabled) {
28785
+ throw new ClawdError(
28786
+ ERROR_CODES.VALIDATION_ERROR,
28787
+ `DM_BLOCKED: Person ${person.displayName} has DM disabled`
28788
+ );
28789
+ }
28790
+ }
28545
28791
  }
28546
- return { id: cap.id, kind: "guest", displayName: cap.displayName };
28547
- }
28548
- const list = async (frame) => {
28549
- const { type: _t, requestId: _r, ...rest } = frame;
28550
- const args = InboxListArgsSchema.parse(rest);
28551
- const events = manager.list({ includeRead: args.includeRead ?? false });
28792
+ const message = manager.postMessage({
28793
+ capabilityId: args.capabilityId,
28794
+ senderPrincipalId: ctx.principal.id,
28795
+ text: args.text
28796
+ });
28552
28797
  return {
28553
- response: { type: "inbox:list", events }
28798
+ response: { type: "inbox:postMessage:ok", message }
28554
28799
  };
28555
28800
  };
28556
- const markRead = async (frame) => {
28801
+ const list = async (frame, _client, ctx) => {
28557
28802
  const { type: _t, requestId: _r, ...rest } = frame;
28558
- const args = InboxMarkReadArgsSchema.parse(rest);
28559
- manager.markRead(args.eventId);
28803
+ const args = InboxListArgsSchema.parse(rest);
28804
+ if (!ctx) {
28805
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
28806
+ }
28807
+ assertChannelAccess(ctx, args.capabilityId);
28808
+ const messages = manager.list(args.capabilityId, args.sinceCreatedAt);
28560
28809
  return {
28561
- response: { type: "inbox:markRead:ok", eventId: args.eventId }
28810
+ response: { type: "inbox:list:ok", capabilityId: args.capabilityId, messages }
28562
28811
  };
28563
28812
  };
28564
- const postMessage = async (frame, _client, ctx) => {
28813
+ const markRead = async (frame, _client, ctx) => {
28565
28814
  const { type: _t, requestId: _r, ...rest } = frame;
28566
- const args = InboxPostMessageArgsSchema.parse(rest);
28815
+ const args = InboxMarkReadArgsSchema.parse(rest);
28567
28816
  if (!ctx) {
28568
- throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
28817
+ throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
28569
28818
  }
28570
- const peer = resolvePeerPrincipal(args.peerPrincipalId);
28571
- const ev = manager.recordDirectMessage({
28572
- from: ctx.principal,
28573
- to: peer,
28574
- text: args.text
28819
+ assertChannelAccess(ctx, args.capabilityId);
28820
+ const updated = manager.markRead({
28821
+ capabilityId: args.capabilityId,
28822
+ principalId: ctx.principal.id,
28823
+ upToCreatedAt: args.upToCreatedAt
28575
28824
  });
28576
28825
  return {
28577
- response: { type: "inbox:postMessage:ok", event: ev }
28826
+ response: {
28827
+ type: "inbox:markRead:ok",
28828
+ capabilityId: args.capabilityId,
28829
+ upToCreatedAt: args.upToCreatedAt,
28830
+ updated
28831
+ }
28578
28832
  };
28579
28833
  };
28580
28834
  return {
28835
+ "inbox:postMessage": postMessage,
28581
28836
  "inbox:list": list,
28582
- "inbox:markRead": markRead,
28583
- "inbox:postMessage": postMessage
28837
+ "inbox:markRead": markRead
28584
28838
  };
28585
28839
  }
28586
28840
 
28587
28841
  // src/handlers/remote-persona.ts
28588
28842
  init_protocol();
28589
28843
  function buildRemotePersonaHandlers(deps) {
28590
- const { store } = deps;
28844
+ const { store, personStore, aliasStore } = deps;
28591
28845
  const now = deps.now ?? Date.now;
28592
28846
  const add = async (frame) => {
28593
28847
  const { type: _t, requestId: _r, ...rest } = frame;
28594
28848
  const args = RemotePersonaAddArgsSchema.parse(rest);
28849
+ if (!personStore.get(args.personId)) {
28850
+ throw new ClawdError(
28851
+ ERROR_CODES.VALIDATION_ERROR,
28852
+ `Person not found: ${args.personId}`
28853
+ );
28854
+ }
28855
+ const { personId, ...persistFields } = args;
28595
28856
  const rp = {
28596
- ...args,
28857
+ ...persistFields,
28597
28858
  addedAt: now()
28598
28859
  };
28599
28860
  try {
@@ -28607,6 +28868,15 @@ function buildRemotePersonaHandlers(deps) {
28607
28868
  }
28608
28869
  throw err;
28609
28870
  }
28871
+ try {
28872
+ aliasStore.set(rp.ownerPrincipalId, personId);
28873
+ } catch (err) {
28874
+ try {
28875
+ store.remove(rp.alias);
28876
+ } catch {
28877
+ }
28878
+ throw err;
28879
+ }
28610
28880
  return {
28611
28881
  response: { type: "remote-persona:add:ok", remotePersona: stripRemotePersonaSecret(rp) }
28612
28882
  };
@@ -28624,7 +28894,11 @@ function buildRemotePersonaHandlers(deps) {
28624
28894
  const remove = async (frame) => {
28625
28895
  const { type: _t, requestId: _r, ...rest } = frame;
28626
28896
  const args = RemotePersonaRemoveArgsSchema.parse(rest);
28897
+ const existing = store.get(args.alias);
28627
28898
  const removed = store.remove(args.alias);
28899
+ if (existing) {
28900
+ aliasStore.remove(existing.ownerPrincipalId);
28901
+ }
28628
28902
  return {
28629
28903
  response: { type: "remote-persona:remove:ok", alias: args.alias, removed }
28630
28904
  };
@@ -28643,11 +28917,11 @@ function buildWhoamiHandler(deps) {
28643
28917
  if (!ctx) {
28644
28918
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
28645
28919
  }
28646
- const owner = { ...OWNER_PRINCIPAL, displayName: deps.ownerDisplayName };
28920
+ const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
28647
28921
  let capability;
28648
28922
  if (ctx.principal.kind === "owner") {
28649
28923
  capability = {
28650
- id: "owner",
28924
+ id: deps.ownerPrincipalId,
28651
28925
  displayName: deps.ownerDisplayName,
28652
28926
  grants: ctx.grants,
28653
28927
  issuedAt: 0,
@@ -28791,28 +29065,151 @@ function buildPersonaHandlers(deps) {
28791
29065
  response: { type: "persona:deleted", personaId: args.personaId }
28792
29066
  };
28793
29067
  };
28794
- const issueToken = async (frame) => {
28795
- const args = PersonaIssueTokenArgsSchema.parse(frame);
28796
- const { token, persona } = personaManager.issueToken(args.personaId, args.label);
28797
- return {
28798
- response: { type: "persona:tokenIssued", token, persona }
28799
- };
28800
- };
28801
- const revokeToken = async (frame) => {
28802
- const args = PersonaRevokeTokenArgsSchema.parse(frame);
28803
- const persona = personaManager.revokeToken(args.personaId, args.token);
28804
- return { response: { type: "persona:info", ...persona } };
28805
- };
28806
29068
  return {
28807
29069
  "persona:create": create,
28808
29070
  "persona:list": list,
28809
29071
  "persona:get": get,
28810
29072
  "persona:update": update,
28811
- "persona:delete": del,
28812
- "persona:issueToken": issueToken,
28813
- "persona:revokeToken": revokeToken
29073
+ "persona:delete": del
29074
+ };
29075
+ }
29076
+
29077
+ // src/handlers/person.ts
29078
+ var import_node_crypto9 = require("crypto");
29079
+ init_protocol();
29080
+
29081
+ // src/person/cascade.ts
29082
+ var EMPTY_RESULT = {
29083
+ deletedCapabilityIds: [],
29084
+ removedRemoteAliases: [],
29085
+ deletedInboxEvents: 0
29086
+ };
29087
+ function cascadeDeletePerson(personId, deps) {
29088
+ if (!deps.personStore.get(personId)) return { ...EMPTY_RESULT };
29089
+ const principalIds = deps.aliasStore.list().filter((a) => a.personId === personId).map((a) => a.principalId);
29090
+ const principalSet = new Set(principalIds);
29091
+ const allCaps = deps.capabilityManager.list();
29092
+ const capsToDelete = allCaps.filter(
29093
+ (c) => principalSet.has(c.id) || c.linkedPrincipalId !== void 0 && principalSet.has(c.linkedPrincipalId)
29094
+ );
29095
+ let deletedInboxEvents = 0;
29096
+ for (const c of capsToDelete) {
29097
+ deletedInboxEvents += deps.inboxStore.list(c.id).length;
29098
+ }
29099
+ const deletedCapabilityIds = [];
29100
+ for (const c of capsToDelete) {
29101
+ if (deps.capabilityManager.delete(c.id) !== null) {
29102
+ deletedCapabilityIds.push(c.id);
29103
+ }
29104
+ }
29105
+ const allRemotes = deps.remotePersonaStore.list();
29106
+ const remotesToRemove = allRemotes.filter((r) => principalSet.has(r.ownerPrincipalId));
29107
+ const removedRemoteAliases = [];
29108
+ for (const r of remotesToRemove) {
29109
+ if (deps.remotePersonaStore.remove(r.alias)) {
29110
+ removedRemoteAliases.push(r.alias);
29111
+ }
29112
+ }
29113
+ deps.aliasStore.removeAllByPerson(personId);
29114
+ deps.personStore.remove(personId);
29115
+ return { deletedCapabilityIds, removedRemoteAliases, deletedInboxEvents };
29116
+ }
29117
+
29118
+ // src/handlers/person.ts
29119
+ function buildPersonHandlers(deps) {
29120
+ const now = deps.now ?? Date.now;
29121
+ const genId = deps.genId ?? defaultGenId2;
29122
+ const list = async () => {
29123
+ const persons = deps.personStore.list();
29124
+ const aliases = deps.aliasStore.list();
29125
+ const personIdByPrincipal = new Map(aliases.map((a) => [a.principalId, a.personId]));
29126
+ const allCaps = deps.capabilityManager.list();
29127
+ const allRemotes = deps.remotePersonaStore.list();
29128
+ const out = persons.map((p2) => {
29129
+ const linkedCapabilityIds = [];
29130
+ for (const c of allCaps) {
29131
+ const key = c.linkedPrincipalId ?? c.id;
29132
+ if (personIdByPrincipal.get(key) === p2.id) linkedCapabilityIds.push(c.id);
29133
+ }
29134
+ const linkedRemoteAliases = [];
29135
+ for (const r of allRemotes) {
29136
+ if (personIdByPrincipal.get(r.ownerPrincipalId) === p2.id) linkedRemoteAliases.push(r.alias);
29137
+ }
29138
+ return { ...p2, linkedCapabilityIds, linkedRemoteAliases };
29139
+ });
29140
+ return { response: { type: "person:list:ok", persons: out } };
29141
+ };
29142
+ const create = async (frame) => {
29143
+ const { type: _t, requestId: _r, ...rest } = frame;
29144
+ const args = PersonCreateArgsSchema.parse(rest);
29145
+ const t = now();
29146
+ const person = {
29147
+ id: genId(),
29148
+ displayName: args.displayName,
29149
+ ...args.notes !== void 0 ? { notes: args.notes } : {},
29150
+ dmEnabled: args.dmEnabled ?? true,
29151
+ createdAt: t,
29152
+ updatedAt: t
29153
+ };
29154
+ deps.personStore.add(person);
29155
+ return { response: { type: "person:create:ok", person } };
29156
+ };
29157
+ const update = async (frame) => {
29158
+ const { type: _t, requestId: _r, ...rest } = frame;
29159
+ const args = PersonUpdateArgsSchema.parse(rest);
29160
+ const existing = deps.personStore.get(args.id);
29161
+ if (!existing) {
29162
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `Person not found: ${args.id}`);
29163
+ }
29164
+ const t = now();
29165
+ deps.personStore.update(
29166
+ args.id,
29167
+ {
29168
+ ...args.displayName !== void 0 ? { displayName: args.displayName } : {},
29169
+ ...args.notes !== void 0 ? { notes: args.notes } : {},
29170
+ ...args.dmEnabled !== void 0 ? { dmEnabled: args.dmEnabled } : {}
29171
+ },
29172
+ t
29173
+ );
29174
+ const next = deps.personStore.get(args.id);
29175
+ return { response: { type: "person:update:ok", person: next } };
29176
+ };
29177
+ const del = async (frame) => {
29178
+ const { type: _t, requestId: _r, ...rest } = frame;
29179
+ const args = PersonDeleteArgsSchema.parse(rest);
29180
+ if (!deps.personStore.get(args.id)) {
29181
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `Person not found: ${args.id}`);
29182
+ }
29183
+ const result = cascadeDeletePerson(args.id, deps);
29184
+ return {
29185
+ response: {
29186
+ type: "person:delete:ok",
29187
+ deletedCapabilityIds: result.deletedCapabilityIds,
29188
+ removedRemoteAliases: result.removedRemoteAliases,
29189
+ deletedInboxEvents: result.deletedInboxEvents
29190
+ }
29191
+ };
29192
+ };
29193
+ const link = async (frame) => {
29194
+ const { type: _t, requestId: _r, ...rest } = frame;
29195
+ const args = PersonLinkArgsSchema.parse(rest);
29196
+ if (!deps.personStore.get(args.personId)) {
29197
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `Person not found: ${args.personId}`);
29198
+ }
29199
+ deps.aliasStore.set(args.principalId, args.personId);
29200
+ return { response: { type: "person:link:ok" } };
29201
+ };
29202
+ return {
29203
+ "person:list": list,
29204
+ "person:create": create,
29205
+ "person:update": update,
29206
+ "person:delete": del,
29207
+ "person:link": link
28814
29208
  };
28815
29209
  }
29210
+ function defaultGenId2() {
29211
+ return `p_${(0, import_node_crypto9.randomUUID)()}`;
29212
+ }
28816
29213
 
28817
29214
  // src/handlers/attachment.ts
28818
29215
  init_protocol();
@@ -28907,23 +29304,11 @@ function buildAttachmentHandlers(deps) {
28907
29304
  const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
28908
29305
  return { response: { type: "attachment.groupList", entries } };
28909
29306
  };
28910
- const groupListPersona = async (frame) => {
28911
- if (!deps.groupFileStore) {
28912
- throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
28913
- }
28914
- const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
28915
- if (!parsed.success) {
28916
- throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
28917
- }
28918
- const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
28919
- return { response: { type: "attachment.groupListPersona", perSession } };
28920
- };
28921
29307
  return {
28922
29308
  "attachment.signUrl": signUrl,
28923
29309
  "attachment.groupAdd": groupAdd,
28924
29310
  "attachment.groupRemove": groupRemove,
28925
- "attachment.groupList": groupList,
28926
- "attachment.groupListPersona": groupListPersona
29311
+ "attachment.groupList": groupList
28927
29312
  };
28928
29313
  }
28929
29314
 
@@ -28943,15 +29328,33 @@ function buildMethodHandlers(deps) {
28943
29328
  }),
28944
29329
  ...buildCapabilityHandlers({
28945
29330
  manager: deps.capabilityManager,
28946
- getShareBaseUrl: deps.getShareBaseUrl
29331
+ getShareBaseUrl: deps.getShareBaseUrl,
29332
+ personStore: deps.personStore,
29333
+ aliasStore: deps.personAliasStore
28947
29334
  }),
28948
29335
  ...buildInboxHandlers({
28949
29336
  manager: deps.inboxManager,
28950
- capabilityRegistry: deps.capabilityRegistry
29337
+ personStore: deps.personStore,
29338
+ aliasStore: deps.personAliasStore
29339
+ }),
29340
+ ...buildRemotePersonaHandlers({
29341
+ store: deps.remotePersonaStore,
29342
+ personStore: deps.personStore,
29343
+ aliasStore: deps.personAliasStore
29344
+ }),
29345
+ ...buildPersonHandlers({
29346
+ personStore: deps.personStore,
29347
+ aliasStore: deps.personAliasStore,
29348
+ capabilityManager: deps.capabilityManager,
29349
+ remotePersonaStore: deps.remotePersonaStore,
29350
+ // inbox channel cascade: person:delete 时用 inboxStore.list 统计每个 cap channel
29351
+ // 消息数 sum 起来作为 deletedInboxEvents 返回 (实际删除走 capability:delete
29352
+ // onDeleted hook → inboxManager.removeChannel, cascade 不主动 rm 文件).
29353
+ inboxStore: deps.inboxStore
28951
29354
  }),
28952
- ...buildRemotePersonaHandlers({ store: deps.remotePersonaStore }),
28953
29355
  whoami: buildWhoamiHandler({
28954
29356
  ownerDisplayName: deps.ownerDisplayName,
29357
+ ownerPrincipalId: deps.ownerPrincipalId,
28955
29358
  personaStore: deps.personaStore,
28956
29359
  capabilityManager: deps.capabilityManager
28957
29360
  }),
@@ -28977,17 +29380,23 @@ var METHOD_GRANT_MAP = {
28977
29380
  // ---- capability platform(admin-only,本 PR 新增) ----
28978
29381
  "capability:issue": ADMIN_ANY,
28979
29382
  "capability:list": ADMIN_ANY,
28980
- "capability:revoke": ADMIN_ANY,
28981
- // ---- inbox 跨用户通知 (Phase 3 admin-only, owner 调; Phase 4 加 postMessage) ----
28982
- "inbox:list": ADMIN_ANY,
28983
- "inbox:markRead": ADMIN_ANY,
28984
- // Phase 4 Task 4.2: DM 是 capability 自带能力 (plan §2),
28985
- // 任何 principal (owner / guest) 可调, 不通过 grant 表达.
29383
+ "capability:delete": ADMIN_ANY,
29384
+ // ---- inbox: channel-based IM (capability platform v3) ----
29385
+ // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
29386
+ // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
29387
+ "inbox:list": { kind: "public" },
29388
+ "inbox:markRead": { kind: "public" },
28986
29389
  "inbox:postMessage": { kind: "public" },
28987
29390
  // Phase 4 Task 4.3: 远程 persona 仅 owner 管理 (admin-only)
28988
29391
  "remote-persona:add": ADMIN_ANY,
28989
29392
  "remote-persona:list": ADMIN_ANY,
28990
29393
  "remote-persona:remove": ADMIN_ANY,
29394
+ // Person identity Phase 1: 联系人本质是 owner 视角的 People 列表,仅 owner 管理。
29395
+ "person:list": ADMIN_ANY,
29396
+ "person:create": ADMIN_ANY,
29397
+ "person:update": ADMIN_ANY,
29398
+ "person:delete": ADMIN_ANY,
29399
+ "person:link": ADMIN_ANY,
28991
29400
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
28992
29401
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
28993
29402
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -29034,17 +29443,14 @@ var METHOD_GRANT_MAP = {
29034
29443
  "persona:get": ADMIN_ANY,
29035
29444
  "persona:update": ADMIN_ANY,
29036
29445
  "persona:delete": ADMIN_ANY,
29037
- "persona:issueToken": ADMIN_ANY,
29038
- "persona:revokeToken": ADMIN_ANY,
29039
29446
  "session:pty:input": ADMIN_ANY,
29040
29447
  "session:pty:resize": ADMIN_ANY,
29041
- // file-sharing attachment.*:handler 内部已有 requireOwner(HTTP 路径同),dispatcher 这里
29042
- // admin-only 兜底(双保险,wire-level 也拦)
29448
+ // file-sharing attachment.* RPC:dispatcher admin-only 兜底(wire-level 拦),
29449
+ // 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
29043
29450
  "attachment.signUrl": ADMIN_ANY,
29044
29451
  "attachment.groupAdd": ADMIN_ANY,
29045
29452
  "attachment.groupRemove": ADMIN_ANY,
29046
- "attachment.groupList": ADMIN_ANY,
29047
- "attachment.groupListPersona": ADMIN_ANY
29453
+ "attachment.groupList": ADMIN_ANY
29048
29454
  };
29049
29455
  function computeGrantForFrame(method, frame) {
29050
29456
  const rule = METHOD_GRANT_MAP[method];
@@ -29074,13 +29480,20 @@ async function startDaemon(config) {
29074
29480
  if (pre.status === "stale") {
29075
29481
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
29076
29482
  }
29483
+ const auth = loadOrCreateAuth({ dataDir: config.dataDir });
29077
29484
  let resolvedAuthToken = null;
29078
29485
  if (config.authToken && config.authToken.trim()) {
29079
29486
  resolvedAuthToken = config.authToken.trim();
29080
29487
  } else if (config.tunnel) {
29081
- resolvedAuthToken = loadOrCreateAuthToken({ dataDir: config.dataDir });
29488
+ resolvedAuthToken = auth.token;
29082
29489
  }
29490
+ const ownerPrincipalId = auth.ownerPrincipalId;
29491
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
29083
29492
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
29493
+ const inboxStore = new InboxStore(config.dataDir);
29494
+ const inboxManager = new InboxManager(inboxStore, (capabilityId, frame) => {
29495
+ wsServer?.broadcastToCapabilityChannel(capabilityId, frame);
29496
+ });
29084
29497
  const capabilityStore = new CapabilityStore(config.dataDir);
29085
29498
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
29086
29499
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
@@ -29091,46 +29504,66 @@ async function startDaemon(config) {
29091
29504
  token
29092
29505
  });
29093
29506
  },
29094
- onRevoked: (cap) => {
29507
+ onDeleted: (cap) => {
29508
+ const deletedAt = Date.now();
29095
29509
  wsServer?.broadcastToOwners({
29096
- type: "capability:tokenRevoked",
29510
+ type: "capability:tokenDeleted",
29097
29511
  capabilityId: cap.id,
29098
- revokedAt: cap.revokedAt
29512
+ deletedAt
29099
29513
  });
29100
29514
  wsServer?.closeConnectionsByCapability(cap.id);
29101
29515
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
29102
29516
  if (cleanup.removed.length > 0) {
29103
- logger.info("capability revoke cascade: guest sessions removed", {
29517
+ logger.info("capability delete cascade: guest sessions removed", {
29104
29518
  capabilityId: cap.id,
29105
29519
  removedDirs: cleanup.removed
29106
29520
  });
29107
29521
  }
29522
+ inboxManager.removeChannel(cap.id);
29108
29523
  }
29109
29524
  });
29110
- const inboxStore = new InboxStore(config.dataDir);
29111
29525
  const remotePersonaStore = new RemotePersonaStore(config.dataDir);
29112
- const inboxManager = new InboxManager(inboxStore, (frame) => {
29113
- if (frame.event.kind === "direct-message") {
29114
- const fromId = frame.event.fromPrincipal.id;
29115
- const toId = frame.event.toPrincipal.id;
29116
- wsServer?.broadcastToPrincipal(fromId, frame);
29117
- if (toId !== fromId) wsServer?.broadcastToPrincipal(toId, frame);
29118
- return;
29119
- }
29120
- wsServer?.broadcastToOwners(frame);
29121
- });
29526
+ const personStore = new PersonStore(config.dataDir);
29527
+ const personAliasStore = new PersonAliasStore(config.dataDir);
29122
29528
  let wsServer = null;
29123
29529
  const authGate = authMode === "first-message" ? new AuthGate({
29124
29530
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
29125
29531
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
29126
29532
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
29127
29533
  expectedToken: resolvedAuthToken,
29128
- authenticate: (t) => authenticate(t, {
29129
- isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
29130
- capabilityRegistry
29131
- }),
29534
+ authenticate: (t, selfPrincipalId) => {
29535
+ const r = authenticate(t, {
29536
+ isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
29537
+ ownerPrincipalId,
29538
+ ownerDisplayName,
29539
+ capabilityRegistry
29540
+ });
29541
+ if (r.ok && selfPrincipalId && r.context.principal.kind === "guest" && r.context.capabilityId) {
29542
+ const capId = r.context.capabilityId;
29543
+ const cap = capabilityManager.findById(capId);
29544
+ if (cap && cap.linkedPrincipalId === void 0) {
29545
+ capabilityManager.updateLinkedPrincipalId(capId, selfPrincipalId);
29546
+ try {
29547
+ personAliasStore.rekey(capId, selfPrincipalId);
29548
+ } catch (e) {
29549
+ logger.warn("auth.alias.rekey.failed", {
29550
+ capId,
29551
+ selfPrincipalId,
29552
+ err: e.message
29553
+ });
29554
+ }
29555
+ } else if (cap && cap.linkedPrincipalId !== selfPrincipalId) {
29556
+ logger.warn("auth.selfPrincipalId.mismatch", {
29557
+ capId,
29558
+ expected: cap.linkedPrincipalId,
29559
+ got: selfPrincipalId
29560
+ });
29561
+ }
29562
+ }
29563
+ return r;
29564
+ },
29132
29565
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
29133
- buildOwnerContext: ownerContext,
29566
+ buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
29134
29567
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
29135
29568
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
29136
29569
  }) : null;
@@ -29157,7 +29590,6 @@ async function startDaemon(config) {
29157
29590
  } else {
29158
29591
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
29159
29592
  }
29160
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
29161
29593
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
29162
29594
  const manager = new SessionManager({
29163
29595
  store,
@@ -29314,24 +29746,29 @@ async function startDaemon(config) {
29314
29746
  getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
29315
29747
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
29316
29748
  ownerDisplayName,
29749
+ // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
29750
+ ownerPrincipalId,
29317
29751
  personaStore,
29318
- // Phase 4 Task 4.2: inbox:postMessage 要查 capabilityRegistry 解析 peer Principal
29752
+ // capability handler 也用 (capability:issue registry / capability:list)
29319
29753
  capabilityRegistry,
29320
- // Phase 3 Task 3.4: inbox:list/markRead handler 依赖
29754
+ // capability platform v3 inbox: handler 接 manager (post/list/markRead),
29755
+ // cascade 接 store (list 统计 deletedInboxEvents).
29321
29756
  inboxManager,
29757
+ inboxStore,
29322
29758
  // Phase 4 Task 4.3: remote-persona:* handler 依赖 (本地存储, v1 不接 outgoing WS)
29323
- remotePersonaStore
29759
+ remotePersonaStore,
29760
+ // Person identity Phase 1 (B13): person:* RPC handlers + 给 capability:issue /
29761
+ // remote-persona:add atomic 写 alias / cascade 删 Person 用
29762
+ personStore,
29763
+ personAliasStore
29324
29764
  });
29325
29765
  const authResolver = new AuthContextResolver({
29326
- ownerToken: resolvedAuthToken,
29327
- personaRegistry
29766
+ ownerToken: resolvedAuthToken
29328
29767
  });
29329
29768
  const httpRouter = createHttpRouter({
29330
29769
  authResolver,
29331
29770
  daemonVersion: version,
29332
29771
  logger,
29333
- personaStore,
29334
- groupFileStore,
29335
29772
  sessionStore: store,
29336
29773
  // /files HMAC verify 用同一份 owner token 做 secret(与 attachment.signUrl 同源)
29337
29774
  getSignSecret: () => resolvedAuthToken ?? null
@@ -29340,6 +29777,8 @@ async function startDaemon(config) {
29340
29777
  host: config.host,
29341
29778
  port: config.port,
29342
29779
  logger,
29780
+ // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
29781
+ ownerPrincipalId,
29343
29782
  readyFrameBuilder: (ctx) => buildReadyFrame(
29344
29783
  {
29345
29784
  manager,
@@ -29355,6 +29794,19 @@ async function startDaemon(config) {
29355
29794
  ),
29356
29795
  protocolVersion: PROTOCOL_VERSION,
29357
29796
  authGate: authGate ?? void 0,
29797
+ // noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
29798
+ // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
29799
+ // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
29800
+ // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
29801
+ tryVerifyCapabilityToken: (token) => {
29802
+ const v2 = capabilityRegistry.verifyToken(token);
29803
+ if (!v2.ok) return null;
29804
+ return {
29805
+ principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
29806
+ grants: v2.capability.grants,
29807
+ capabilityId: v2.capability.id
29808
+ };
29809
+ },
29358
29810
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
29359
29811
  httpRequestHandler: httpRouter,
29360
29812
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
@@ -29407,7 +29859,7 @@ async function startDaemon(config) {
29407
29859
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
29408
29860
  const handler = handlers[type];
29409
29861
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
29410
- const ctx = wsServer.getClientContext(client.id) ?? ownerContext();
29862
+ const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
29411
29863
  const verdict = computeGrantForFrame(type, frame);
29412
29864
  if (verdict.kind === "check") {
29413
29865
  const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);