@clawos-dev/clawd 0.2.123-beta.244.5e5107f → 0.2.123

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.
package/dist/cli.cjs CHANGED
@@ -132,11 +132,15 @@ var init_methods = __esm({
132
132
  "attachment.groupRemove",
133
133
  "attachment.groupList",
134
134
  // ---- capability:* (capability platform 鉴权底座) ----
135
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal token / 邀请码全链路删除——
136
- // device:connect 已完整替代(中心 server 签发自包含 connect token)。capability:list / capability:delete
137
- // 保留:调试 + whoami 反查 + 撤销级联 + 清旧 per-peer cap。personal-cap:get 已下线。
135
+ // global personal token 模型 (2026-05-25): UI 不再调 capability:issue —— daemon 启动时
136
+ // loadOrCreatePersonalCapability 自动创建 1 fixed-grants personal capability,
137
+ // 所有联系人共用 personal token。capability:list / capability:delete 保留 (供调试 /
138
+ // 老板手动清旧 cap);capability:issue / capability:bindPeer 已下线。
138
139
  "capability:list",
139
140
  "capability:delete",
141
+ // global personal token (2026-05-25): UI invite/accept dialog 挂载即调,拿 personal
142
+ // token + capability + shareBaseUrl 用于拼 inviteUrl / autoAttach 反向推。owner-only。
143
+ "personal-cap:get",
140
144
  // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
141
145
  // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
142
146
  // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
@@ -144,12 +148,14 @@ var init_methods = __esm({
144
148
  "inbox:list",
145
149
  "inbox:markRead",
146
150
  "inbox:postMessage",
147
- // ---- contact:* (联系人列表) ----
148
- // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名为 contact:*——Phase 2 后
149
- // 联系人身份由中心 server introspect 背书,不再是"对方颁发的 capability"。
150
- // 写入路径:device:connect store + 自动反向(决策 #11);list / remove 作联系人列表读删。
151
- "contact:list",
152
- "contact:remove",
151
+ // ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
152
+ // owner 入口: list / add / remove
153
+ // guest 端: autoAttach (B 接 A 的 token 后通过 cap channel ws 推 cap 给 A)
154
+ // 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
155
+ "received-capability:list",
156
+ "received-capability:add",
157
+ "received-capability:remove",
158
+ "received-capability:autoAttach",
153
159
  // ---- whoami (v2 capability platform) ----
154
160
  // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
155
161
  // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
@@ -175,17 +181,6 @@ var init_methods = __esm({
175
181
  // 无 subscription 持久化:guest 端身份完全来自文件系统 + reconciled publishedExtensions.
176
182
  "extension.install-from-channel",
177
183
  "extension.update-from-channel",
178
- // ---- auth:* 飞书统一身份 Phase 1 (spec 2026-06-01-feishu-identity-design) ----
179
- // owner-only:daemon loopback 登录流程(auth:login:start 起 state → HTTP /auth/callback
180
- // 回调落盘 → auth:login:done 事件)。中央字面量见 protocol/feishu-auth.ts FEISHU_AUTH_METHODS。
181
- "auth:login:start",
182
- "auth:getIdentity",
183
- "auth:logout",
184
- // ---- device:* 设备目录 + 连接(决策 #15:deviceId + 自包含 token) ----
185
- // owner-only:公共设备目录 + audience 绑定设备的签名 token 连接(daemon 代理中心 server)。
186
- // 中央字面量见 protocol/feishu-auth.ts DEVICE_METHODS。
187
- "device:list",
188
- "device:connect",
189
184
  // ---- app-builder Project (spec 2026-06-01 picker → 2026-06-02 单栏默认 refactor) ----
190
185
  // persona-app-builder Project 管理;schemas: ProjectMetadataSchema + 5 个 args/result 对。
191
186
  // listProjects: readdir projects/,按目录读 .clawd-project.json
@@ -279,10 +274,6 @@ var init_errors = __esm({
279
274
  UNAUTHORIZED: "UNAUTHORIZED",
280
275
  FORBIDDEN: "FORBIDDEN",
281
276
  INVALID_PARAM: "INVALID_PARAM",
282
- // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
283
- // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
284
- // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
285
- FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED",
286
277
  // app-builder 单栏默认 refactor (spec 2026-06-02-app-builder-single-pane-default-design):
287
278
  // appBuilder:createProject 加 personaId + session 未绑校验后抛的两个错码。
288
279
  WRONG_PERSONA: "WRONG_PERSONA",
@@ -716,8 +707,8 @@ var init_parseUtil = __esm({
716
707
  init_errors2();
717
708
  init_en();
718
709
  makeIssue = (params) => {
719
- const { data, path: path60, errorMaps, issueData } = params;
720
- const fullPath = [...path60, ...issueData.path || []];
710
+ const { data, path: path59, errorMaps, issueData } = params;
711
+ const fullPath = [...path59, ...issueData.path || []];
721
712
  const fullIssue = {
722
713
  ...issueData,
723
714
  path: fullPath
@@ -1028,11 +1019,11 @@ var init_types = __esm({
1028
1019
  init_parseUtil();
1029
1020
  init_util();
1030
1021
  ParseInputLazyPath = class {
1031
- constructor(parent, value, path60, key) {
1022
+ constructor(parent, value, path59, key) {
1032
1023
  this._cachedPath = [];
1033
1024
  this.parent = parent;
1034
1025
  this.data = value;
1035
- this._path = path60;
1026
+ this._path = path59;
1036
1027
  this._key = key;
1037
1028
  }
1038
1029
  get path() {
@@ -4673,41 +4664,53 @@ var init_schemas = __esm({
4673
4664
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4674
4665
  chatId: external_exports.string().min(1).optional(),
4675
4666
  /**
4676
- * 创建该 session 的设备 id(决策 #16:从 ctx.principal.id 派生 = deviceId)。
4677
- * UI self 判定收敛为单一比较 `!creatorPrincipalId || creatorPrincipalId === 本机 deviceId`
4678
- * 「我自己创建」vs「别的设备接入创建」。MyPersonas 二级分组按 creatorPrincipalId(设备)
4679
- * 分桶、按 contact(deviceId→ownerName) 反查显示名。
4667
+ * 创建该 session principal id(owner uuid 或 guest cap.id),从 session:create handler
4668
+ * ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4669
+ * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4670
+ * 后者再通过 PersonAlias 表反查 personId 二级分组。
4680
4671
  *
4681
- * 命名保留 creatorPrincipalId(命名债,语义已是 creatorDeviceId);值由 connect token
4682
- * subDevice 背书(guest ctx)或本机 deviceId(owner ctx)。optional 兼容老 session。
4683
- *
4684
- * 决策 #16 删除了 creatorOwnerPrincipalId(personal token #735 遗留)——当年 guest 的
4685
- * principal.id 都是同张 cap.id 区分不了人才加它;现在 principal.id = 设备 deviceId、
4686
- * 归属人由 contact(deviceId→ownerId) 反查,该字段理由消失。
4672
+ * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4687
4673
  */
4688
4674
  creatorPrincipalId: external_exports.string().min(1).optional(),
4689
4675
  /**
4690
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4691
- * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
4692
- * 字段值 = 远端 daemon 的 deviceId(决策 #16:朋友设备标识,即我接入的那台机器)。
4676
+ * 创建该 session 的接入者 owner identity(auth selfPrincipalId)。global personal token
4677
+ * (#735) creatorPrincipalId guest ctx 永远是同一张 personal cap.id,UI 端无法区分
4678
+ * 接入者;这个字段把"是哪个人"的信息固定下来。
4679
+ *
4680
+ * 派生规则:
4681
+ * - owner ctx 创建:= 本机 ownerPrincipalId
4682
+ * - guest ctx + auth 帧带 selfPrincipalId:= ctx.peerOwnerPrincipalId
4683
+ * - 旧 per-peer cap 路径 / 老 session:undefined(UI 视作 self 兼容)
4684
+ *
4685
+ * UI 用法:
4686
+ * - Recents/Active/Pinned 过滤:`!creatorOwnerPrincipalId || === ownerPrincipalId` 才显示
4687
+ * - MyPersonas 二级分组:non-self → 按 contacts 反查 displayName 分桶
4688
+ *
4689
+ * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4690
+ */
4691
+ creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4692
+ /**
4693
+ * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4694
+ * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4695
+ * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4693
4696
  *
4694
4697
  * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4695
4698
  * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4696
4699
  *
4697
4700
  * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4698
- * - 非空 → 我的会话在该 deviceId 朋友的 daemon 上跑,本机为持久化副本
4701
+ * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4699
4702
  *
4700
- * 四类 session 取值矩阵(决策 #16 deviceId 版,creatorOwnerPrincipalId 列已删):
4701
- * | 形态 | creatorPrincipalId(=设备) | originOwnerPrincipalId(=朋友设备) |
4702
- * |-------------------------------------|--------------------------|----------------------------------|
4703
- * | 本机 owner 自创 | 本机 deviceId | undefined |
4704
- * | 本机被别的设备接入创建 | 接入者 deviceId(token 背书) | undefined |
4705
- * | 我在朋友 daemon 上创建(mirror 回本机) | 我的 deviceId | 朋友 deviceId |
4703
+ * `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4704
+ * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4705
+ * |-------------------------------------|--------------------------|-------------------------|------------------------|
4706
+ * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4707
+ * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4708
+ * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4706
4709
  *
4707
4710
  * UI 用法:
4708
4711
  * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4709
4712
  * - 行内显示 `@<朋友>` chip 暗示位置
4710
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端(按 deviceId)
4713
+ * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4711
4714
  *
4712
4715
  * daemon 用法:
4713
4716
  * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
@@ -5141,14 +5144,18 @@ var init_schemas = __esm({
5141
5144
  token: external_exports.string().min(1),
5142
5145
  scheme: external_exports.literal("bearer").optional(),
5143
5146
  /**
5144
- * 决策 #16:自报身份字段(selfPrincipalId / selfDisplayName / selfDeviceId)全部删除——
5145
- * guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
5146
- * (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
5147
- *
5148
- * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5149
- * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5147
+ * guest ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5148
+ * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5149
+ * cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5150
+ * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5150
5151
  */
5151
- selfUrl: external_exports.string().min(1).optional()
5152
+ selfPrincipalId: external_exports.string().min(1).optional(),
5153
+ /**
5154
+ * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5155
+ * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5156
+ * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5157
+ */
5158
+ selfDisplayName: external_exports.string().optional()
5152
5159
  });
5153
5160
  AuthOkFrameSchema = external_exports.object({
5154
5161
  type: external_exports.literal("auth:ok"),
@@ -5417,7 +5424,7 @@ function stripSecretHash(cap) {
5417
5424
  const { secretHash: _hash, ...wire } = cap;
5418
5425
  return wire;
5419
5426
  }
5420
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5427
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5421
5428
  var init_capability = __esm({
5422
5429
  "../protocol/src/capability.ts"() {
5423
5430
  "use strict";
@@ -5469,11 +5476,7 @@ var init_capability = __esm({
5469
5476
  owner: external_exports.object({
5470
5477
  id: external_exports.string(),
5471
5478
  kind: external_exports.enum(["owner", "guest"]),
5472
- displayName: external_exports.string(),
5473
- /** 归属人 id(决策 #16:TTC unique_id 等,落 contact.ownerId) */
5474
- ownerId: external_exports.string(),
5475
- /** 身份来源(决策 #16:'ttc'/'google',落 contact.provider) */
5476
- provider: external_exports.string()
5479
+ displayName: external_exports.string()
5477
5480
  }).strict(),
5478
5481
  capability: CapabilityWireSchema,
5479
5482
  grantedPersonas: external_exports.array(
@@ -5489,6 +5492,13 @@ var init_capability = __esm({
5489
5492
  actions: Object.freeze(["read", "send"])
5490
5493
  })
5491
5494
  ]);
5495
+ PersonalCapGetResponseSchema = external_exports.object({
5496
+ type: external_exports.literal("personal-cap:get:ok"),
5497
+ token: external_exports.string().min(1),
5498
+ capability: CapabilityWireSchema,
5499
+ /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5500
+ shareBaseUrl: external_exports.string().min(1)
5501
+ }).strict();
5492
5502
  }
5493
5503
  });
5494
5504
 
@@ -5500,80 +5510,83 @@ var init_inbox = __esm({
5500
5510
  init_zod();
5501
5511
  InboxMessageSchema = external_exports.object({
5502
5512
  id: external_exports.string().min(1),
5503
- peerDeviceId: external_exports.string().min(1),
5504
- senderDeviceId: external_exports.string().min(1),
5513
+ peerOwnerId: external_exports.string().min(1),
5514
+ senderPrincipalId: external_exports.string().min(1),
5505
5515
  text: external_exports.string().min(1),
5506
5516
  createdAt: external_exports.number().int().nonnegative(),
5507
5517
  readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5508
5518
  }).strict();
5509
5519
  InboxPostMessageArgsSchema = external_exports.object({
5510
- peerDeviceId: external_exports.string().min(1).optional(),
5520
+ peerOwnerId: external_exports.string().min(1).optional(),
5511
5521
  id: external_exports.string().min(1),
5512
5522
  text: external_exports.string().min(1),
5513
5523
  createdAt: external_exports.number().int().nonnegative()
5514
5524
  }).strict();
5515
5525
  InboxListArgsSchema = external_exports.object({
5516
- peerDeviceId: external_exports.string().min(1),
5526
+ peerOwnerId: external_exports.string().min(1),
5517
5527
  sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5518
5528
  }).strict();
5519
5529
  InboxMarkReadArgsSchema = external_exports.object({
5520
- peerDeviceId: external_exports.string().min(1),
5530
+ peerOwnerId: external_exports.string().min(1),
5521
5531
  upToCreatedAt: external_exports.number().int().nonnegative()
5522
5532
  }).strict();
5523
5533
  }
5524
5534
  });
5525
5535
 
5526
- // ../protocol/src/contact.ts
5527
- var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema;
5528
- var init_contact = __esm({
5529
- "../protocol/src/contact.ts"() {
5536
+ // ../protocol/src/received-capability.ts
5537
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5538
+ var init_received_capability = __esm({
5539
+ "../protocol/src/received-capability.ts"() {
5530
5540
  "use strict";
5531
5541
  init_zod();
5532
5542
  init_capability();
5533
- ContactSchema = external_exports.object({
5534
- /**
5535
- * 对方设备标识(主键):对方 daemon 自己生成并持久化的标识(auth.json deviceId,
5536
- * 如 dev-<uuid>),provider 无关。同 deviceId 重复 upsert 覆盖。
5537
- */
5538
- deviceId: external_exports.string().min(1),
5539
- /**
5540
- * 归属人 id(决策 #16):TTC unique_id 等,由 connect token subOwner 签名背书。
5541
- * 与 provider 一起唯一标识一个人。DM 不再按它路由(路由用 deviceId),仅显示/分组用。
5542
- */
5543
- ownerId: external_exports.string().min(1),
5544
- /** 身份来源(决策 #16):'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...,token 背书 */
5545
- provider: external_exports.string().min(1),
5546
- /** 联系人显示名 */
5547
- displayName: external_exports.string(),
5548
- /** 该设备可达的 ws/tunnel 地址 */
5543
+ ReceivedCapabilitySchema = external_exports.object({
5544
+ ownerPrincipalId: external_exports.string().min(1),
5545
+ ownerDisplayName: external_exports.string(),
5549
5546
  remoteUrl: external_exports.string().min(1),
5550
- // 自动反向建立联系人(spec 决策 #11):被连接方本地验票成功后自动落 A 为
5551
- // 联系人,但它不持有 A 颁发的 token(A 从未给它换票)→ connectToken 为空串。
5552
- // device:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5553
- // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5554
- connectToken: external_exports.string(),
5547
+ capabilityId: external_exports.string().min(1),
5548
+ capabilityToken: external_exports.string().min(1),
5555
5549
  grants: external_exports.array(GrantSchema),
5556
- addedAt: external_exports.number().int().nonnegative()
5550
+ receivedAt: external_exports.number().int().nonnegative()
5551
+ }).strict();
5552
+ ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5553
+ ReceivedCapabilityAddArgsSchema = external_exports.object({
5554
+ remoteUrl: external_exports.string().min(1),
5555
+ token: external_exports.string().min(1)
5556
+ }).strict();
5557
+ ReceivedCapabilityAddOkSchema = external_exports.object({
5558
+ type: external_exports.literal("received-capability:add:ok"),
5559
+ receivedCap: ReceivedCapabilityWireSchema
5557
5560
  }).strict();
5558
- ContactWireSchema = ContactSchema;
5559
- ContactRemoveArgsSchema = external_exports.object({
5560
- deviceId: external_exports.string().min(1)
5561
+ ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5562
+ ownerPrincipalId: external_exports.string().min(1)
5561
5563
  }).strict();
5562
- ContactRemoveOkSchema = external_exports.object({
5563
- type: external_exports.literal("contact:remove:ok"),
5564
- deviceId: external_exports.string().min(1)
5564
+ ReceivedCapabilityRemoveOkSchema = external_exports.object({
5565
+ type: external_exports.literal("received-capability:remove:ok"),
5566
+ ownerPrincipalId: external_exports.string().min(1)
5567
+ }).strict();
5568
+ ReceivedCapabilityListOkSchema = external_exports.object({
5569
+ type: external_exports.literal("received-capability:list:ok"),
5570
+ receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5571
+ }).strict();
5572
+ ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5573
+ ownerPrincipalId: external_exports.string().min(1),
5574
+ ownerDisplayName: external_exports.string(),
5575
+ remoteUrl: external_exports.string().min(1),
5576
+ capabilityId: external_exports.string().min(1),
5577
+ token: external_exports.string().min(1),
5578
+ grants: external_exports.array(GrantSchema)
5565
5579
  }).strict();
5566
- ContactListOkSchema = external_exports.object({
5567
- type: external_exports.literal("contact:list:ok"),
5568
- contacts: external_exports.array(ContactWireSchema)
5580
+ ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5581
+ type: external_exports.literal("received-capability:autoAttach:ok")
5569
5582
  }).strict();
5570
- ContactAddedFrameSchema = external_exports.object({
5571
- type: external_exports.literal("contact:added"),
5572
- contact: ContactWireSchema
5583
+ ReceivedCapabilityAddedFrameSchema = external_exports.object({
5584
+ type: external_exports.literal("received-capability:added"),
5585
+ receivedCap: ReceivedCapabilityWireSchema
5573
5586
  }).strict();
5574
- ContactRemovedFrameSchema = external_exports.object({
5575
- type: external_exports.literal("contact:removed"),
5576
- deviceId: external_exports.string().min(1)
5587
+ ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5588
+ type: external_exports.literal("received-capability:removed"),
5589
+ ownerPrincipalId: external_exports.string().min(1)
5577
5590
  }).strict();
5578
5591
  }
5579
5592
  });
@@ -5777,107 +5790,6 @@ var init_extension = __esm({
5777
5790
  }
5778
5791
  });
5779
5792
 
5780
- // ../protocol/src/feishu-auth.ts
5781
- var FEISHU_GATED_METHODS, DeviceEntrySchema, DeviceListResponseSchema, DeviceConnectArgsSchema, DeviceConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5782
- var init_feishu_auth = __esm({
5783
- "../protocol/src/feishu-auth.ts"() {
5784
- "use strict";
5785
- init_zod();
5786
- FEISHU_GATED_METHODS = [
5787
- // capability(调试 / 撤销,仍属远程身份体系)
5788
- "capability:list",
5789
- "capability:delete",
5790
- // 联系人列表(device:connect 落同一 store;list 读 / remove 删)
5791
- "contact:list",
5792
- "contact:remove",
5793
- // DM / 跨用户通知
5794
- "inbox:list",
5795
- "inbox:markRead",
5796
- "inbox:postMessage",
5797
- // mirror peer sessions(远端会话镜像)
5798
- "peerSession:upsert",
5799
- "peerSession:remove",
5800
- // 设备目录 + 连接(决策 #15:daemon 代理中心 server,需要 owner 的 ttcJwt)
5801
- "device:list",
5802
- "device:connect"
5803
- ];
5804
- DeviceEntrySchema = external_exports.object({
5805
- /**
5806
- * 设备标识(决策 #15:daemon 本地生成的 dev-<uuid>,与人的身份 unionId 无关)。
5807
- * 中心 server clawd_devices 注册表仅作服务发现(url 下发 + 目录),不在安全路径上。
5808
- */
5809
- deviceId: external_exports.string().min(1),
5810
- /** 显示名 */
5811
- name: external_exports.string().min(1),
5812
- /** ws/tunnel 地址 */
5813
- url: external_exports.string().min(1)
5814
- });
5815
- DeviceListResponseSchema = external_exports.object({
5816
- type: external_exports.literal("device:list:ok"),
5817
- devices: external_exports.array(DeviceEntrySchema)
5818
- });
5819
- DeviceConnectArgsSchema = external_exports.object({
5820
- /** 目标设备的 deviceId(对方 daemon 生成的 dev-<uuid>) */
5821
- deviceId: external_exports.string().min(1),
5822
- /**
5823
- * 目标设备的 ws/tunnel 地址(可选):
5824
- * - 公开目录点击:带 url(device:list 已下发,省一次 server 查询)
5825
- * - 凭 deviceId 连接:缺省 url → daemon exchange 时由中心 server 下发该设备上报的 url
5826
- * - 注册表不在安全路径上:知道对方 url 时显式传入即可连接(不依赖中心 server 服务发现)
5827
- */
5828
- url: external_exports.string().min(1).optional(),
5829
- /** 显示名(公共设备来自目录;缺省时连上后用 whoami 回填) */
5830
- name: external_exports.string().optional()
5831
- });
5832
- DeviceConnectResponseSchema = external_exports.object({
5833
- type: external_exports.literal("device:connect:ok"),
5834
- /** 连接的目标设备标识(= 请求 args.deviceId) */
5835
- deviceId: external_exports.string(),
5836
- name: external_exports.string(),
5837
- url: external_exports.string()
5838
- });
5839
- FeishuIdentitySchema = external_exports.object({
5840
- /** 归属人 id(决策 #16:TTC unique_id,取代飞书 union_id;跨登录方式稳定) */
5841
- ownerId: external_exports.string().min(1),
5842
- /** 身份来源:'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...(决策 #16,拓展用) */
5843
- provider: external_exports.string().min(1),
5844
- /** 显示名。TTC user_info 接口 data.name */
5845
- displayName: external_exports.string().min(1),
5846
- /** TTC user_info 接口 data.avatar_url,可缺省 */
5847
- avatarUrl: external_exports.string().optional()
5848
- });
5849
- AuthLoginStartResponseSchema = external_exports.object({
5850
- type: external_exports.literal("auth:login:start:ok"),
5851
- /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5852
- authUrl: external_exports.string().min(1),
5853
- /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5854
- state: external_exports.string().min(1)
5855
- });
5856
- AuthGetIdentityResponseSchema = external_exports.object({
5857
- type: external_exports.literal("auth:getIdentity:ok"),
5858
- /** null = 未登录 */
5859
- identity: FeishuIdentitySchema.nullable(),
5860
- /**
5861
- * 本机设备标识(决策 #15:daemon 生成并持久化在 auth.json,如 dev-<uuid>)。
5862
- * 设备属性,与登录态无关、登录登出不变。UI"我的设备标识"用它拼
5863
- * clawos://device/<deviceId>(替代旧的 unionId——不再向外暴露人的身份标识)。
5864
- */
5865
- deviceId: external_exports.string().min(1)
5866
- });
5867
- AuthLogoutResponseSchema = external_exports.object({
5868
- type: external_exports.literal("auth:logout:ok")
5869
- });
5870
- AuthLoginDoneEventSchema = external_exports.object({
5871
- type: external_exports.literal("auth:login:done"),
5872
- identity: FeishuIdentitySchema
5873
- });
5874
- AuthLoginFailedEventSchema = external_exports.object({
5875
- type: external_exports.literal("auth:login:failed"),
5876
- reason: external_exports.string()
5877
- });
5878
- }
5879
- });
5880
-
5881
5793
  // ../protocol/src/runtime.ts
5882
5794
  var init_runtime = __esm({
5883
5795
  "../protocol/src/runtime.ts"() {
@@ -5893,9 +5805,8 @@ var init_runtime = __esm({
5893
5805
  init_principal();
5894
5806
  init_capability();
5895
5807
  init_inbox();
5896
- init_contact();
5808
+ init_received_capability();
5897
5809
  init_extension();
5898
- init_feishu_auth();
5899
5810
  }
5900
5811
  });
5901
5812
 
@@ -6170,8 +6081,8 @@ var require_req = __commonJS({
6170
6081
  if (req.originalUrl) {
6171
6082
  _req.url = req.originalUrl;
6172
6083
  } else {
6173
- const path60 = req.path;
6174
- _req.url = typeof path60 === "string" ? path60 : req.url ? req.url.path || req.url : void 0;
6084
+ const path59 = req.path;
6085
+ _req.url = typeof path59 === "string" ? path59 : req.url ? req.url.path || req.url : void 0;
6175
6086
  }
6176
6087
  if (req.query) {
6177
6088
  _req.query = req.query;
@@ -6336,14 +6247,14 @@ var require_redact = __commonJS({
6336
6247
  }
6337
6248
  return obj;
6338
6249
  }
6339
- function parsePath(path60) {
6250
+ function parsePath(path59) {
6340
6251
  const parts = [];
6341
6252
  let current = "";
6342
6253
  let inBrackets = false;
6343
6254
  let inQuotes = false;
6344
6255
  let quoteChar = "";
6345
- for (let i = 0; i < path60.length; i++) {
6346
- const char = path60[i];
6256
+ for (let i = 0; i < path59.length; i++) {
6257
+ const char = path59[i];
6347
6258
  if (!inBrackets && char === ".") {
6348
6259
  if (current) {
6349
6260
  parts.push(current);
@@ -6474,10 +6385,10 @@ var require_redact = __commonJS({
6474
6385
  return current;
6475
6386
  }
6476
6387
  function redactPaths(obj, paths, censor, remove = false) {
6477
- for (const path60 of paths) {
6478
- const parts = parsePath(path60);
6388
+ for (const path59 of paths) {
6389
+ const parts = parsePath(path59);
6479
6390
  if (parts.includes("*")) {
6480
- redactWildcardPath(obj, parts, censor, path60, remove);
6391
+ redactWildcardPath(obj, parts, censor, path59, remove);
6481
6392
  } else {
6482
6393
  if (remove) {
6483
6394
  removeKey(obj, parts);
@@ -6562,8 +6473,8 @@ var require_redact = __commonJS({
6562
6473
  }
6563
6474
  } else {
6564
6475
  if (afterWildcard.includes("*")) {
6565
- const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6566
- const fullPath = [...pathArray.slice(0, pathLength), ...path60];
6476
+ const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6477
+ const fullPath = [...pathArray.slice(0, pathLength), ...path59];
6567
6478
  return censor(value, fullPath);
6568
6479
  } : censor;
6569
6480
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6598,8 +6509,8 @@ var require_redact = __commonJS({
6598
6509
  return null;
6599
6510
  }
6600
6511
  const pathStructure = /* @__PURE__ */ new Map();
6601
- for (const path60 of pathsToClone) {
6602
- const parts = parsePath(path60);
6512
+ for (const path59 of pathsToClone) {
6513
+ const parts = parsePath(path59);
6603
6514
  let current = pathStructure;
6604
6515
  for (let i = 0; i < parts.length; i++) {
6605
6516
  const part = parts[i];
@@ -6651,24 +6562,24 @@ var require_redact = __commonJS({
6651
6562
  }
6652
6563
  return cloneSelectively(obj, pathStructure);
6653
6564
  }
6654
- function validatePath(path60) {
6655
- if (typeof path60 !== "string") {
6565
+ function validatePath(path59) {
6566
+ if (typeof path59 !== "string") {
6656
6567
  throw new Error("Paths must be (non-empty) strings");
6657
6568
  }
6658
- if (path60 === "") {
6569
+ if (path59 === "") {
6659
6570
  throw new Error("Invalid redaction path ()");
6660
6571
  }
6661
- if (path60.includes("..")) {
6662
- throw new Error(`Invalid redaction path (${path60})`);
6572
+ if (path59.includes("..")) {
6573
+ throw new Error(`Invalid redaction path (${path59})`);
6663
6574
  }
6664
- if (path60.includes(",")) {
6665
- throw new Error(`Invalid redaction path (${path60})`);
6575
+ if (path59.includes(",")) {
6576
+ throw new Error(`Invalid redaction path (${path59})`);
6666
6577
  }
6667
6578
  let bracketCount = 0;
6668
6579
  let inQuotes = false;
6669
6580
  let quoteChar = "";
6670
- for (let i = 0; i < path60.length; i++) {
6671
- const char = path60[i];
6581
+ for (let i = 0; i < path59.length; i++) {
6582
+ const char = path59[i];
6672
6583
  if ((char === '"' || char === "'") && bracketCount > 0) {
6673
6584
  if (!inQuotes) {
6674
6585
  inQuotes = true;
@@ -6682,20 +6593,20 @@ var require_redact = __commonJS({
6682
6593
  } else if (char === "]" && !inQuotes) {
6683
6594
  bracketCount--;
6684
6595
  if (bracketCount < 0) {
6685
- throw new Error(`Invalid redaction path (${path60})`);
6596
+ throw new Error(`Invalid redaction path (${path59})`);
6686
6597
  }
6687
6598
  }
6688
6599
  }
6689
6600
  if (bracketCount !== 0) {
6690
- throw new Error(`Invalid redaction path (${path60})`);
6601
+ throw new Error(`Invalid redaction path (${path59})`);
6691
6602
  }
6692
6603
  }
6693
6604
  function validatePaths(paths) {
6694
6605
  if (!Array.isArray(paths)) {
6695
6606
  throw new TypeError("paths must be an array");
6696
6607
  }
6697
- for (const path60 of paths) {
6698
- validatePath(path60);
6608
+ for (const path59 of paths) {
6609
+ validatePath(path59);
6699
6610
  }
6700
6611
  }
6701
6612
  function slowRedact(options = {}) {
@@ -6863,8 +6774,8 @@ var require_redaction = __commonJS({
6863
6774
  if (shape[k2] === null) {
6864
6775
  o[k2] = (value) => topCensor(value, [k2]);
6865
6776
  } else {
6866
- const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6867
- return censor(value, [k2, ...path60]);
6777
+ const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6778
+ return censor(value, [k2, ...path59]);
6868
6779
  } : censor;
6869
6780
  o[k2] = Redact({
6870
6781
  paths: shape[k2],
@@ -7082,10 +6993,10 @@ var require_atomic_sleep = __commonJS({
7082
6993
  var require_sonic_boom = __commonJS({
7083
6994
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7084
6995
  "use strict";
7085
- var fs49 = require("fs");
6996
+ var fs48 = require("fs");
7086
6997
  var EventEmitter3 = require("events");
7087
6998
  var inherits = require("util").inherits;
7088
- var path60 = require("path");
6999
+ var path59 = require("path");
7089
7000
  var sleep = require_atomic_sleep();
7090
7001
  var assert = require("assert");
7091
7002
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7139,20 +7050,20 @@ var require_sonic_boom = __commonJS({
7139
7050
  const mode = sonic.mode;
7140
7051
  if (sonic.sync) {
7141
7052
  try {
7142
- if (sonic.mkdir) fs49.mkdirSync(path60.dirname(file), { recursive: true });
7143
- const fd = fs49.openSync(file, flags, mode);
7053
+ if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
7054
+ const fd = fs48.openSync(file, flags, mode);
7144
7055
  fileOpened(null, fd);
7145
7056
  } catch (err) {
7146
7057
  fileOpened(err);
7147
7058
  throw err;
7148
7059
  }
7149
7060
  } else if (sonic.mkdir) {
7150
- fs49.mkdir(path60.dirname(file), { recursive: true }, (err) => {
7061
+ fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
7151
7062
  if (err) return fileOpened(err);
7152
- fs49.open(file, flags, mode, fileOpened);
7063
+ fs48.open(file, flags, mode, fileOpened);
7153
7064
  });
7154
7065
  } else {
7155
- fs49.open(file, flags, mode, fileOpened);
7066
+ fs48.open(file, flags, mode, fileOpened);
7156
7067
  }
7157
7068
  }
7158
7069
  function SonicBoom(opts) {
@@ -7193,8 +7104,8 @@ var require_sonic_boom = __commonJS({
7193
7104
  this.flush = flushBuffer;
7194
7105
  this.flushSync = flushBufferSync;
7195
7106
  this._actualWrite = actualWriteBuffer;
7196
- fsWriteSync = () => fs49.writeSync(this.fd, this._writingBuf);
7197
- fsWrite = () => fs49.write(this.fd, this._writingBuf, this.release);
7107
+ fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
7108
+ fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
7198
7109
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7199
7110
  this._writingBuf = "";
7200
7111
  this.write = write;
@@ -7203,15 +7114,15 @@ var require_sonic_boom = __commonJS({
7203
7114
  this._actualWrite = actualWrite;
7204
7115
  fsWriteSync = () => {
7205
7116
  if (Buffer.isBuffer(this._writingBuf)) {
7206
- return fs49.writeSync(this.fd, this._writingBuf);
7117
+ return fs48.writeSync(this.fd, this._writingBuf);
7207
7118
  }
7208
- return fs49.writeSync(this.fd, this._writingBuf, "utf8");
7119
+ return fs48.writeSync(this.fd, this._writingBuf, "utf8");
7209
7120
  };
7210
7121
  fsWrite = () => {
7211
7122
  if (Buffer.isBuffer(this._writingBuf)) {
7212
- return fs49.write(this.fd, this._writingBuf, this.release);
7123
+ return fs48.write(this.fd, this._writingBuf, this.release);
7213
7124
  }
7214
- return fs49.write(this.fd, this._writingBuf, "utf8", this.release);
7125
+ return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
7215
7126
  };
7216
7127
  } else {
7217
7128
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7268,7 +7179,7 @@ var require_sonic_boom = __commonJS({
7268
7179
  }
7269
7180
  }
7270
7181
  if (this._fsync) {
7271
- fs49.fsyncSync(this.fd);
7182
+ fs48.fsyncSync(this.fd);
7272
7183
  }
7273
7184
  const len = this._len;
7274
7185
  if (this._reopening) {
@@ -7382,7 +7293,7 @@ var require_sonic_boom = __commonJS({
7382
7293
  const onDrain = () => {
7383
7294
  if (!this._fsync) {
7384
7295
  try {
7385
- fs49.fsync(this.fd, (err) => {
7296
+ fs48.fsync(this.fd, (err) => {
7386
7297
  this._flushPending = false;
7387
7298
  cb(err);
7388
7299
  });
@@ -7484,7 +7395,7 @@ var require_sonic_boom = __commonJS({
7484
7395
  const fd = this.fd;
7485
7396
  this.once("ready", () => {
7486
7397
  if (fd !== this.fd) {
7487
- fs49.close(fd, (err) => {
7398
+ fs48.close(fd, (err) => {
7488
7399
  if (err) {
7489
7400
  return this.emit("error", err);
7490
7401
  }
@@ -7533,7 +7444,7 @@ var require_sonic_boom = __commonJS({
7533
7444
  buf = this._bufs[0];
7534
7445
  }
7535
7446
  try {
7536
- const n = Buffer.isBuffer(buf) ? fs49.writeSync(this.fd, buf) : fs49.writeSync(this.fd, buf, "utf8");
7447
+ const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7537
7448
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7538
7449
  buf = releasedBufObj.writingBuf;
7539
7450
  this._len = releasedBufObj.len;
@@ -7549,7 +7460,7 @@ var require_sonic_boom = __commonJS({
7549
7460
  }
7550
7461
  }
7551
7462
  try {
7552
- fs49.fsyncSync(this.fd);
7463
+ fs48.fsyncSync(this.fd);
7553
7464
  } catch {
7554
7465
  }
7555
7466
  }
@@ -7570,7 +7481,7 @@ var require_sonic_boom = __commonJS({
7570
7481
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7571
7482
  }
7572
7483
  try {
7573
- const n = fs49.writeSync(this.fd, buf);
7484
+ const n = fs48.writeSync(this.fd, buf);
7574
7485
  buf = buf.subarray(n);
7575
7486
  this._len = Math.max(this._len - n, 0);
7576
7487
  if (buf.length <= 0) {
@@ -7598,13 +7509,13 @@ var require_sonic_boom = __commonJS({
7598
7509
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7599
7510
  if (this.sync) {
7600
7511
  try {
7601
- const written = Buffer.isBuffer(this._writingBuf) ? fs49.writeSync(this.fd, this._writingBuf) : fs49.writeSync(this.fd, this._writingBuf, "utf8");
7512
+ const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7602
7513
  release(null, written);
7603
7514
  } catch (err) {
7604
7515
  release(err);
7605
7516
  }
7606
7517
  } else {
7607
- fs49.write(this.fd, this._writingBuf, release);
7518
+ fs48.write(this.fd, this._writingBuf, release);
7608
7519
  }
7609
7520
  }
7610
7521
  function actualWriteBuffer() {
@@ -7613,7 +7524,7 @@ var require_sonic_boom = __commonJS({
7613
7524
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7614
7525
  if (this.sync) {
7615
7526
  try {
7616
- const written = fs49.writeSync(this.fd, this._writingBuf);
7527
+ const written = fs48.writeSync(this.fd, this._writingBuf);
7617
7528
  release(null, written);
7618
7529
  } catch (err) {
7619
7530
  release(err);
@@ -7622,7 +7533,7 @@ var require_sonic_boom = __commonJS({
7622
7533
  if (kCopyBuffer) {
7623
7534
  this._writingBuf = Buffer.from(this._writingBuf);
7624
7535
  }
7625
- fs49.write(this.fd, this._writingBuf, release);
7536
+ fs48.write(this.fd, this._writingBuf, release);
7626
7537
  }
7627
7538
  }
7628
7539
  function actualClose(sonic) {
@@ -7638,12 +7549,12 @@ var require_sonic_boom = __commonJS({
7638
7549
  sonic._lens = [];
7639
7550
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7640
7551
  try {
7641
- fs49.fsync(sonic.fd, closeWrapped);
7552
+ fs48.fsync(sonic.fd, closeWrapped);
7642
7553
  } catch {
7643
7554
  }
7644
7555
  function closeWrapped() {
7645
7556
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7646
- fs49.close(sonic.fd, done);
7557
+ fs48.close(sonic.fd, done);
7647
7558
  } else {
7648
7559
  done();
7649
7560
  }
@@ -7900,7 +7811,7 @@ var require_thread_stream = __commonJS({
7900
7811
  var { version: version2 } = require_package();
7901
7812
  var { EventEmitter: EventEmitter3 } = require("events");
7902
7813
  var { Worker } = require("worker_threads");
7903
- var { join: join15 } = require("path");
7814
+ var { join: join14 } = require("path");
7904
7815
  var { pathToFileURL } = require("url");
7905
7816
  var { wait } = require_wait();
7906
7817
  var {
@@ -7936,7 +7847,7 @@ var require_thread_stream = __commonJS({
7936
7847
  function createWorker(stream, opts) {
7937
7848
  const { filename, workerData } = opts;
7938
7849
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7939
- const toExecute = bundlerOverrides["thread-stream-worker"] || join15(__dirname, "lib", "worker.js");
7850
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join14(__dirname, "lib", "worker.js");
7940
7851
  const worker = new Worker(toExecute, {
7941
7852
  ...opts.workerOpts,
7942
7853
  trackUnmanagedFds: false,
@@ -8322,7 +8233,7 @@ var require_transport = __commonJS({
8322
8233
  "use strict";
8323
8234
  var { createRequire } = require("module");
8324
8235
  var getCallers = require_caller();
8325
- var { join: join15, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8236
+ var { join: join14, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8326
8237
  var sleep = require_atomic_sleep();
8327
8238
  var onExit = require_on_exit_leak_free();
8328
8239
  var ThreadStream = require_thread_stream();
@@ -8385,7 +8296,7 @@ var require_transport = __commonJS({
8385
8296
  throw new Error("only one of target or targets can be specified");
8386
8297
  }
8387
8298
  if (targets) {
8388
- target = bundlerOverrides["pino-worker"] || join15(__dirname, "worker.js");
8299
+ target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
8389
8300
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8390
8301
  return {
8391
8302
  ...dest,
@@ -8403,7 +8314,7 @@ var require_transport = __commonJS({
8403
8314
  });
8404
8315
  });
8405
8316
  } else if (pipeline3) {
8406
- target = bundlerOverrides["pino-worker"] || join15(__dirname, "worker.js");
8317
+ target = bundlerOverrides["pino-worker"] || join14(__dirname, "worker.js");
8407
8318
  options.pipelines = [pipeline3.map((dest) => {
8408
8319
  return {
8409
8320
  ...dest,
@@ -8425,7 +8336,7 @@ var require_transport = __commonJS({
8425
8336
  return origin;
8426
8337
  }
8427
8338
  if (origin === "pino/file") {
8428
- return join15(__dirname, "..", "file.js");
8339
+ return join14(__dirname, "..", "file.js");
8429
8340
  }
8430
8341
  let fixTarget2;
8431
8342
  for (const filePath of callers) {
@@ -9415,7 +9326,7 @@ var require_safe_stable_stringify = __commonJS({
9415
9326
  return circularValue;
9416
9327
  }
9417
9328
  let res = "";
9418
- let join15 = ",";
9329
+ let join14 = ",";
9419
9330
  const originalIndentation = indentation;
9420
9331
  if (Array.isArray(value)) {
9421
9332
  if (value.length === 0) {
@@ -9429,7 +9340,7 @@ var require_safe_stable_stringify = __commonJS({
9429
9340
  indentation += spacer;
9430
9341
  res += `
9431
9342
  ${indentation}`;
9432
- join15 = `,
9343
+ join14 = `,
9433
9344
  ${indentation}`;
9434
9345
  }
9435
9346
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9437,13 +9348,13 @@ ${indentation}`;
9437
9348
  for (; i < maximumValuesToStringify - 1; i++) {
9438
9349
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9439
9350
  res += tmp2 !== void 0 ? tmp2 : "null";
9440
- res += join15;
9351
+ res += join14;
9441
9352
  }
9442
9353
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9443
9354
  res += tmp !== void 0 ? tmp : "null";
9444
9355
  if (value.length - 1 > maximumBreadth) {
9445
9356
  const removedKeys = value.length - maximumBreadth - 1;
9446
- res += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9357
+ res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9447
9358
  }
9448
9359
  if (spacer !== "") {
9449
9360
  res += `
@@ -9464,7 +9375,7 @@ ${originalIndentation}`;
9464
9375
  let separator = "";
9465
9376
  if (spacer !== "") {
9466
9377
  indentation += spacer;
9467
- join15 = `,
9378
+ join14 = `,
9468
9379
  ${indentation}`;
9469
9380
  whitespace = " ";
9470
9381
  }
@@ -9478,13 +9389,13 @@ ${indentation}`;
9478
9389
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9479
9390
  if (tmp !== void 0) {
9480
9391
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9481
- separator = join15;
9392
+ separator = join14;
9482
9393
  }
9483
9394
  }
9484
9395
  if (keyLength > maximumBreadth) {
9485
9396
  const removedKeys = keyLength - maximumBreadth;
9486
9397
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9487
- separator = join15;
9398
+ separator = join14;
9488
9399
  }
9489
9400
  if (spacer !== "" && separator.length > 1) {
9490
9401
  res = `
@@ -9525,7 +9436,7 @@ ${originalIndentation}`;
9525
9436
  }
9526
9437
  const originalIndentation = indentation;
9527
9438
  let res = "";
9528
- let join15 = ",";
9439
+ let join14 = ",";
9529
9440
  if (Array.isArray(value)) {
9530
9441
  if (value.length === 0) {
9531
9442
  return "[]";
@@ -9538,7 +9449,7 @@ ${originalIndentation}`;
9538
9449
  indentation += spacer;
9539
9450
  res += `
9540
9451
  ${indentation}`;
9541
- join15 = `,
9452
+ join14 = `,
9542
9453
  ${indentation}`;
9543
9454
  }
9544
9455
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9546,13 +9457,13 @@ ${indentation}`;
9546
9457
  for (; i < maximumValuesToStringify - 1; i++) {
9547
9458
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9548
9459
  res += tmp2 !== void 0 ? tmp2 : "null";
9549
- res += join15;
9460
+ res += join14;
9550
9461
  }
9551
9462
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9552
9463
  res += tmp !== void 0 ? tmp : "null";
9553
9464
  if (value.length - 1 > maximumBreadth) {
9554
9465
  const removedKeys = value.length - maximumBreadth - 1;
9555
- res += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9466
+ res += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9556
9467
  }
9557
9468
  if (spacer !== "") {
9558
9469
  res += `
@@ -9565,7 +9476,7 @@ ${originalIndentation}`;
9565
9476
  let whitespace = "";
9566
9477
  if (spacer !== "") {
9567
9478
  indentation += spacer;
9568
- join15 = `,
9479
+ join14 = `,
9569
9480
  ${indentation}`;
9570
9481
  whitespace = " ";
9571
9482
  }
@@ -9574,7 +9485,7 @@ ${indentation}`;
9574
9485
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9575
9486
  if (tmp !== void 0) {
9576
9487
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9577
- separator = join15;
9488
+ separator = join14;
9578
9489
  }
9579
9490
  }
9580
9491
  if (spacer !== "" && separator.length > 1) {
@@ -9632,20 +9543,20 @@ ${originalIndentation}`;
9632
9543
  indentation += spacer;
9633
9544
  let res2 = `
9634
9545
  ${indentation}`;
9635
- const join16 = `,
9546
+ const join15 = `,
9636
9547
  ${indentation}`;
9637
9548
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9638
9549
  let i = 0;
9639
9550
  for (; i < maximumValuesToStringify - 1; i++) {
9640
9551
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9641
9552
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9642
- res2 += join16;
9553
+ res2 += join15;
9643
9554
  }
9644
9555
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9645
9556
  res2 += tmp !== void 0 ? tmp : "null";
9646
9557
  if (value.length - 1 > maximumBreadth) {
9647
9558
  const removedKeys = value.length - maximumBreadth - 1;
9648
- res2 += `${join16}"... ${getItemCount(removedKeys)} not stringified"`;
9559
+ res2 += `${join15}"... ${getItemCount(removedKeys)} not stringified"`;
9649
9560
  }
9650
9561
  res2 += `
9651
9562
  ${originalIndentation}`;
@@ -9661,16 +9572,16 @@ ${originalIndentation}`;
9661
9572
  return '"[Object]"';
9662
9573
  }
9663
9574
  indentation += spacer;
9664
- const join15 = `,
9575
+ const join14 = `,
9665
9576
  ${indentation}`;
9666
9577
  let res = "";
9667
9578
  let separator = "";
9668
9579
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9669
9580
  if (isTypedArrayWithEntries(value)) {
9670
- res += stringifyTypedArray(value, join15, maximumBreadth);
9581
+ res += stringifyTypedArray(value, join14, maximumBreadth);
9671
9582
  keys = keys.slice(value.length);
9672
9583
  maximumPropertiesToStringify -= value.length;
9673
- separator = join15;
9584
+ separator = join14;
9674
9585
  }
9675
9586
  if (deterministic) {
9676
9587
  keys = sort(keys, comparator);
@@ -9681,13 +9592,13 @@ ${indentation}`;
9681
9592
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9682
9593
  if (tmp !== void 0) {
9683
9594
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9684
- separator = join15;
9595
+ separator = join14;
9685
9596
  }
9686
9597
  }
9687
9598
  if (keyLength > maximumBreadth) {
9688
9599
  const removedKeys = keyLength - maximumBreadth;
9689
9600
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9690
- separator = join15;
9601
+ separator = join14;
9691
9602
  }
9692
9603
  if (separator !== "") {
9693
9604
  res = `
@@ -10778,11 +10689,11 @@ var init_lib = __esm({
10778
10689
  }
10779
10690
  }
10780
10691
  },
10781
- addToPath: function addToPath(path60, added, removed, oldPosInc, options) {
10782
- var last = path60.lastComponent;
10692
+ addToPath: function addToPath(path59, added, removed, oldPosInc, options) {
10693
+ var last = path59.lastComponent;
10783
10694
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10784
10695
  return {
10785
- oldPos: path60.oldPos + oldPosInc,
10696
+ oldPos: path59.oldPos + oldPosInc,
10786
10697
  lastComponent: {
10787
10698
  count: last.count + 1,
10788
10699
  added,
@@ -10792,7 +10703,7 @@ var init_lib = __esm({
10792
10703
  };
10793
10704
  } else {
10794
10705
  return {
10795
- oldPos: path60.oldPos + oldPosInc,
10706
+ oldPos: path59.oldPos + oldPosInc,
10796
10707
  lastComponent: {
10797
10708
  count: 1,
10798
10709
  added,
@@ -11238,10 +11149,10 @@ function attachmentToHistoryMessage(o, ts) {
11238
11149
  const memories = raw.map((m2) => {
11239
11150
  if (!m2 || typeof m2 !== "object") return null;
11240
11151
  const rec = m2;
11241
- const path60 = typeof rec.path === "string" ? rec.path : null;
11152
+ const path59 = typeof rec.path === "string" ? rec.path : null;
11242
11153
  const content = typeof rec.content === "string" ? rec.content : null;
11243
- if (!path60 || content == null) return null;
11244
- const entry = { path: path60, content };
11154
+ if (!path59 || content == null) return null;
11155
+ const entry = { path: path59, content };
11245
11156
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
11246
11157
  return entry;
11247
11158
  }).filter((m2) => m2 !== null);
@@ -12072,10 +11983,10 @@ function parseAttachment(obj) {
12072
11983
  const memories = raw.map((m2) => {
12073
11984
  if (!m2 || typeof m2 !== "object") return null;
12074
11985
  const rec = m2;
12075
- const path60 = typeof rec.path === "string" ? rec.path : null;
11986
+ const path59 = typeof rec.path === "string" ? rec.path : null;
12076
11987
  const content = typeof rec.content === "string" ? rec.content : null;
12077
- if (!path60 || content == null) return null;
12078
- const out = { path: path60, content };
11988
+ if (!path59 || content == null) return null;
11989
+ const out = { path: path59, content };
12079
11990
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
12080
11991
  return out;
12081
11992
  }).filter((m2) => m2 !== null);
@@ -21274,7 +21185,7 @@ var require_BufferList = __commonJS({
21274
21185
  this.head = this.tail = null;
21275
21186
  this.length = 0;
21276
21187
  };
21277
- BufferList.prototype.join = function join15(s) {
21188
+ BufferList.prototype.join = function join14(s) {
21278
21189
  if (this.length === 0) return "";
21279
21190
  var p2 = this.head;
21280
21191
  var ret = "" + p2.data;
@@ -23864,8 +23775,8 @@ var require_utils = __commonJS({
23864
23775
  var result = transform[inputType][outputType](input);
23865
23776
  return result;
23866
23777
  };
23867
- exports2.resolve = function(path60) {
23868
- var parts = path60.split("/");
23778
+ exports2.resolve = function(path59) {
23779
+ var parts = path59.split("/");
23869
23780
  var result = [];
23870
23781
  for (var index = 0; index < parts.length; index++) {
23871
23782
  var part = parts[index];
@@ -29718,18 +29629,18 @@ var require_object = __commonJS({
29718
29629
  var object = new ZipObject(name, zipObjectContent, o);
29719
29630
  this.files[name] = object;
29720
29631
  };
29721
- var parentFolder = function(path60) {
29722
- if (path60.slice(-1) === "/") {
29723
- path60 = path60.substring(0, path60.length - 1);
29632
+ var parentFolder = function(path59) {
29633
+ if (path59.slice(-1) === "/") {
29634
+ path59 = path59.substring(0, path59.length - 1);
29724
29635
  }
29725
- var lastSlash = path60.lastIndexOf("/");
29726
- return lastSlash > 0 ? path60.substring(0, lastSlash) : "";
29636
+ var lastSlash = path59.lastIndexOf("/");
29637
+ return lastSlash > 0 ? path59.substring(0, lastSlash) : "";
29727
29638
  };
29728
- var forceTrailingSlash = function(path60) {
29729
- if (path60.slice(-1) !== "/") {
29730
- path60 += "/";
29639
+ var forceTrailingSlash = function(path59) {
29640
+ if (path59.slice(-1) !== "/") {
29641
+ path59 += "/";
29731
29642
  }
29732
- return path60;
29643
+ return path59;
29733
29644
  };
29734
29645
  var folderAdd = function(name, createFolders) {
29735
29646
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -33215,7 +33126,7 @@ var SessionManager = class {
33215
33126
  }
33216
33127
  }
33217
33128
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
33218
- create(args, creatorPrincipalId) {
33129
+ create(args, creatorPrincipalId, creatorOwnerPrincipalId) {
33219
33130
  let cwd;
33220
33131
  if (args.ownerPersonaId) {
33221
33132
  cwd = derivePersonaSpawnCwd(
@@ -33258,6 +33169,7 @@ var SessionManager = class {
33258
33169
  ownerPersonaId: args.ownerPersonaId,
33259
33170
  appBuilderProject: args.appBuilderProject,
33260
33171
  creatorPrincipalId,
33172
+ ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33261
33173
  createdAt: iso,
33262
33174
  updatedAt: iso
33263
33175
  };
@@ -36726,10 +36638,9 @@ var LocalWsServer = class {
36726
36638
  httpServer = null;
36727
36639
  frameHandler = null;
36728
36640
  clients = /* @__PURE__ */ new Map();
36729
- // guest principal id → Set<clientId>,撤销时 O(1) 找到该 guest 的所有活跃连接做关闭级联。
36730
- // 同一 guest 可能多端同时连(多设备 / 多 tab)。
36731
- // Phase 3 (决策 #14): 索引键从 ctx.capabilityId 改为 guest principal.id(两者恒等,纯正名)。
36732
- guestIdToClients = /* @__PURE__ */ new Map();
36641
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36642
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36643
+ capabilityIdToClients = /* @__PURE__ */ new Map();
36733
36644
  logger;
36734
36645
  pingIntervalMs;
36735
36646
  async start() {
@@ -36847,6 +36758,38 @@ var LocalWsServer = class {
36847
36758
  this.safeSend(c.ws, frame);
36848
36759
  }
36849
36760
  }
36761
+ // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36762
+ // - 所有 owner ws (owner 看自家所有 channel)
36763
+ // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36764
+ // 一次调用同时推两端, inbox:event push 帧由此走.
36765
+ broadcastToCapabilityChannel(capabilityId, frame) {
36766
+ this.broadcastToOwners(frame);
36767
+ const set = this.capabilityIdToClients.get(capabilityId);
36768
+ if (!set) return;
36769
+ for (const id of set) {
36770
+ const c = this.clients.get(id);
36771
+ if (!c) continue;
36772
+ this.safeSend(c.ws, frame);
36773
+ }
36774
+ }
36775
+ // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36776
+ // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36777
+ // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36778
+ // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36779
+ // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36780
+ broadcastToPrincipal(principalId, frame) {
36781
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36782
+ this.broadcastToOwners(frame);
36783
+ return;
36784
+ }
36785
+ const set = this.capabilityIdToClients.get(principalId);
36786
+ if (!set) return;
36787
+ for (const id of set) {
36788
+ const c = this.clients.get(id);
36789
+ if (!c) continue;
36790
+ this.safeSend(c.ws, frame);
36791
+ }
36792
+ }
36850
36793
  firstSubscriber(sessionId) {
36851
36794
  for (const c of this.clients.values()) {
36852
36795
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36869,16 +36812,16 @@ var LocalWsServer = class {
36869
36812
  this.safeSend(c.ws, frame);
36870
36813
  }
36871
36814
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36872
- // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36815
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36873
36816
  attachClientContext(clientId, ctx) {
36874
36817
  const c = this.clients.get(clientId);
36875
36818
  if (!c) return;
36876
36819
  c.ctx = ctx;
36877
- if (ctx.principal.kind === "guest") {
36878
- let set = this.guestIdToClients.get(ctx.principal.id);
36820
+ if (ctx.capabilityId) {
36821
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
36879
36822
  if (!set) {
36880
36823
  set = /* @__PURE__ */ new Set();
36881
- this.guestIdToClients.set(ctx.principal.id, set);
36824
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
36882
36825
  }
36883
36826
  set.add(clientId);
36884
36827
  }
@@ -36887,27 +36830,10 @@ var LocalWsServer = class {
36887
36830
  getClientContext(clientId) {
36888
36831
  return this.clients.get(clientId)?.ctx ?? null;
36889
36832
  }
36890
- // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36891
- // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36892
- // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36893
- // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36894
- closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36895
- const ids = [...this.clients.keys()];
36896
- for (const id of ids) {
36897
- const c = this.clients.get(id);
36898
- if (!c) continue;
36899
- try {
36900
- c.ws.close(code, reason);
36901
- } catch {
36902
- }
36903
- }
36904
- }
36905
- // Task 1.10 撤销级联:关闭某 guest principal 的所有活跃 ws 连接。close code 4401
36833
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36906
36834
  // 让客户端识别 'capability revoked' 而非普通断连。
36907
- // capability:delete 级联用 cap.id 调本方法——noAuth per-peer cap guest 的
36908
- // principal.id cap.id,级联语义不变(Phase 3 决策 #14 正名)。
36909
- closeConnectionsByGuestId(guestPrincipalId, code = 4401, reason = "TOKEN_REVOKED") {
36910
- const set = this.guestIdToClients.get(guestPrincipalId);
36835
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36836
+ const set = this.capabilityIdToClients.get(capabilityId);
36911
36837
  if (!set) return;
36912
36838
  const ids = [...set];
36913
36839
  for (const id of ids) {
@@ -36972,18 +36898,16 @@ var LocalWsServer = class {
36972
36898
  this.logger?.warn("ready frame build failed", { err: err.message });
36973
36899
  }
36974
36900
  socket.on("message", (data) => {
36975
- void this.onMessage(handle, data, remoteAddress).catch((err) => {
36976
- this.logger?.warn("onMessage failed", { err: err.message });
36977
- });
36901
+ this.onMessage(handle, data, remoteAddress);
36978
36902
  });
36979
36903
  socket.on("close", () => {
36980
36904
  const c = this.clients.get(id);
36981
36905
  if (c?.pingTimer) clearInterval(c.pingTimer);
36982
- const guestId = c?.ctx?.principal.kind === "guest" ? c.ctx.principal.id : void 0;
36983
- if (guestId) {
36984
- const set = this.guestIdToClients.get(guestId);
36906
+ const capId = c?.ctx?.capabilityId;
36907
+ if (capId) {
36908
+ const set = this.capabilityIdToClients.get(capId);
36985
36909
  set?.delete(id);
36986
- if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36910
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36987
36911
  }
36988
36912
  this.clients.delete(id);
36989
36913
  authGate?.unregister(id);
@@ -36993,7 +36917,7 @@ var LocalWsServer = class {
36993
36917
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36994
36918
  });
36995
36919
  }
36996
- async onMessage(client, data, remoteAddress) {
36920
+ onMessage(client, data, remoteAddress) {
36997
36921
  let frame;
36998
36922
  try {
36999
36923
  frame = JSON.parse(data.toString());
@@ -37004,7 +36928,7 @@ var LocalWsServer = class {
37004
36928
  const authGate = this.opts.authGate;
37005
36929
  if (authGate) {
37006
36930
  const wasAuthed = authGate.isAuthed(client.id);
37007
- const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36931
+ const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
37008
36932
  if (verdict !== "pass") {
37009
36933
  if (!wasAuthed && authGate.isAuthed(client.id)) {
37010
36934
  try {
@@ -37019,8 +36943,14 @@ var LocalWsServer = class {
37019
36943
  }
37020
36944
  } else if (frame.type === "auth") {
37021
36945
  const token = typeof frame.token === "string" ? frame.token ?? "" : "";
36946
+ const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
36947
+ const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
37022
36948
  if (token && this.opts.tryVerifyCapabilityToken) {
37023
- const guestCtx = this.opts.tryVerifyCapabilityToken(token);
36949
+ const guestCtx = this.opts.tryVerifyCapabilityToken(
36950
+ token,
36951
+ selfPrincipalId || void 0,
36952
+ selfDisplayName || void 0
36953
+ );
37024
36954
  if (guestCtx) this.attachClientContext(client.id, guestCtx);
37025
36955
  }
37026
36956
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
@@ -37101,7 +37031,7 @@ var AuthGate = class {
37101
37031
  registerConnection(handle) {
37102
37032
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
37103
37033
  if (!enforce) {
37104
- this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
37034
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null });
37105
37035
  return true;
37106
37036
  }
37107
37037
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -37110,7 +37040,7 @@ var AuthGate = class {
37110
37040
  if (!st || st.authed) return;
37111
37041
  this.opts.closeConnection(handle, 1008, "auth timeout");
37112
37042
  }, this.opts.timeoutMs);
37113
- this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
37043
+ this.states.set(handle.id, { authed: false, enforce: true, timer });
37114
37044
  return false;
37115
37045
  }
37116
37046
  unregister(handleId) {
@@ -37126,15 +37056,12 @@ var AuthGate = class {
37126
37056
  }
37127
37057
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
37128
37058
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
37129
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
37059
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
37130
37060
  // - pass:已 authed,调用方继续走业务路由
37131
- //
37132
- // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
37133
- async handleFrame(handle, frame) {
37061
+ handleFrame(handle, frame) {
37134
37062
  const st = this.states.get(handle.id);
37135
37063
  if (!st) return "reject";
37136
37064
  if (st.authed) return "pass";
37137
- if (st.pending) return "reject";
37138
37065
  const parsed = AuthRequestFrameSchema.safeParse(frame);
37139
37066
  if (!parsed.success) {
37140
37067
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -37144,14 +37071,11 @@ var AuthGate = class {
37144
37071
  }
37145
37072
  let ctx = null;
37146
37073
  if (this.opts.authenticate) {
37147
- st.pending = true;
37148
- let r;
37149
- try {
37150
- r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
37151
- } finally {
37152
- st.pending = false;
37153
- }
37154
- if (!this.states.has(handle.id)) return "reject";
37074
+ const r = this.opts.authenticate(
37075
+ parsed.data.token,
37076
+ parsed.data.selfPrincipalId,
37077
+ parsed.data.selfDisplayName
37078
+ );
37155
37079
  if (!r.ok) {
37156
37080
  this.opts.closeConnection(handle, 4401, r.code);
37157
37081
  this.markFailed(handle.id);
@@ -37254,33 +37178,26 @@ function ownerContext(ownerPrincipalId, displayName) {
37254
37178
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
37255
37179
  };
37256
37180
  }
37257
- function connectGuestContext(subDevice, subOwner, provider, displayName) {
37181
+ function guestContext(cap, peerOwnerPrincipalId) {
37258
37182
  return {
37259
- principal: { id: subDevice, kind: "guest", displayName },
37260
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37261
- resource: { ...g2.resource },
37262
- actions: [...g2.actions]
37263
- })),
37264
- ownerId: subOwner,
37265
- provider
37183
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
37184
+ grants: cap.grants,
37185
+ capabilityId: cap.id,
37186
+ ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
37266
37187
  };
37267
37188
  }
37268
- async function authenticate(token, deps) {
37189
+ function authenticate(token, deps) {
37269
37190
  if (!token) return { ok: false, code: "NO_TOKEN" };
37270
37191
  if (deps.isOwnerToken(token)) {
37271
37192
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
37272
37193
  }
37273
- if (!deps.verifyConnectToken) return { ok: false, code: "BAD_TOKEN" };
37274
- const result = deps.verifyConnectToken(token);
37275
- if (!result.ok) {
37276
- if (result.reason === "EXPIRED") return { ok: false, code: "TOKEN_EXPIRED" };
37277
- if (result.reason === "WRONG_AUDIENCE") return { ok: false, code: "TOKEN_REVOKED" };
37278
- return { ok: false, code: "BAD_TOKEN" };
37194
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
37195
+ const v2 = deps.capabilityRegistry.verifyToken(token);
37196
+ if (!v2.ok) {
37197
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
37198
+ return { ok: false, code: v2.code };
37279
37199
  }
37280
- return {
37281
- ok: true,
37282
- context: connectGuestContext(result.subDevice, result.subOwner, result.provider, result.displayName)
37283
- };
37200
+ return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
37284
37201
  }
37285
37202
 
37286
37203
  // src/permission/capability-store.ts
@@ -37426,8 +37343,8 @@ var CapabilityManager = class {
37426
37343
  list() {
37427
37344
  return this.registry.list();
37428
37345
  }
37429
- // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37430
- // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37346
+ // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37347
+ // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37431
37348
  findById(id) {
37432
37349
  return this.registry.findById(id);
37433
37350
  }
@@ -37460,14 +37377,89 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37460
37377
  return { removed };
37461
37378
  }
37462
37379
 
37380
+ // src/permission/personal-capability.ts
37381
+ var import_node_fs15 = __toESM(require("fs"), 1);
37382
+ var import_node_path16 = __toESM(require("path"), 1);
37383
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37384
+ var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37385
+ var PERSONAL_CAP_DISPLAY_NAME = "personal";
37386
+ function personalCapFilePath(dataDir) {
37387
+ return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37388
+ }
37389
+ function loadOrCreatePersonalCapability(opts) {
37390
+ const file = personalCapFilePath(opts.dataDir);
37391
+ const generateToken = opts.generateToken ?? defaultGenerateToken;
37392
+ const generateId = opts.generateId ?? defaultGenerateId;
37393
+ const now = opts.now ?? Date.now;
37394
+ const existing = readFile(file);
37395
+ if (existing) return existing;
37396
+ const token = generateToken();
37397
+ const id = generateId();
37398
+ const capability = {
37399
+ id,
37400
+ secretHash: sha256Hex2(token),
37401
+ displayName: PERSONAL_CAP_DISPLAY_NAME,
37402
+ // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37403
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37404
+ resource: { ...g2.resource },
37405
+ actions: [...g2.actions]
37406
+ })),
37407
+ issuedAt: now(),
37408
+ usedCount: 0
37409
+ };
37410
+ const content = { token, capability };
37411
+ writeFile(file, content);
37412
+ return content;
37413
+ }
37414
+ function readFile(file) {
37415
+ let raw;
37416
+ try {
37417
+ raw = import_node_fs15.default.readFileSync(file, "utf8");
37418
+ } catch (err) {
37419
+ const code = err?.code;
37420
+ if (code === "ENOENT") return null;
37421
+ return null;
37422
+ }
37423
+ let parsed;
37424
+ try {
37425
+ parsed = JSON.parse(raw);
37426
+ } catch {
37427
+ return null;
37428
+ }
37429
+ if (!parsed || typeof parsed !== "object") return null;
37430
+ const obj = parsed;
37431
+ if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37432
+ const capParsed = CapabilitySchema.safeParse(obj.capability);
37433
+ if (!capParsed.success) return null;
37434
+ if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37435
+ return { token: obj.token, capability: capParsed.data };
37436
+ }
37437
+ function writeFile(file, content) {
37438
+ import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37439
+ import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37440
+ try {
37441
+ import_node_fs15.default.chmodSync(file, 384);
37442
+ } catch {
37443
+ }
37444
+ }
37445
+ function defaultGenerateToken() {
37446
+ return import_node_crypto4.default.randomBytes(24).toString("base64url");
37447
+ }
37448
+ function defaultGenerateId() {
37449
+ return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37450
+ }
37451
+ function sha256Hex2(s) {
37452
+ return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37453
+ }
37454
+
37463
37455
  // src/inbox/inbox-store.ts
37464
- var fs19 = __toESM(require("fs"), 1);
37465
- var path21 = __toESM(require("path"), 1);
37456
+ var fs20 = __toESM(require("fs"), 1);
37457
+ var path22 = __toESM(require("path"), 1);
37466
37458
  var INBOX_SUBDIR = "inbox";
37467
37459
  var InboxStore = class {
37468
37460
  constructor(dataDir) {
37469
37461
  this.dataDir = dataDir;
37470
- fs19.mkdirSync(this.dirPath(), { recursive: true });
37462
+ fs20.mkdirSync(this.dirPath(), { recursive: true });
37471
37463
  }
37472
37464
  dataDir;
37473
37465
  /**
@@ -37475,11 +37467,11 @@ var InboxStore = class {
37475
37467
  * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
37476
37468
  * 文件不存在时返回 [] (不抛, 不创建).
37477
37469
  */
37478
- list(peerDeviceId, sinceCreatedAt) {
37479
- const file = this.filePath(peerDeviceId);
37470
+ list(peerOwnerId, sinceCreatedAt) {
37471
+ const file = this.filePath(peerOwnerId);
37480
37472
  let raw;
37481
37473
  try {
37482
- raw = fs19.readFileSync(file, "utf8");
37474
+ raw = fs20.readFileSync(file, "utf8");
37483
37475
  } catch (err) {
37484
37476
  if (err?.code === "ENOENT") return [];
37485
37477
  return [];
@@ -37489,13 +37481,13 @@ var InboxStore = class {
37489
37481
  return all.filter((m2) => m2.createdAt > sinceCreatedAt);
37490
37482
  }
37491
37483
  /**
37492
- * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
37484
+ * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
37493
37485
  */
37494
- listAllPeerDeviceIds() {
37486
+ listAllPeerOwnerIds() {
37495
37487
  const dir = this.dirPath();
37496
37488
  let entries;
37497
37489
  try {
37498
- entries = fs19.readdirSync(dir);
37490
+ entries = fs20.readdirSync(dir);
37499
37491
  } catch (err) {
37500
37492
  if (err?.code === "ENOENT") return [];
37501
37493
  return [];
@@ -37507,63 +37499,63 @@ var InboxStore = class {
37507
37499
  * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
37508
37500
  */
37509
37501
  append(message) {
37510
- const existing = this.list(message.peerDeviceId);
37502
+ const existing = this.list(message.peerOwnerId);
37511
37503
  if (existing.some((m2) => m2.id === message.id)) return;
37512
- const file = this.filePath(message.peerDeviceId);
37504
+ const file = this.filePath(message.peerOwnerId);
37513
37505
  const line = JSON.stringify(message) + "\n";
37514
- fs19.appendFileSync(file, line, { mode: 384 });
37506
+ fs20.appendFileSync(file, line, { mode: 384 });
37515
37507
  try {
37516
- fs19.chmodSync(file, 384);
37508
+ fs20.chmodSync(file, 384);
37517
37509
  } catch {
37518
37510
  }
37519
37511
  }
37520
37512
  /**
37521
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
37522
- * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37513
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
37514
+ * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37523
37515
  * 返写入了几条.
37524
37516
  */
37525
- markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
37526
- const messages = this.list(peerDeviceId);
37517
+ markRead(peerOwnerId, principalId, upToCreatedAt) {
37518
+ const messages = this.list(peerOwnerId);
37527
37519
  let changed = 0;
37528
37520
  const next = messages.map((m2) => {
37529
- if (m2.senderDeviceId === readerDeviceId) return m2;
37521
+ if (m2.senderPrincipalId === principalId) return m2;
37530
37522
  if (m2.createdAt > upToCreatedAt) return m2;
37531
- const cur = m2.readBy[readerDeviceId];
37523
+ const cur = m2.readBy[principalId];
37532
37524
  if (cur !== void 0 && cur >= upToCreatedAt) return m2;
37533
37525
  changed++;
37534
- return { ...m2, readBy: { ...m2.readBy, [readerDeviceId]: upToCreatedAt } };
37526
+ return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
37535
37527
  });
37536
37528
  if (changed === 0) return 0;
37537
- this.rewriteFile(peerDeviceId, next);
37529
+ this.rewriteFile(peerOwnerId, next);
37538
37530
  return changed;
37539
37531
  }
37540
37532
  /**
37541
37533
  * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
37542
37534
  */
37543
- removeByPeerDeviceId(peerDeviceId) {
37544
- const file = this.filePath(peerDeviceId);
37535
+ removeByPeerOwnerId(peerOwnerId) {
37536
+ const file = this.filePath(peerOwnerId);
37545
37537
  try {
37546
- fs19.unlinkSync(file);
37538
+ fs20.unlinkSync(file);
37547
37539
  } catch (err) {
37548
37540
  if (err?.code === "ENOENT") return;
37549
37541
  }
37550
37542
  }
37551
- rewriteFile(peerDeviceId, messages) {
37552
- const file = this.filePath(peerDeviceId);
37543
+ rewriteFile(peerOwnerId, messages) {
37544
+ const file = this.filePath(peerOwnerId);
37553
37545
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37554
37546
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37555
- fs19.writeFileSync(tmp, content, { mode: 384 });
37556
- fs19.renameSync(tmp, file);
37547
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37548
+ fs20.renameSync(tmp, file);
37557
37549
  try {
37558
- fs19.chmodSync(file, 384);
37550
+ fs20.chmodSync(file, 384);
37559
37551
  } catch {
37560
37552
  }
37561
37553
  }
37562
37554
  dirPath() {
37563
- return path21.join(this.dataDir, INBOX_SUBDIR);
37555
+ return path22.join(this.dataDir, INBOX_SUBDIR);
37564
37556
  }
37565
- filePath(peerDeviceId) {
37566
- return path21.join(this.dirPath(), `${peerDeviceId}.jsonl`);
37557
+ filePath(peerOwnerId) {
37558
+ return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37567
37559
  }
37568
37560
  };
37569
37561
  function parseAllLines(raw) {
@@ -37599,72 +37591,72 @@ var InboxManager = class {
37599
37591
  * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
37600
37592
  */
37601
37593
  postMessage(args) {
37602
- const beforeLen = this.store.list(args.peerDeviceId).length;
37594
+ const beforeLen = this.store.list(args.peerOwnerId).length;
37603
37595
  const msg = {
37604
37596
  id: args.id,
37605
- peerDeviceId: args.peerDeviceId,
37606
- senderDeviceId: args.senderDeviceId,
37597
+ peerOwnerId: args.peerOwnerId,
37598
+ senderPrincipalId: args.senderPrincipalId,
37607
37599
  text: args.text,
37608
37600
  createdAt: args.createdAt,
37609
37601
  readBy: {}
37610
37602
  };
37611
37603
  this.store.append(msg);
37612
- const afterLen = this.store.list(args.peerDeviceId).length;
37604
+ const afterLen = this.store.list(args.peerOwnerId).length;
37613
37605
  if (afterLen === beforeLen) return msg;
37614
- this.broadcast(args.peerDeviceId, {
37606
+ this.broadcast(args.peerOwnerId, {
37615
37607
  type: "inbox:event",
37616
- peerDeviceId: args.peerDeviceId,
37608
+ peerOwnerId: args.peerOwnerId,
37617
37609
  message: msg
37618
37610
  });
37619
37611
  return msg;
37620
37612
  }
37621
- list(peerDeviceId, sinceCreatedAt) {
37622
- return this.store.list(peerDeviceId, sinceCreatedAt);
37613
+ list(peerOwnerId, sinceCreatedAt) {
37614
+ return this.store.list(peerOwnerId, sinceCreatedAt);
37623
37615
  }
37624
37616
  /**
37625
37617
  * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
37626
37618
  * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
37627
37619
  */
37628
37620
  markRead(args) {
37629
- const before = this.store.list(args.peerDeviceId);
37630
- const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
37621
+ const before = this.store.list(args.peerOwnerId);
37622
+ const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
37631
37623
  if (changed === 0) return 0;
37632
- const after = this.store.list(args.peerDeviceId);
37624
+ const after = this.store.list(args.peerOwnerId);
37633
37625
  const beforeById = new Map(before.map((m2) => [m2.id, m2]));
37634
37626
  for (const m2 of after) {
37635
37627
  const prev = beforeById.get(m2.id);
37636
37628
  if (!prev) continue;
37637
- if (prev.readBy[args.readerDeviceId] === m2.readBy[args.readerDeviceId]) continue;
37638
- this.broadcast(args.peerDeviceId, {
37629
+ if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
37630
+ this.broadcast(args.peerOwnerId, {
37639
37631
  type: "inbox:event",
37640
- peerDeviceId: args.peerDeviceId,
37632
+ peerOwnerId: args.peerOwnerId,
37641
37633
  message: m2
37642
37634
  });
37643
37635
  }
37644
37636
  return changed;
37645
37637
  }
37646
37638
  /** peer:remove cascade. 删整个 peer 的 jsonl. */
37647
- removeChannel(peerDeviceId) {
37648
- this.store.removeByPeerDeviceId(peerDeviceId);
37639
+ removeChannel(peerOwnerId) {
37640
+ this.store.removeByPeerOwnerId(peerOwnerId);
37649
37641
  }
37650
37642
  };
37651
37643
 
37652
- // src/state/contact-store.ts
37653
- var fs20 = __toESM(require("fs"), 1);
37654
- var path22 = __toESM(require("path"), 1);
37655
- var FILE_NAME = "contacts.json";
37656
- var ContactStore = class {
37644
+ // src/state/received-capability-store.ts
37645
+ var fs21 = __toESM(require("fs"), 1);
37646
+ var path23 = __toESM(require("path"), 1);
37647
+ var FILE_NAME = "received-capabilities.json";
37648
+ var ReceivedCapabilityStore = class {
37657
37649
  constructor(dataDir) {
37658
37650
  this.dataDir = dataDir;
37659
37651
  }
37660
37652
  dataDir;
37661
- contacts = /* @__PURE__ */ new Map();
37653
+ caps = /* @__PURE__ */ new Map();
37662
37654
  load() {
37663
- this.contacts.clear();
37664
- const file = path22.join(this.dataDir, FILE_NAME);
37655
+ this.caps.clear();
37656
+ const file = path23.join(this.dataDir, FILE_NAME);
37665
37657
  let raw;
37666
37658
  try {
37667
- raw = fs20.readFileSync(file, "utf8");
37659
+ raw = fs21.readFileSync(file, "utf8");
37668
37660
  } catch (err) {
37669
37661
  if (err?.code !== "ENOENT") this.renameBak(file);
37670
37662
  return;
@@ -37676,58 +37668,52 @@ var ContactStore = class {
37676
37668
  this.renameBak(file);
37677
37669
  return;
37678
37670
  }
37679
- const arr = parsed?.contacts;
37671
+ const arr = parsed?.receivedCaps;
37680
37672
  if (!Array.isArray(arr)) return;
37681
37673
  for (const entry of arr) {
37682
- const r = ContactSchema.safeParse(entry);
37683
- if (r.success) this.contacts.set(r.data.deviceId, r.data);
37674
+ const r = ReceivedCapabilitySchema.safeParse(entry);
37675
+ if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
37684
37676
  }
37685
37677
  }
37686
37678
  list() {
37687
- return Array.from(this.contacts.values());
37688
- }
37689
- get(deviceId) {
37690
- return this.contacts.get(deviceId) ?? null;
37679
+ return Array.from(this.caps.values());
37691
37680
  }
37692
- has(deviceId) {
37693
- return this.contacts.has(deviceId);
37681
+ get(ownerPrincipalId) {
37682
+ return this.caps.get(ownerPrincipalId) ?? null;
37694
37683
  }
37695
- /** 某个人(ownerId, provider)的所有设备记录——人级视图(显示/分组用,不参与路由) */
37696
- findByOwner(ownerId, provider) {
37697
- return this.list().filter((c) => c.ownerId === ownerId && c.provider === provider);
37684
+ hasOwner(ownerPrincipalId) {
37685
+ return this.caps.has(ownerPrincipalId);
37698
37686
  }
37699
- upsert(contact) {
37700
- this.contacts.set(contact.deviceId, contact);
37687
+ upsert(cap) {
37688
+ this.caps.set(cap.ownerPrincipalId, cap);
37701
37689
  this.flush();
37702
37690
  }
37703
- /** 删单台设备(contact:remove 语义,决策 #16):删该 deviceId 一条,返回是否删到 */
37704
- removeByDeviceId(deviceId) {
37705
- const existed = this.contacts.delete(deviceId);
37706
- if (existed) this.flush();
37707
- return existed;
37691
+ remove(ownerPrincipalId) {
37692
+ if (!this.caps.delete(ownerPrincipalId)) return;
37693
+ this.flush();
37708
37694
  }
37709
37695
  flush() {
37710
- const file = path22.join(this.dataDir, FILE_NAME);
37696
+ const file = path23.join(this.dataDir, FILE_NAME);
37711
37697
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37712
37698
  const content = JSON.stringify(
37713
- { contacts: Array.from(this.contacts.values()) },
37699
+ { receivedCaps: Array.from(this.caps.values()) },
37714
37700
  null,
37715
37701
  2
37716
37702
  );
37717
- fs20.mkdirSync(this.dataDir, { recursive: true });
37718
- fs20.writeFileSync(tmp, content, { mode: 384 });
37719
- fs20.renameSync(tmp, file);
37703
+ fs21.mkdirSync(this.dataDir, { recursive: true });
37704
+ fs21.writeFileSync(tmp, content, { mode: 384 });
37705
+ fs21.renameSync(tmp, file);
37720
37706
  }
37721
37707
  renameBak(file) {
37722
37708
  try {
37723
- fs20.renameSync(file, `${file}.bak`);
37709
+ fs21.renameSync(file, `${file}.bak`);
37724
37710
  } catch {
37725
37711
  }
37726
37712
  }
37727
37713
  };
37728
37714
 
37729
- // src/contact/connect-remote.ts
37730
- var crypto5 = __toESM(require("crypto"), 1);
37715
+ // src/received-capability/connect-remote.ts
37716
+ var crypto6 = __toESM(require("crypto"), 1);
37731
37717
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37732
37718
  var RPC_TIMEOUT_MS = 5e3;
37733
37719
  async function connectRemote(args) {
@@ -37744,11 +37730,8 @@ async function connectRemote(args) {
37744
37730
  JSON.stringify({
37745
37731
  type: "auth",
37746
37732
  token: args.token,
37747
- // 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
37748
- // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
37749
- // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
37750
- // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
37751
- ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37733
+ selfPrincipalId: args.selfPrincipalId,
37734
+ selfDisplayName: args.selfDisplayName
37752
37735
  })
37753
37736
  );
37754
37737
  await new Promise((resolve6, reject) => {
@@ -37759,7 +37742,7 @@ async function connectRemote(args) {
37759
37742
  } catch {
37760
37743
  return;
37761
37744
  }
37762
- if (f.type === "auth:ok") {
37745
+ if (f.type === "auth:ok" || f.type === "ready") {
37763
37746
  ws.off("message", onMessage);
37764
37747
  resolve6();
37765
37748
  return;
@@ -37776,7 +37759,7 @@ async function connectRemote(args) {
37776
37759
  }, HANDSHAKE_TIMEOUT_MS);
37777
37760
  });
37778
37761
  function call(method, payload) {
37779
- const requestId = crypto5.randomUUID();
37762
+ const requestId = crypto6.randomUUID();
37780
37763
  return new Promise((resolve6, reject) => {
37781
37764
  const onMessage = (raw) => {
37782
37765
  let f;
@@ -37805,9 +37788,9 @@ async function connectRemote(args) {
37805
37788
  whoami: async () => {
37806
37789
  const f = await call("whoami", {});
37807
37790
  return {
37808
- ownerId: f.owner.ownerId,
37809
- provider: f.owner.provider,
37791
+ ownerId: f.owner.id,
37810
37792
  displayName: f.owner.displayName,
37793
+ capabilityId: f.capability.id,
37811
37794
  grants: f.capability.grants
37812
37795
  };
37813
37796
  },
@@ -37820,96 +37803,62 @@ async function connectRemote(args) {
37820
37803
  };
37821
37804
  }
37822
37805
 
37823
- // src/contact/auto-reverse.ts
37824
- init_protocol();
37825
- async function autoReverseContact(args) {
37826
- if (!args.selfUrl) return;
37827
- const now = args.now ?? Date.now;
37828
- const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
37829
- resource: { ...g2.resource },
37830
- actions: [...g2.actions]
37831
- }));
37832
- const base = {
37833
- deviceId: args.deviceId,
37834
- ownerId: args.ownerId,
37835
- provider: args.provider,
37836
- displayName: args.displayName,
37837
- remoteUrl: args.selfUrl,
37838
- connectToken: "",
37839
- grants,
37840
- addedAt: now()
37841
- };
37842
- args.store.upsert(base);
37843
- args.broadcast({ type: "contact:added", contact: base });
37844
- if (!args.exchangeToken) return;
37845
- let token;
37846
- try {
37847
- token = await args.exchangeToken(args.deviceId);
37848
- } catch {
37849
- return;
37850
- }
37851
- if (!token) return;
37852
- const upgraded = { ...base, connectToken: token };
37853
- args.store.upsert(upgraded);
37854
- args.broadcast({ type: "contact:added", contact: upgraded });
37855
- }
37856
-
37857
37806
  // src/migrations/2026-05-20-flatten-sessions.ts
37858
- var fs21 = __toESM(require("fs"), 1);
37859
- var path23 = __toESM(require("path"), 1);
37807
+ var fs22 = __toESM(require("fs"), 1);
37808
+ var path24 = __toESM(require("path"), 1);
37860
37809
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37861
37810
  function migrateFlattenSessions(opts) {
37862
37811
  const dataDir = opts.dataDir;
37863
37812
  const now = opts.now ?? Date.now;
37864
- const sessionsDir = path23.join(dataDir, "sessions");
37865
- const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37813
+ const sessionsDir = path24.join(dataDir, "sessions");
37814
+ const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37866
37815
  if (existsSync3(flagPath)) {
37867
37816
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37868
37817
  }
37869
37818
  let movedBare = 0;
37870
37819
  let movedVmOwner = 0;
37871
37820
  let archivedListener = 0;
37872
- const defaultDir = path23.join(sessionsDir, "default");
37821
+ const defaultDir = path24.join(sessionsDir, "default");
37873
37822
  if (existsSync3(defaultDir)) {
37874
37823
  for (const entry of readdirSafe(defaultDir)) {
37875
37824
  if (!entry.endsWith(".json")) continue;
37876
- const src = path23.join(defaultDir, entry);
37877
- const dst = path23.join(sessionsDir, entry);
37878
- fs21.renameSync(src, dst);
37825
+ const src = path24.join(defaultDir, entry);
37826
+ const dst = path24.join(sessionsDir, entry);
37827
+ fs22.renameSync(src, dst);
37879
37828
  movedBare += 1;
37880
37829
  }
37881
37830
  rmdirIfEmpty(defaultDir);
37882
37831
  }
37883
37832
  for (const pid of readdirSafe(sessionsDir)) {
37884
- const personaDir = path23.join(sessionsDir, pid);
37833
+ const personaDir = path24.join(sessionsDir, pid);
37885
37834
  if (!isDir(personaDir)) continue;
37886
37835
  if (pid === "default") continue;
37887
- const ownerSrc = path23.join(personaDir, "owner");
37836
+ const ownerSrc = path24.join(personaDir, "owner");
37888
37837
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37889
- const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37890
- fs21.mkdirSync(ownerDst, { recursive: true });
37838
+ const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37839
+ fs22.mkdirSync(ownerDst, { recursive: true });
37891
37840
  for (const file of readdirSafe(ownerSrc)) {
37892
37841
  if (!file.endsWith(".json")) continue;
37893
- fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37842
+ fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37894
37843
  movedVmOwner += 1;
37895
37844
  }
37896
37845
  rmdirIfEmpty(ownerSrc);
37897
37846
  }
37898
- const listenerSrc = path23.join(personaDir, "listener");
37847
+ const listenerSrc = path24.join(personaDir, "listener");
37899
37848
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37900
- const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37901
- fs21.mkdirSync(archiveDst, { recursive: true });
37849
+ const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37850
+ fs22.mkdirSync(archiveDst, { recursive: true });
37902
37851
  for (const file of readdirSafe(listenerSrc)) {
37903
37852
  if (!file.endsWith(".json")) continue;
37904
- fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37853
+ fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37905
37854
  archivedListener += 1;
37906
37855
  }
37907
37856
  rmdirIfEmpty(listenerSrc);
37908
37857
  }
37909
37858
  rmdirIfEmpty(personaDir);
37910
37859
  }
37911
- fs21.mkdirSync(sessionsDir, { recursive: true });
37912
- fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37860
+ fs22.mkdirSync(sessionsDir, { recursive: true });
37861
+ fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37913
37862
  return {
37914
37863
  skipped: false,
37915
37864
  flagWritten: true,
@@ -37920,7 +37869,7 @@ function migrateFlattenSessions(opts) {
37920
37869
  }
37921
37870
  function existsSync3(p2) {
37922
37871
  try {
37923
- fs21.statSync(p2);
37872
+ fs22.statSync(p2);
37924
37873
  return true;
37925
37874
  } catch {
37926
37875
  return false;
@@ -37928,31 +37877,31 @@ function existsSync3(p2) {
37928
37877
  }
37929
37878
  function isDir(p2) {
37930
37879
  try {
37931
- return fs21.statSync(p2).isDirectory();
37880
+ return fs22.statSync(p2).isDirectory();
37932
37881
  } catch {
37933
37882
  return false;
37934
37883
  }
37935
37884
  }
37936
37885
  function readdirSafe(p2) {
37937
37886
  try {
37938
- return fs21.readdirSync(p2);
37887
+ return fs22.readdirSync(p2);
37939
37888
  } catch {
37940
37889
  return [];
37941
37890
  }
37942
37891
  }
37943
37892
  function rmdirIfEmpty(p2) {
37944
37893
  try {
37945
- fs21.rmdirSync(p2);
37894
+ fs22.rmdirSync(p2);
37946
37895
  } catch {
37947
37896
  }
37948
37897
  }
37949
37898
 
37950
37899
  // src/transport/http-router.ts
37951
- var import_node_fs16 = __toESM(require("fs"), 1);
37952
- var import_node_path19 = __toESM(require("path"), 1);
37900
+ var import_node_fs17 = __toESM(require("fs"), 1);
37901
+ var import_node_path20 = __toESM(require("path"), 1);
37953
37902
 
37954
37903
  // src/attachment/mime.ts
37955
- var import_node_path16 = __toESM(require("path"), 1);
37904
+ var import_node_path17 = __toESM(require("path"), 1);
37956
37905
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37957
37906
  var EXT_TO_NATIVE_MIME = {
37958
37907
  // 图片
@@ -38059,14 +38008,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
38059
38008
  ".mk"
38060
38009
  ]);
38061
38010
  function lookupMime(filePathOrName) {
38062
- const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
38011
+ const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
38063
38012
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
38064
38013
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
38065
38014
  return "application/octet-stream";
38066
38015
  }
38067
38016
 
38068
38017
  // src/attachment/sign-url.ts
38069
- var import_node_crypto4 = __toESM(require("crypto"), 1);
38018
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
38070
38019
  var HMAC_ALGO = "sha256";
38071
38020
  function base64urlEncode(buf) {
38072
38021
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -38083,7 +38032,7 @@ function decodeAbsPathFromUrl(encoded) {
38083
38032
  }
38084
38033
  function computeSig(secret, absPath, e) {
38085
38034
  const msg = e === null ? absPath : `${absPath}|${e}`;
38086
- return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
38035
+ return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
38087
38036
  }
38088
38037
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
38089
38038
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -38118,7 +38067,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38118
38067
  if (provided.length !== expected.length) {
38119
38068
  return { ok: false, code: "BAD_SIG" };
38120
38069
  }
38121
- if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
38070
+ if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
38122
38071
  return { ok: false, code: "BAD_SIG" };
38123
38072
  }
38124
38073
  if (e !== null && now() / 1e3 > e) {
@@ -38128,9 +38077,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38128
38077
  }
38129
38078
 
38130
38079
  // src/attachment/upload.ts
38131
- var import_node_fs15 = __toESM(require("fs"), 1);
38132
- var import_node_path17 = __toESM(require("path"), 1);
38133
- var import_node_crypto5 = __toESM(require("crypto"), 1);
38080
+ var import_node_fs16 = __toESM(require("fs"), 1);
38081
+ var import_node_path18 = __toESM(require("path"), 1);
38082
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38134
38083
  var import_promises = require("stream/promises");
38135
38084
  var UploadError = class extends Error {
38136
38085
  constructor(code, message) {
@@ -38141,24 +38090,24 @@ var UploadError = class extends Error {
38141
38090
  code;
38142
38091
  };
38143
38092
  function assertValidFileName(fileName) {
38144
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
38093
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
38145
38094
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
38146
38095
  }
38147
38096
  }
38148
38097
  var HASH_PREFIX_LEN = 16;
38149
38098
  async function writeUploadedAttachment(args) {
38150
38099
  assertValidFileName(args.fileName);
38151
- const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
38100
+ const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
38152
38101
  try {
38153
- import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
38102
+ import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
38154
38103
  } catch (err) {
38155
38104
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
38156
38105
  }
38157
- const hasher = import_node_crypto5.default.createHash("sha256");
38106
+ const hasher = import_node_crypto6.default.createHash("sha256");
38158
38107
  let actualSize = 0;
38159
- const tmpPath = import_node_path17.default.join(
38108
+ const tmpPath = import_node_path18.default.join(
38160
38109
  attachmentsRoot,
38161
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
38110
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
38162
38111
  );
38163
38112
  try {
38164
38113
  await (0, import_promises.pipeline)(
@@ -38171,18 +38120,18 @@ async function writeUploadedAttachment(args) {
38171
38120
  yield buf;
38172
38121
  }
38173
38122
  },
38174
- import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
38123
+ import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
38175
38124
  );
38176
38125
  } catch (err) {
38177
38126
  try {
38178
- import_node_fs15.default.unlinkSync(tmpPath);
38127
+ import_node_fs16.default.unlinkSync(tmpPath);
38179
38128
  } catch {
38180
38129
  }
38181
38130
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
38182
38131
  }
38183
38132
  if (actualSize !== args.contentLength) {
38184
38133
  try {
38185
- import_node_fs15.default.unlinkSync(tmpPath);
38134
+ import_node_fs16.default.unlinkSync(tmpPath);
38186
38135
  } catch {
38187
38136
  }
38188
38137
  throw new UploadError(
@@ -38191,35 +38140,35 @@ async function writeUploadedAttachment(args) {
38191
38140
  );
38192
38141
  }
38193
38142
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
38194
- const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
38143
+ const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
38195
38144
  let finalFileName;
38196
38145
  let hashDirExists = false;
38197
38146
  try {
38198
- hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
38147
+ hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
38199
38148
  } catch {
38200
38149
  }
38201
38150
  if (hashDirExists) {
38202
- const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38151
+ const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38203
38152
  finalFileName = existing[0] ?? args.fileName;
38204
38153
  try {
38205
- import_node_fs15.default.unlinkSync(tmpPath);
38154
+ import_node_fs16.default.unlinkSync(tmpPath);
38206
38155
  } catch {
38207
38156
  }
38208
38157
  } else {
38209
38158
  try {
38210
- import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
38159
+ import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
38211
38160
  finalFileName = args.fileName;
38212
- import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
38161
+ import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
38213
38162
  } catch (err) {
38214
38163
  try {
38215
- import_node_fs15.default.unlinkSync(tmpPath);
38164
+ import_node_fs16.default.unlinkSync(tmpPath);
38216
38165
  } catch {
38217
38166
  }
38218
38167
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
38219
38168
  }
38220
38169
  }
38221
- const absPath = import_node_path17.default.join(hashDir, finalFileName);
38222
- const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
38170
+ const absPath = import_node_path18.default.join(hashDir, finalFileName);
38171
+ const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
38223
38172
  args.groupFileStore.upsert(args.scope, args.sessionId, {
38224
38173
  relPath: absPath,
38225
38174
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -38233,7 +38182,7 @@ async function writeUploadedAttachment(args) {
38233
38182
 
38234
38183
  // src/extension/import.ts
38235
38184
  var import_promises2 = __toESM(require("fs/promises"), 1);
38236
- var import_node_path18 = __toESM(require("path"), 1);
38185
+ var import_node_path19 = __toESM(require("path"), 1);
38237
38186
  var import_node_os10 = __toESM(require("os"), 1);
38238
38187
  var import_jszip = __toESM(require_lib3(), 1);
38239
38188
  var ImportError = class extends Error {
@@ -38251,7 +38200,7 @@ async function importZip(buf, root) {
38251
38200
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
38252
38201
  }
38253
38202
  for (const name of Object.keys(zip.files)) {
38254
- if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
38203
+ if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
38255
38204
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
38256
38205
  }
38257
38206
  }
@@ -38284,7 +38233,7 @@ async function importZip(buf, root) {
38284
38233
  );
38285
38234
  }
38286
38235
  }
38287
- const destDir = import_node_path18.default.join(root, manifest.id);
38236
+ const destDir = import_node_path19.default.join(root, manifest.id);
38288
38237
  let destExists = false;
38289
38238
  try {
38290
38239
  await import_promises2.default.access(destDir);
@@ -38294,15 +38243,15 @@ async function importZip(buf, root) {
38294
38243
  if (destExists) {
38295
38244
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38296
38245
  }
38297
- const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38246
+ const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38298
38247
  try {
38299
38248
  for (const [name, entry] of Object.entries(zip.files)) {
38300
- const dest = import_node_path18.default.join(stage, name);
38249
+ const dest = import_node_path19.default.join(stage, name);
38301
38250
  if (entry.dir) {
38302
38251
  await import_promises2.default.mkdir(dest, { recursive: true });
38303
38252
  continue;
38304
38253
  }
38305
- await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38254
+ await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
38306
38255
  const content = await entry.async("nodebuffer");
38307
38256
  await import_promises2.default.writeFile(dest, content);
38308
38257
  }
@@ -38416,7 +38365,7 @@ function isValidUploadFileName(fileName) {
38416
38365
  if (fileName === "." || fileName === "..") return false;
38417
38366
  if (fileName.startsWith(".")) return false;
38418
38367
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38419
- return fileName === import_node_path19.default.basename(fileName);
38368
+ return fileName === import_node_path20.default.basename(fileName);
38420
38369
  }
38421
38370
  function createHttpRouter(deps) {
38422
38371
  return async (req, res) => {
@@ -38431,25 +38380,6 @@ function createHttpRouter(deps) {
38431
38380
  });
38432
38381
  return true;
38433
38382
  }
38434
- if (url.pathname === "/auth/callback") {
38435
- if (req.method !== "GET") {
38436
- sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38437
- return true;
38438
- }
38439
- if (!deps.feishuLogin) {
38440
- sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38441
- return true;
38442
- }
38443
- const result = await deps.feishuLogin.handleLoginCallback({
38444
- token: url.searchParams.get("token") ?? "",
38445
- state: url.searchParams.get("state") ?? "",
38446
- expiresAt: url.searchParams.get("expires_at")
38447
- });
38448
- const html = result.ok ? `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#222}</style><h2>\u767B\u5F55\u6210\u529F\uFF0C\u53EF\u5173\u95ED\u6B64\u9875\u3002</h2><p>\u8EAB\u4EFD\uFF1A${escapeHtml(result.identity.displayName)}</p><script>setTimeout(()=>window.close(),1500)</script>` : `<!doctype html><meta charset="utf-8"><title>clawd</title><style>body{font-family:-apple-system,sans-serif;padding:48px;color:#922}</style><h2>\u767B\u5F55\u5931\u8D25</h2><p>${escapeHtml(result.reason)}</p>`;
38449
- res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38450
- res.end(html);
38451
- return true;
38452
- }
38453
38383
  if (url.pathname.startsWith(RPC_ROUTE_PREFIX)) {
38454
38384
  if (!deps.rpcDispatcher) {
38455
38385
  sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "HTTP RPC adapter not wired" });
@@ -38645,7 +38575,7 @@ function createHttpRouter(deps) {
38645
38575
  return true;
38646
38576
  }
38647
38577
  let absPath;
38648
- if (import_node_path19.default.isAbsolute(pathParam)) {
38578
+ if (import_node_path20.default.isAbsolute(pathParam)) {
38649
38579
  absPath = pathParam;
38650
38580
  } else if (deps.sessionStore) {
38651
38581
  const file = deps.sessionStore.read(sid);
@@ -38653,7 +38583,7 @@ function createHttpRouter(deps) {
38653
38583
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38654
38584
  return true;
38655
38585
  }
38656
- absPath = import_node_path19.default.join(file.cwd, pathParam);
38586
+ absPath = import_node_path20.default.join(file.cwd, pathParam);
38657
38587
  } else {
38658
38588
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38659
38589
  return true;
@@ -38720,9 +38650,6 @@ function parseUrl(rawUrl) {
38720
38650
  return null;
38721
38651
  }
38722
38652
  }
38723
- function escapeHtml(s) {
38724
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38725
- }
38726
38653
  function sendJson(res, status, body, extraHeaders) {
38727
38654
  res.writeHead(status, {
38728
38655
  "Content-Type": "application/json; charset=utf-8",
@@ -38736,7 +38663,7 @@ function withCtx(ctx, body) {
38736
38663
  function streamFile(res, absPath, logger) {
38737
38664
  let stat;
38738
38665
  try {
38739
- stat = import_node_fs16.default.statSync(absPath);
38666
+ stat = import_node_fs17.default.statSync(absPath);
38740
38667
  } catch (err) {
38741
38668
  const code = err?.code;
38742
38669
  if (code === "ENOENT") {
@@ -38751,7 +38678,7 @@ function streamFile(res, absPath, logger) {
38751
38678
  return;
38752
38679
  }
38753
38680
  const mime = lookupMime(absPath);
38754
- const basename = import_node_path19.default.basename(absPath);
38681
+ const basename = import_node_path20.default.basename(absPath);
38755
38682
  res.writeHead(200, {
38756
38683
  "Content-Type": mime,
38757
38684
  "Content-Length": String(stat.size),
@@ -38759,7 +38686,7 @@ function streamFile(res, absPath, logger) {
38759
38686
  // 防止浏览器把任意 mime 当 html 渲染
38760
38687
  "X-Content-Type-Options": "nosniff"
38761
38688
  });
38762
- const stream = import_node_fs16.default.createReadStream(absPath);
38689
+ const stream = import_node_fs17.default.createReadStream(absPath);
38763
38690
  stream.on("error", (err) => {
38764
38691
  logger?.warn("streamFile read error", { absPath, err: err.message });
38765
38692
  res.destroy();
@@ -38768,8 +38695,8 @@ function streamFile(res, absPath, logger) {
38768
38695
  }
38769
38696
 
38770
38697
  // src/attachment/gc.ts
38771
- var import_node_fs17 = __toESM(require("fs"), 1);
38772
- var import_node_path20 = __toESM(require("path"), 1);
38698
+ var import_node_fs18 = __toESM(require("fs"), 1);
38699
+ var import_node_path21 = __toESM(require("path"), 1);
38773
38700
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38774
38701
  function runAttachmentGc(args) {
38775
38702
  const now = (args.now ?? Date.now)();
@@ -38778,38 +38705,38 @@ function runAttachmentGc(args) {
38778
38705
  for (const { scope, sessionId } of args.sessionScopes) {
38779
38706
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38780
38707
  if (entry.stale) continue;
38781
- if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38708
+ if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38782
38709
  }
38783
38710
  }
38784
38711
  for (const { scope, sessionId } of args.sessionScopes) {
38785
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38712
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38786
38713
  args.dataDir,
38787
38714
  "sessions",
38788
38715
  ...scopeSubPath(scope).map(safeFileName),
38789
38716
  safeFileName(sessionId)
38790
38717
  );
38791
- const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38718
+ const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38792
38719
  let hashDirs;
38793
38720
  try {
38794
- hashDirs = import_node_fs17.default.readdirSync(attRoot);
38721
+ hashDirs = import_node_fs18.default.readdirSync(attRoot);
38795
38722
  } catch (err) {
38796
38723
  if (err.code === "ENOENT") continue;
38797
38724
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38798
38725
  continue;
38799
38726
  }
38800
38727
  for (const hashDir of hashDirs) {
38801
- const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38728
+ const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38802
38729
  let files;
38803
38730
  try {
38804
- files = import_node_fs17.default.readdirSync(hashDirAbs);
38731
+ files = import_node_fs18.default.readdirSync(hashDirAbs);
38805
38732
  } catch {
38806
38733
  continue;
38807
38734
  }
38808
38735
  for (const name of files) {
38809
- const file = import_node_path20.default.join(hashDirAbs, name);
38736
+ const file = import_node_path21.default.join(hashDirAbs, name);
38810
38737
  let stat;
38811
38738
  try {
38812
- stat = import_node_fs17.default.statSync(file);
38739
+ stat = import_node_fs18.default.statSync(file);
38813
38740
  } catch {
38814
38741
  continue;
38815
38742
  }
@@ -38818,27 +38745,27 @@ function runAttachmentGc(args) {
38818
38745
  if (age < ttlMs) continue;
38819
38746
  if (liveAbs.has(file)) continue;
38820
38747
  try {
38821
- import_node_fs17.default.unlinkSync(file);
38748
+ import_node_fs18.default.unlinkSync(file);
38822
38749
  } catch (err) {
38823
38750
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38824
38751
  }
38825
38752
  }
38826
38753
  try {
38827
- if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38754
+ if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38828
38755
  } catch {
38829
38756
  }
38830
38757
  }
38831
38758
  try {
38832
- if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38759
+ if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38833
38760
  } catch {
38834
38761
  }
38835
38762
  }
38836
38763
  }
38837
38764
 
38838
38765
  // src/attachment/group.ts
38839
- var import_node_fs18 = __toESM(require("fs"), 1);
38840
- var import_node_path21 = __toESM(require("path"), 1);
38841
- var import_node_crypto6 = __toESM(require("crypto"), 1);
38766
+ var import_node_fs19 = __toESM(require("fs"), 1);
38767
+ var import_node_path22 = __toESM(require("path"), 1);
38768
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38842
38769
  init_protocol();
38843
38770
  var GroupFileStore = class {
38844
38771
  dataDir;
@@ -38849,11 +38776,11 @@ var GroupFileStore = class {
38849
38776
  this.logger = opts.logger;
38850
38777
  }
38851
38778
  rootForScope(scope) {
38852
- return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38779
+ return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38853
38780
  }
38854
38781
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38855
38782
  filePath(scope, sessionId) {
38856
- return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38783
+ return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38857
38784
  }
38858
38785
  cacheKey(scope, sessionId) {
38859
38786
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38862,7 +38789,7 @@ var GroupFileStore = class {
38862
38789
  readFile(scope, sessionId) {
38863
38790
  const file = this.filePath(scope, sessionId);
38864
38791
  try {
38865
- const raw = import_node_fs18.default.readFileSync(file, "utf8");
38792
+ const raw = import_node_fs19.default.readFileSync(file, "utf8");
38866
38793
  const parsed = JSON.parse(raw);
38867
38794
  if (!Array.isArray(parsed)) {
38868
38795
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38888,10 +38815,10 @@ var GroupFileStore = class {
38888
38815
  }
38889
38816
  writeFile(scope, sessionId, entries) {
38890
38817
  const file = this.filePath(scope, sessionId);
38891
- import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38818
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38892
38819
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38893
- import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38894
- import_node_fs18.default.renameSync(tmp, file);
38820
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38821
+ import_node_fs19.default.renameSync(tmp, file);
38895
38822
  }
38896
38823
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38897
38824
  list(scope, sessionId) {
@@ -38927,7 +38854,7 @@ var GroupFileStore = class {
38927
38854
  entries[idx] = next;
38928
38855
  } else {
38929
38856
  next = {
38930
- id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38857
+ id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38931
38858
  relPath: input.relPath,
38932
38859
  from: input.from,
38933
38860
  label: input.label,
@@ -38977,10 +38904,10 @@ var GroupFileStore = class {
38977
38904
  };
38978
38905
 
38979
38906
  // src/discovery/state-file.ts
38980
- var import_node_fs19 = __toESM(require("fs"), 1);
38981
- var import_node_path22 = __toESM(require("path"), 1);
38907
+ var import_node_fs20 = __toESM(require("fs"), 1);
38908
+ var import_node_path23 = __toESM(require("path"), 1);
38982
38909
  function defaultStateFilePath(dataDir) {
38983
- return import_node_path22.default.join(dataDir, "state.json");
38910
+ return import_node_path23.default.join(dataDir, "state.json");
38984
38911
  }
38985
38912
  function isPidAlive(pid) {
38986
38913
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -39002,7 +38929,7 @@ var StateFileManager = class {
39002
38929
  }
39003
38930
  read() {
39004
38931
  try {
39005
- const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38932
+ const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
39006
38933
  const parsed = JSON.parse(raw);
39007
38934
  return parsed;
39008
38935
  } catch {
@@ -39016,34 +38943,34 @@ var StateFileManager = class {
39016
38943
  return { status: "stale", existing };
39017
38944
  }
39018
38945
  write(state) {
39019
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38946
+ import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
39020
38947
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
39021
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
39022
- import_node_fs19.default.renameSync(tmp, this.file);
38948
+ import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38949
+ import_node_fs20.default.renameSync(tmp, this.file);
39023
38950
  if (process.platform !== "win32") {
39024
38951
  try {
39025
- import_node_fs19.default.chmodSync(this.file, 384);
38952
+ import_node_fs20.default.chmodSync(this.file, 384);
39026
38953
  } catch {
39027
38954
  }
39028
38955
  }
39029
38956
  }
39030
38957
  delete() {
39031
38958
  try {
39032
- import_node_fs19.default.unlinkSync(this.file);
38959
+ import_node_fs20.default.unlinkSync(this.file);
39033
38960
  } catch {
39034
38961
  }
39035
38962
  }
39036
38963
  };
39037
38964
 
39038
38965
  // src/tunnel/tunnel-manager.ts
39039
- var import_node_fs23 = __toESM(require("fs"), 1);
39040
- var import_node_path26 = __toESM(require("path"), 1);
39041
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38966
+ var import_node_fs24 = __toESM(require("fs"), 1);
38967
+ var import_node_path27 = __toESM(require("path"), 1);
38968
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
39042
38969
  var import_node_child_process5 = require("child_process");
39043
38970
 
39044
38971
  // src/tunnel/tunnel-store.ts
39045
- var import_node_fs20 = __toESM(require("fs"), 1);
39046
- var import_node_path23 = __toESM(require("path"), 1);
38972
+ var import_node_fs21 = __toESM(require("fs"), 1);
38973
+ var import_node_path24 = __toESM(require("path"), 1);
39047
38974
  var TunnelStore = class {
39048
38975
  constructor(filePath) {
39049
38976
  this.filePath = filePath;
@@ -39051,7 +38978,7 @@ var TunnelStore = class {
39051
38978
  filePath;
39052
38979
  async get() {
39053
38980
  try {
39054
- const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38981
+ const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
39055
38982
  const obj = JSON.parse(raw);
39056
38983
  if (!isPersistedTunnel(obj)) return null;
39057
38984
  return obj;
@@ -39062,22 +38989,22 @@ var TunnelStore = class {
39062
38989
  }
39063
38990
  }
39064
38991
  async set(v2) {
39065
- const dir = import_node_path23.default.dirname(this.filePath);
39066
- await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38992
+ const dir = import_node_path24.default.dirname(this.filePath);
38993
+ await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
39067
38994
  const data = JSON.stringify(v2, null, 2);
39068
38995
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
39069
- await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38996
+ await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
39070
38997
  if (process.platform !== "win32") {
39071
38998
  try {
39072
- await import_node_fs20.default.promises.chmod(tmp, 384);
38999
+ await import_node_fs21.default.promises.chmod(tmp, 384);
39073
39000
  } catch {
39074
39001
  }
39075
39002
  }
39076
- await import_node_fs20.default.promises.rename(tmp, this.filePath);
39003
+ await import_node_fs21.default.promises.rename(tmp, this.filePath);
39077
39004
  }
39078
39005
  async clear() {
39079
39006
  try {
39080
- await import_node_fs20.default.promises.unlink(this.filePath);
39007
+ await import_node_fs21.default.promises.unlink(this.filePath);
39081
39008
  } catch (err) {
39082
39009
  const code = err?.code;
39083
39010
  if (code !== "ENOENT") throw err;
@@ -39172,9 +39099,9 @@ function escape(v2) {
39172
39099
  }
39173
39100
 
39174
39101
  // src/tunnel/frpc-binary.ts
39175
- var import_node_fs21 = __toESM(require("fs"), 1);
39102
+ var import_node_fs22 = __toESM(require("fs"), 1);
39176
39103
  var import_node_os11 = __toESM(require("os"), 1);
39177
- var import_node_path24 = __toESM(require("path"), 1);
39104
+ var import_node_path25 = __toESM(require("path"), 1);
39178
39105
  var import_node_child_process3 = require("child_process");
39179
39106
  var import_node_stream2 = require("stream");
39180
39107
  var import_promises3 = require("stream/promises");
@@ -39206,20 +39133,20 @@ function frpcDownloadUrl(version2, p2) {
39206
39133
  }
39207
39134
  async function ensureFrpcBinary(opts) {
39208
39135
  if (opts.override) {
39209
- if (!import_node_fs21.default.existsSync(opts.override)) {
39136
+ if (!import_node_fs22.default.existsSync(opts.override)) {
39210
39137
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
39211
39138
  }
39212
39139
  return opts.override;
39213
39140
  }
39214
39141
  const version2 = opts.version ?? FRPC_VERSION;
39215
39142
  const platform = opts.platform ?? detectPlatform();
39216
- const binDir = import_node_path24.default.join(opts.dataDir, "bin");
39217
- import_node_fs21.default.mkdirSync(binDir, { recursive: true });
39143
+ const binDir = import_node_path25.default.join(opts.dataDir, "bin");
39144
+ import_node_fs22.default.mkdirSync(binDir, { recursive: true });
39218
39145
  cleanupStaleArtifacts(binDir);
39219
- const stableBin = import_node_path24.default.join(binDir, "frpc");
39220
- if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
39146
+ const stableBin = import_node_path25.default.join(binDir, "frpc");
39147
+ if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
39221
39148
  const partialBin = `${stableBin}.partial`;
39222
- const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39149
+ const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39223
39150
  try {
39224
39151
  const url = frpcDownloadUrl(version2, platform);
39225
39152
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -39228,8 +39155,8 @@ async function ensureFrpcBinary(opts) {
39228
39155
  } else {
39229
39156
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
39230
39157
  }
39231
- import_node_fs21.default.chmodSync(partialBin, 493);
39232
- import_node_fs21.default.renameSync(partialBin, stableBin);
39158
+ import_node_fs22.default.chmodSync(partialBin, 493);
39159
+ import_node_fs22.default.renameSync(partialBin, stableBin);
39233
39160
  } finally {
39234
39161
  safeUnlink(tarballPath);
39235
39162
  safeUnlink(partialBin);
@@ -39239,15 +39166,15 @@ async function ensureFrpcBinary(opts) {
39239
39166
  function cleanupStaleArtifacts(binDir) {
39240
39167
  let entries;
39241
39168
  try {
39242
- entries = import_node_fs21.default.readdirSync(binDir);
39169
+ entries = import_node_fs22.default.readdirSync(binDir);
39243
39170
  } catch {
39244
39171
  return;
39245
39172
  }
39246
39173
  for (const name of entries) {
39247
39174
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
39248
- const full = import_node_path24.default.join(binDir, name);
39175
+ const full = import_node_path25.default.join(binDir, name);
39249
39176
  try {
39250
- import_node_fs21.default.rmSync(full, { recursive: true, force: true });
39177
+ import_node_fs22.default.rmSync(full, { recursive: true, force: true });
39251
39178
  } catch {
39252
39179
  }
39253
39180
  }
@@ -39255,7 +39182,7 @@ function cleanupStaleArtifacts(binDir) {
39255
39182
  }
39256
39183
  function safeUnlink(p2) {
39257
39184
  try {
39258
- import_node_fs21.default.unlinkSync(p2);
39185
+ import_node_fs22.default.unlinkSync(p2);
39259
39186
  } catch {
39260
39187
  }
39261
39188
  }
@@ -39266,13 +39193,13 @@ async function downloadToFile(url, dest, fetchImpl) {
39266
39193
  if (!res.ok || !res.body) {
39267
39194
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
39268
39195
  }
39269
- const out = import_node_fs21.default.createWriteStream(dest);
39196
+ const out = import_node_fs22.default.createWriteStream(dest);
39270
39197
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
39271
39198
  await (0, import_promises3.pipeline)(nodeStream, out);
39272
39199
  }
39273
39200
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
39274
- const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39275
- import_node_fs21.default.mkdirSync(work, { recursive: true });
39201
+ const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39202
+ import_node_fs22.default.mkdirSync(work, { recursive: true });
39276
39203
  try {
39277
39204
  await new Promise((resolve6, reject) => {
39278
39205
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -39280,32 +39207,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
39280
39207
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
39281
39208
  });
39282
39209
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
39283
- const src = import_node_path24.default.join(work, dirName, "frpc");
39284
- if (!import_node_fs21.default.existsSync(src)) {
39210
+ const src = import_node_path25.default.join(work, dirName, "frpc");
39211
+ if (!import_node_fs22.default.existsSync(src)) {
39285
39212
  throw new Error(`frpc not found inside tarball at ${src}`);
39286
39213
  }
39287
- import_node_fs21.default.copyFileSync(src, destBin);
39214
+ import_node_fs22.default.copyFileSync(src, destBin);
39288
39215
  } finally {
39289
- import_node_fs21.default.rmSync(work, { recursive: true, force: true });
39216
+ import_node_fs22.default.rmSync(work, { recursive: true, force: true });
39290
39217
  }
39291
39218
  }
39292
39219
 
39293
39220
  // src/tunnel/frpc-process.ts
39294
- var import_node_fs22 = __toESM(require("fs"), 1);
39295
- var import_node_path25 = __toESM(require("path"), 1);
39221
+ var import_node_fs23 = __toESM(require("fs"), 1);
39222
+ var import_node_path26 = __toESM(require("path"), 1);
39296
39223
  var import_node_child_process4 = require("child_process");
39297
39224
  function frpcPidFilePath(dataDir) {
39298
- return import_node_path25.default.join(dataDir, "frpc.pid");
39225
+ return import_node_path26.default.join(dataDir, "frpc.pid");
39299
39226
  }
39300
39227
  function writeFrpcPid(dataDir, pid) {
39301
39228
  try {
39302
- import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39229
+ import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39303
39230
  } catch {
39304
39231
  }
39305
39232
  }
39306
39233
  function clearFrpcPid(dataDir) {
39307
39234
  try {
39308
- import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
39235
+ import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
39309
39236
  } catch {
39310
39237
  }
39311
39238
  }
@@ -39321,7 +39248,7 @@ function defaultIsPidAlive(pid) {
39321
39248
  }
39322
39249
  function defaultReadPidFile(file) {
39323
39250
  try {
39324
- return import_node_fs22.default.readFileSync(file, "utf8");
39251
+ return import_node_fs23.default.readFileSync(file, "utf8");
39325
39252
  } catch {
39326
39253
  return null;
39327
39254
  }
@@ -39337,7 +39264,7 @@ function defaultSleep(ms) {
39337
39264
  }
39338
39265
  async function killStaleFrpc(deps) {
39339
39266
  const pidFile = frpcPidFilePath(deps.dataDir);
39340
- const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
39267
+ const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
39341
39268
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
39342
39269
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
39343
39270
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -39361,7 +39288,7 @@ async function killStaleFrpc(deps) {
39361
39288
  }
39362
39289
  if (victims.size === 0) {
39363
39290
  try {
39364
- import_node_fs22.default.unlinkSync(pidFile);
39291
+ import_node_fs23.default.unlinkSync(pidFile);
39365
39292
  } catch {
39366
39293
  }
39367
39294
  return;
@@ -39372,7 +39299,7 @@ async function killStaleFrpc(deps) {
39372
39299
  }
39373
39300
  await sleep(deps.reapWaitMs ?? 300);
39374
39301
  try {
39375
- import_node_fs22.default.unlinkSync(pidFile);
39302
+ import_node_fs23.default.unlinkSync(pidFile);
39376
39303
  } catch {
39377
39304
  }
39378
39305
  }
@@ -39409,7 +39336,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
39409
39336
  var TunnelManager = class {
39410
39337
  constructor(deps) {
39411
39338
  this.deps = deps;
39412
- this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39339
+ this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
39413
39340
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39414
39341
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39415
39342
  }
@@ -39536,8 +39463,8 @@ var TunnelManager = class {
39536
39463
  dataDir: this.deps.dataDir,
39537
39464
  override: this.deps.frpcBinaryOverride ?? void 0
39538
39465
  });
39539
- const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39540
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39466
+ const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39467
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39541
39468
  const toml = buildFrpcToml({
39542
39469
  serverAddr: t.frpsHost,
39543
39470
  serverPort: t.frpsPort,
@@ -39547,12 +39474,12 @@ var TunnelManager = class {
39547
39474
  localPort,
39548
39475
  logLevel: "info"
39549
39476
  });
39550
- await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39477
+ await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39551
39478
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39552
39479
  stdio: ["ignore", "pipe", "pipe"]
39553
39480
  });
39554
- const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39555
- const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39481
+ const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39482
+ const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39556
39483
  logStream.on("error", () => {
39557
39484
  });
39558
39485
  const tee = (chunk) => {
@@ -39635,61 +39562,59 @@ async function waitForFrpcReady(proc, timeoutMs) {
39635
39562
 
39636
39563
  // src/tunnel/device-key.ts
39637
39564
  var import_node_os12 = __toESM(require("os"), 1);
39638
- var import_node_path27 = __toESM(require("path"), 1);
39639
- var import_node_crypto8 = __toESM(require("crypto"), 1);
39565
+ var import_node_path28 = __toESM(require("path"), 1);
39566
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39640
39567
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39641
39568
  function deriveStableDeviceKey(opts = {}) {
39642
39569
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39643
39570
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39644
39571
  const home = opts.home ?? import_node_os12.default.homedir();
39645
- const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39646
- const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39572
+ const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39573
+ const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39647
39574
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39648
39575
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39649
- return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39576
+ return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39650
39577
  }
39651
39578
 
39652
39579
  // src/auth-store.ts
39653
- var import_node_fs24 = __toESM(require("fs"), 1);
39654
- var import_node_path28 = __toESM(require("path"), 1);
39655
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39580
+ var import_node_fs25 = __toESM(require("fs"), 1);
39581
+ var import_node_path29 = __toESM(require("path"), 1);
39582
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39656
39583
  var AUTH_FILE_NAME = "auth.json";
39657
39584
  function authFilePath(dataDir) {
39658
- return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39585
+ return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39659
39586
  }
39660
39587
  function loadOrCreateAuthFile(opts) {
39661
39588
  const file = authFilePath(opts.dataDir);
39662
- const generate = opts.generate ?? defaultGenerateToken;
39589
+ const generate = opts.generate ?? defaultGenerateToken2;
39663
39590
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39664
39591
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39665
39592
  const existing = readAuthFile(file);
39666
- if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId && existing.deviceId && existing.deviceId === existing.ownerPrincipalId) {
39593
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
39667
39594
  return {
39668
39595
  token: existing.token,
39669
39596
  signSecret: existing.signSecret,
39670
39597
  ownerPrincipalId: existing.ownerPrincipalId,
39671
- deviceId: existing.deviceId,
39672
39598
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
39673
39599
  };
39674
39600
  }
39675
39601
  const token = existing?.token || generate();
39676
39602
  const signSecret = existing?.signSecret || generate();
39677
39603
  const ownerPrincipalId = existing?.ownerPrincipalId || genId();
39678
- const deviceId = ownerPrincipalId;
39679
39604
  const createdAt = existing?.createdAt || now().toISOString();
39680
- const next = { token, signSecret, ownerPrincipalId, deviceId, createdAt };
39605
+ const next = { token, signSecret, ownerPrincipalId, createdAt };
39681
39606
  writeAuthFile(file, next);
39682
39607
  return next;
39683
39608
  }
39684
- function defaultGenerateToken() {
39685
- return import_node_crypto9.default.randomBytes(32).toString("base64url");
39609
+ function defaultGenerateToken2() {
39610
+ return import_node_crypto10.default.randomBytes(32).toString("base64url");
39686
39611
  }
39687
39612
  function defaultGenerateOwnerPrincipalId() {
39688
- return `owner-${import_node_crypto9.default.randomUUID()}`;
39613
+ return `owner-${import_node_crypto10.default.randomUUID()}`;
39689
39614
  }
39690
39615
  function readAuthFile(file) {
39691
39616
  try {
39692
- const raw = import_node_fs24.default.readFileSync(file, "utf8");
39617
+ const raw = import_node_fs25.default.readFileSync(file, "utf8");
39693
39618
  const parsed = JSON.parse(raw);
39694
39619
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39695
39620
  return null;
@@ -39698,7 +39623,6 @@ function readAuthFile(file) {
39698
39623
  token: parsed.token,
39699
39624
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
39700
39625
  ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
39701
- deviceId: typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? parsed.deviceId : void 0,
39702
39626
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
39703
39627
  };
39704
39628
  } catch (err) {
@@ -39708,25 +39632,25 @@ function readAuthFile(file) {
39708
39632
  }
39709
39633
  }
39710
39634
  function writeAuthFile(file, content) {
39711
- import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39712
- import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39635
+ import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39636
+ import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39713
39637
  try {
39714
- import_node_fs24.default.chmodSync(file, 384);
39638
+ import_node_fs25.default.chmodSync(file, 384);
39715
39639
  } catch {
39716
39640
  }
39717
39641
  }
39718
39642
 
39719
39643
  // src/owner-profile.ts
39720
- var import_node_fs25 = __toESM(require("fs"), 1);
39644
+ var import_node_fs26 = __toESM(require("fs"), 1);
39721
39645
  var import_node_os13 = __toESM(require("os"), 1);
39722
- var import_node_path29 = __toESM(require("path"), 1);
39646
+ var import_node_path30 = __toESM(require("path"), 1);
39723
39647
  var PROFILE_FILENAME = "profile.json";
39724
39648
  function loadOwnerDisplayName(dataDir) {
39725
39649
  const fallback = import_node_os13.default.userInfo().username;
39726
- const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39650
+ const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39727
39651
  let raw;
39728
39652
  try {
39729
- raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39653
+ raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39730
39654
  } catch {
39731
39655
  return fallback;
39732
39656
  }
@@ -39748,357 +39672,6 @@ function loadOwnerDisplayName(dataDir) {
39748
39672
  return displayName;
39749
39673
  }
39750
39674
 
39751
- // src/feishu-auth/owner-identity-store.ts
39752
- var import_node_fs26 = __toESM(require("fs"), 1);
39753
- var import_node_path30 = __toESM(require("path"), 1);
39754
- var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39755
- var OwnerIdentityStore = class {
39756
- file;
39757
- constructor(dataDir) {
39758
- this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39759
- }
39760
- read() {
39761
- let raw;
39762
- try {
39763
- raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39764
- } catch {
39765
- return null;
39766
- }
39767
- let parsed;
39768
- try {
39769
- parsed = JSON.parse(raw);
39770
- } catch {
39771
- return null;
39772
- }
39773
- const r = parsed;
39774
- if (!r.identity || typeof r.identity.ownerId !== "string" || r.identity.ownerId.length === 0 || typeof r.identity.provider !== "string" || r.identity.provider.length === 0 || typeof r.identity.displayName !== "string" || r.identity.displayName.length === 0 || typeof r.ttcToken !== "string" || r.ttcToken.length === 0) {
39775
- return null;
39776
- }
39777
- return {
39778
- identity: {
39779
- ownerId: r.identity.ownerId,
39780
- provider: r.identity.provider,
39781
- displayName: r.identity.displayName,
39782
- ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39783
- },
39784
- ttcToken: r.ttcToken,
39785
- ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39786
- loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39787
- };
39788
- }
39789
- write(record) {
39790
- import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39791
- import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39792
- try {
39793
- import_node_fs26.default.chmodSync(this.file, 384);
39794
- } catch {
39795
- }
39796
- }
39797
- clear() {
39798
- try {
39799
- import_node_fs26.default.unlinkSync(this.file);
39800
- } catch (err) {
39801
- const code = err?.code;
39802
- if (code !== "ENOENT") throw err;
39803
- }
39804
- }
39805
- };
39806
-
39807
- // src/feishu-auth/login-flow.ts
39808
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39809
- var STATE_TTL_MS = 5 * 60 * 1e3;
39810
- var LoginFlow = class {
39811
- constructor(deps) {
39812
- this.deps = deps;
39813
- }
39814
- deps;
39815
- pendingStates = /* @__PURE__ */ new Map();
39816
- start() {
39817
- const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39818
- const now = (this.deps.now ?? Date.now)();
39819
- this.pendingStates.set(state, now);
39820
- this.gcExpired(now);
39821
- const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39822
- url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39823
- url.searchParams.set("state", state);
39824
- url.searchParams.set("auto", "1");
39825
- return { authUrl: url.toString(), state };
39826
- }
39827
- async handleCallback(params) {
39828
- const now = (this.deps.now ?? Date.now)();
39829
- const issuedAt = this.pendingStates.get(params.state);
39830
- if (issuedAt === void 0) {
39831
- return { ok: false, reason: "state mismatch" };
39832
- }
39833
- this.pendingStates.delete(params.state);
39834
- if (now - issuedAt > STATE_TTL_MS) {
39835
- return { ok: false, reason: "state expired" };
39836
- }
39837
- if (!params.token) {
39838
- return { ok: false, reason: "missing token" };
39839
- }
39840
- let identity;
39841
- try {
39842
- identity = await this.deps.fetchUserInfo(params.token);
39843
- } catch (err) {
39844
- return { ok: false, reason: `user_info failed: ${err.message}` };
39845
- }
39846
- const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39847
- this.deps.store.write({
39848
- identity,
39849
- ttcToken: params.token,
39850
- ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39851
- loginAt: new Date(now).toISOString()
39852
- });
39853
- return { ok: true, identity };
39854
- }
39855
- gcExpired(now) {
39856
- for (const [state, issuedAt] of this.pendingStates) {
39857
- if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39858
- }
39859
- }
39860
- };
39861
-
39862
- // src/feishu-auth/ttc-client.ts
39863
- var TTC_HOSTS = {
39864
- auth: "https://app.ttcadvisory.com",
39865
- api: "https://api.ttcadvisory.com"
39866
- };
39867
- var TtcUserInfoError = class extends Error {
39868
- constructor(code, message) {
39869
- super(message);
39870
- this.code = code;
39871
- this.name = "TtcUserInfoError";
39872
- }
39873
- code;
39874
- };
39875
- async function fetchTtcUserInfo(opts) {
39876
- const doFetch = opts.fetchImpl ?? fetch;
39877
- let res;
39878
- try {
39879
- res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39880
- headers: { Authorization: `Bearer ${opts.token}` }
39881
- });
39882
- } catch (err) {
39883
- throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39884
- }
39885
- if (!res.ok) {
39886
- if (res.status === 401) {
39887
- throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39888
- }
39889
- throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39890
- }
39891
- const body = await res.json();
39892
- if (body.code !== 0) {
39893
- throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39894
- }
39895
- const d = body.data;
39896
- if (!d || typeof d.unique_id !== "string" || d.unique_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39897
- throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing unique_id or name");
39898
- }
39899
- return {
39900
- ownerId: d.unique_id,
39901
- provider: "ttc",
39902
- displayName: d.name,
39903
- ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39904
- };
39905
- }
39906
-
39907
- // src/feishu-auth/central-client.ts
39908
- var CentralClientError = class extends Error {
39909
- constructor(code, message, cause) {
39910
- super(message);
39911
- this.code = code;
39912
- this.cause = cause;
39913
- this.name = "CentralClientError";
39914
- }
39915
- code;
39916
- cause;
39917
- };
39918
- async function centralRequest(opts, path60, init) {
39919
- const f = opts.fetchImpl ?? globalThis.fetch;
39920
- const url = `${opts.api.replace(/\/+$/, "")}${path60}`;
39921
- const ctrl = new AbortController();
39922
- const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39923
- let res;
39924
- try {
39925
- res = await f(url, {
39926
- method: init.method,
39927
- headers: {
39928
- "content-type": "application/json",
39929
- ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39930
- },
39931
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39932
- signal: ctrl.signal
39933
- });
39934
- } catch (err) {
39935
- clearTimeout(timer);
39936
- throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39937
- }
39938
- clearTimeout(timer);
39939
- if (res.status === 401) {
39940
- throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39941
- }
39942
- if (!res.ok) {
39943
- throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39944
- }
39945
- try {
39946
- return await res.json();
39947
- } catch (err) {
39948
- throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39949
- }
39950
- }
39951
- async function centralExchange(opts) {
39952
- const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39953
- method: "POST",
39954
- body: {
39955
- jwt: opts.jwt,
39956
- provider: opts.provider,
39957
- deviceId: opts.deviceId,
39958
- selfDeviceId: opts.selfDeviceId
39959
- }
39960
- });
39961
- if (typeof body.token !== "string" || body.token.length === 0 || typeof body.ownerId !== "string" || typeof body.provider !== "string" || typeof body.displayName !== "string") {
39962
- throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/ownerId/provider/displayName");
39963
- }
39964
- const deviceUrl = typeof body.deviceUrl === "string" && body.deviceUrl.length > 0 ? body.deviceUrl : null;
39965
- return { token: body.token, ownerId: body.ownerId, provider: body.provider, displayName: body.displayName, deviceUrl };
39966
- }
39967
- async function centralGetPublicKey(opts) {
39968
- const body = await centralRequest(opts, "/api/clawd-identity/public-key", {
39969
- method: "GET"
39970
- });
39971
- if (typeof body.publicKeyPem !== "string" || !body.publicKeyPem.includes("PUBLIC KEY")) {
39972
- throw new CentralClientError("BAD_RESPONSE", "public-key: missing or malformed publicKeyPem");
39973
- }
39974
- return { publicKeyPem: body.publicKeyPem };
39975
- }
39976
- async function centralUpsertDevice(opts) {
39977
- const body = await centralRequest(opts, "/api/clawd-identity/devices/self", {
39978
- method: "PUT",
39979
- bearer: opts.ttcJwt,
39980
- body: {
39981
- deviceId: opts.deviceId,
39982
- provider: opts.provider,
39983
- ...opts.url ? { url: opts.url } : {},
39984
- ...opts.name ? { name: opts.name } : {}
39985
- }
39986
- });
39987
- if (body.ok !== true) {
39988
- throw new CentralClientError("BAD_RESPONSE", "upsertDevice: server did not ack ok");
39989
- }
39990
- return { ok: true };
39991
- }
39992
- async function centralListDevices(opts) {
39993
- const body = await centralRequest(
39994
- opts,
39995
- `/api/clawd-identity/devices?provider=${encodeURIComponent(opts.provider)}`,
39996
- { method: "GET", bearer: opts.ttcJwt }
39997
- );
39998
- if (!Array.isArray(body.devices)) {
39999
- throw new CentralClientError("BAD_RESPONSE", "devices: missing devices array");
40000
- }
40001
- const out = [];
40002
- for (const raw of body.devices) {
40003
- const d = raw;
40004
- if (typeof d.deviceId !== "string" || typeof d.name !== "string" || typeof d.url !== "string") {
40005
- throw new CentralClientError("BAD_RESPONSE", "devices: malformed device entry");
40006
- }
40007
- out.push({ deviceId: d.deviceId, name: d.name, url: d.url });
40008
- }
40009
- return out;
40010
- }
40011
-
40012
- // src/feishu-auth/verify-token.ts
40013
- var crypto13 = __toESM(require("crypto"), 1);
40014
- var CONNECT_TOKEN_PREFIX = "clawdtk1";
40015
- function verifyConnectToken(args) {
40016
- const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
40017
- const parts = args.token.split(".");
40018
- if (parts.length !== 3 || parts[0] !== CONNECT_TOKEN_PREFIX) {
40019
- return { ok: false, reason: "BAD_FORMAT" };
40020
- }
40021
- const [prefix, payloadB64, sigB64] = parts;
40022
- const data = `${prefix}.${payloadB64}`;
40023
- let signatureValid = false;
40024
- try {
40025
- signatureValid = crypto13.verify(
40026
- null,
40027
- Buffer.from(data, "utf8"),
40028
- args.publicKeyPem,
40029
- Buffer.from(sigB64, "base64url")
40030
- );
40031
- } catch {
40032
- return { ok: false, reason: "BAD_SIGNATURE" };
40033
- }
40034
- if (!signatureValid) {
40035
- return { ok: false, reason: "BAD_SIGNATURE" };
40036
- }
40037
- let payload;
40038
- try {
40039
- payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
40040
- } catch {
40041
- return { ok: false, reason: "BAD_FORMAT" };
40042
- }
40043
- if (typeof payload.aud !== "string" || typeof payload.subDevice !== "string" || payload.subDevice.length === 0 || typeof payload.subOwner !== "string" || payload.subOwner.length === 0 || typeof payload.provider !== "string" || payload.provider.length === 0 || typeof payload.name !== "string" || typeof payload.exp !== "number") {
40044
- return { ok: false, reason: "BAD_FORMAT" };
40045
- }
40046
- if (payload.aud !== args.expectedDeviceId) {
40047
- return { ok: false, reason: "WRONG_AUDIENCE" };
40048
- }
40049
- if (payload.exp <= now) {
40050
- return { ok: false, reason: "EXPIRED" };
40051
- }
40052
- return {
40053
- ok: true,
40054
- subDevice: payload.subDevice,
40055
- subOwner: payload.subOwner,
40056
- provider: payload.provider,
40057
- displayName: payload.name
40058
- };
40059
- }
40060
-
40061
- // src/feishu-auth/server-key.ts
40062
- var fs35 = __toESM(require("fs"), 1);
40063
- var path39 = __toESM(require("path"), 1);
40064
- var FILE_NAME2 = "server-signing-key.json";
40065
- var ServerKeyStore = class {
40066
- constructor(dataDir) {
40067
- this.dataDir = dataDir;
40068
- }
40069
- dataDir;
40070
- filePath() {
40071
- return path39.join(this.dataDir, FILE_NAME2);
40072
- }
40073
- /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
40074
- read() {
40075
- try {
40076
- const raw = fs35.readFileSync(this.filePath(), "utf8");
40077
- const parsed = JSON.parse(raw);
40078
- if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
40079
- return parsed.publicKeyPem;
40080
- }
40081
- return null;
40082
- } catch {
40083
- return null;
40084
- }
40085
- }
40086
- write(publicKeyPem) {
40087
- const content = {
40088
- publicKeyPem,
40089
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
40090
- };
40091
- fs35.mkdirSync(this.dataDir, { recursive: true });
40092
- fs35.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
40093
- }
40094
- clear() {
40095
- try {
40096
- fs35.unlinkSync(this.filePath());
40097
- } catch {
40098
- }
40099
- }
40100
- };
40101
-
40102
39675
  // src/index.ts
40103
39676
  init_protocol();
40104
39677
 
@@ -40229,15 +39802,23 @@ function buildSessionHandlers(deps) {
40229
39802
  }
40230
39803
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
40231
39804
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
40232
- const { response, broadcast } = manager.create(args, creatorPrincipalId);
39805
+ const creatorOwnerPrincipalId = ctx?.principal.kind === "guest" ? ctx.peerOwnerPrincipalId : ownerPrincipalId;
39806
+ const { response, broadcast } = manager.create(
39807
+ args,
39808
+ creatorPrincipalId,
39809
+ creatorOwnerPrincipalId
39810
+ );
40233
39811
  return { response: { type: "session:info", ...response }, broadcast };
40234
39812
  };
40235
39813
  const list = async (_frame, _client, ctx) => {
40236
39814
  const { response } = manager.list();
40237
39815
  if (ctx && ctx.principal.kind === "guest") {
40238
39816
  const sessions = response.sessions ?? [];
40239
- const guestDeviceId = ctx.principal.id;
40240
- const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
39817
+ const peerOwnerId = ctx.peerOwnerPrincipalId;
39818
+ const capId = ctx.capabilityId;
39819
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39820
+ (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39821
+ );
40241
39822
  return { response: { type: "session:list", sessions: filtered } };
40242
39823
  }
40243
39824
  return { response: { type: "session:list", ...response } };
@@ -40525,7 +40106,7 @@ function buildPermissionHandlers(deps) {
40525
40106
  }
40526
40107
 
40527
40108
  // src/handlers/history.ts
40528
- var path42 = __toESM(require("path"), 1);
40109
+ var path41 = __toESM(require("path"), 1);
40529
40110
  init_protocol();
40530
40111
 
40531
40112
  // src/session/recent-dirs.ts
@@ -40543,7 +40124,7 @@ function listRecentDirs(store, limit = 50) {
40543
40124
  }
40544
40125
 
40545
40126
  // src/permission/persona-paths.ts
40546
- var path41 = __toESM(require("path"), 1);
40127
+ var path40 = __toESM(require("path"), 1);
40547
40128
  function getAllowedPersonaIds(grants, action) {
40548
40129
  const ids = /* @__PURE__ */ new Set();
40549
40130
  for (const g2 of grants) {
@@ -40556,26 +40137,26 @@ function getAllowedPersonaIds(grants, action) {
40556
40137
  return ids;
40557
40138
  }
40558
40139
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40559
- const root = path41.resolve(personaRoot);
40560
- const target = path41.resolve(absPath);
40561
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40140
+ const root = path40.resolve(personaRoot);
40141
+ const target = path40.resolve(absPath);
40142
+ const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40562
40143
  if (!target.startsWith(root + sep3)) return false;
40563
- const rel = path41.relative(root, target);
40144
+ const rel = path40.relative(root, target);
40564
40145
  if (!rel || rel.startsWith("..")) return false;
40565
- const personaId = rel.split(path41.sep)[0];
40146
+ const personaId = rel.split(path40.sep)[0];
40566
40147
  if (!personaId) return false;
40567
40148
  const allowed = getAllowedPersonaIds(grants, action);
40568
40149
  if (allowed === "*") return true;
40569
40150
  return allowed.has(personaId);
40570
40151
  }
40571
40152
  function personaIdFromPath(absPath, personaRoot) {
40572
- const root = path41.resolve(personaRoot);
40573
- const target = path41.resolve(absPath);
40574
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40153
+ const root = path40.resolve(personaRoot);
40154
+ const target = path40.resolve(absPath);
40155
+ const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40575
40156
  if (!target.startsWith(root + sep3)) return null;
40576
- const rel = path41.relative(root, target);
40157
+ const rel = path40.relative(root, target);
40577
40158
  if (!rel || rel.startsWith("..")) return null;
40578
- const id = rel.split(path41.sep)[0];
40159
+ const id = rel.split(path40.sep)[0];
40579
40160
  return id || null;
40580
40161
  }
40581
40162
 
@@ -40600,7 +40181,7 @@ function buildHistoryHandlers(deps) {
40600
40181
  if (!pid) return false;
40601
40182
  return isGuestPathAllowed(
40602
40183
  ctx.grants,
40603
- path42.join(personaRoot, pid),
40184
+ path41.join(personaRoot, pid),
40604
40185
  personaRoot,
40605
40186
  "read"
40606
40187
  );
@@ -40611,7 +40192,7 @@ function buildHistoryHandlers(deps) {
40611
40192
  };
40612
40193
  const list = async (frame, _client, ctx) => {
40613
40194
  const args = HistoryListArgs.parse(frame);
40614
- assertGuestPath(ctx, path42.resolve(args.projectPath), personaRoot, "history:list");
40195
+ assertGuestPath(ctx, path41.resolve(args.projectPath), personaRoot, "history:list");
40615
40196
  const sessions = await history.listSessions(args);
40616
40197
  return { response: { type: "history:list", sessions } };
40617
40198
  };
@@ -40620,7 +40201,7 @@ function buildHistoryHandlers(deps) {
40620
40201
  const { response: file } = manager.get({ sessionId: args.sessionId });
40621
40202
  const f = file;
40622
40203
  if (ctx && ctx.principal.kind === "guest") {
40623
- const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.principal.id;
40204
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
40624
40205
  if (!ok) {
40625
40206
  throw new ClawdError(
40626
40207
  ERROR_CODES.UNAUTHORIZED,
@@ -40643,13 +40224,13 @@ function buildHistoryHandlers(deps) {
40643
40224
  };
40644
40225
  const subagents = async (frame, _client, ctx) => {
40645
40226
  const args = HistorySubagentsArgs.parse(frame);
40646
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40227
+ assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagents");
40647
40228
  const subs = await history.listSubagents(args);
40648
40229
  return { response: { type: "history:subagents", subagents: subs } };
40649
40230
  };
40650
40231
  const subagentRead = async (frame, _client, ctx) => {
40651
40232
  const args = HistorySubagentReadArgs.parse(frame);
40652
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40233
+ assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagent-read");
40653
40234
  const res = await history.readSubagent(args);
40654
40235
  return { response: { type: "history:subagent-read", ...res } };
40655
40236
  };
@@ -40657,7 +40238,7 @@ function buildHistoryHandlers(deps) {
40657
40238
  const dirs = listRecentDirs(store);
40658
40239
  if (ctx?.principal.kind === "guest" && personaRoot) {
40659
40240
  const filtered = dirs.filter(
40660
- (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40241
+ (d) => isGuestPathAllowed(ctx.grants, path41.resolve(d.cwd), personaRoot, "read")
40661
40242
  );
40662
40243
  return { response: { type: "history:recentDirs", dirs: filtered } };
40663
40244
  }
@@ -40674,7 +40255,7 @@ function buildHistoryHandlers(deps) {
40674
40255
  }
40675
40256
 
40676
40257
  // src/handlers/workspace.ts
40677
- var path43 = __toESM(require("path"), 1);
40258
+ var path42 = __toESM(require("path"), 1);
40678
40259
  var os15 = __toESM(require("os"), 1);
40679
40260
  init_protocol();
40680
40261
  init_protocol();
@@ -40715,22 +40296,22 @@ function buildWorkspaceHandlers(deps) {
40715
40296
  const args = WorkspaceListArgs.parse(frame);
40716
40297
  const isGuest = ctx?.principal.kind === "guest";
40717
40298
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40718
- const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40719
- const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40299
+ const resolvedCwd = path42.resolve(args.cwd ?? fallbackCwd);
40300
+ const target = args.path ? path42.resolve(resolvedCwd, args.path) : resolvedCwd;
40720
40301
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40721
40302
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40722
40303
  return { response: { type: "workspace:list", ...res } };
40723
40304
  };
40724
40305
  const read = async (frame, _client, ctx) => {
40725
40306
  const args = WorkspaceReadArgs.parse(frame);
40726
- const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40307
+ const target = path42.isAbsolute(args.path) ? path42.resolve(args.path) : path42.resolve(args.cwd, args.path);
40727
40308
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40728
40309
  const res = workspace.read(args);
40729
40310
  return { response: { type: "workspace:read", ...res } };
40730
40311
  };
40731
40312
  const skillsList = async (frame, _client, ctx) => {
40732
40313
  const args = SkillsListArgs.parse(frame);
40733
- const cwdAbs = path43.resolve(args.cwd);
40314
+ const cwdAbs = path42.resolve(args.cwd);
40734
40315
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40735
40316
  const list2 = skills.list(args);
40736
40317
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40742,7 +40323,7 @@ function buildWorkspaceHandlers(deps) {
40742
40323
  };
40743
40324
  const agentsList = async (frame, _client, ctx) => {
40744
40325
  const args = AgentsListArgs.parse(frame);
40745
- const cwdAbs = path43.resolve(args.cwd);
40326
+ const cwdAbs = path42.resolve(args.cwd);
40746
40327
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40747
40328
  const list2 = agents.list(args);
40748
40329
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40761,7 +40342,7 @@ function buildWorkspaceHandlers(deps) {
40761
40342
  }
40762
40343
 
40763
40344
  // src/handlers/git.ts
40764
- var path45 = __toESM(require("path"), 1);
40345
+ var path44 = __toESM(require("path"), 1);
40765
40346
  init_protocol();
40766
40347
  init_protocol();
40767
40348
 
@@ -40847,7 +40428,7 @@ async function listGitBranches(cwd) {
40847
40428
  // src/handlers/git.ts
40848
40429
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
40849
40430
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40850
- if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40431
+ if (!isGuestPathAllowed(ctx.grants, path44.resolve(cwd), personaRoot, "read")) {
40851
40432
  throw new ClawdError(
40852
40433
  ERROR_CODES.UNAUTHORIZED,
40853
40434
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40902,7 +40483,7 @@ var DeleteArgsSchema = external_exports.object({
40902
40483
  capabilityId: external_exports.string().min(1)
40903
40484
  }).strict();
40904
40485
  function buildCapabilityHandlers(deps) {
40905
- const { manager } = deps;
40486
+ const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40906
40487
  const list = async () => {
40907
40488
  return {
40908
40489
  response: {
@@ -40928,45 +40509,70 @@ function buildCapabilityHandlers(deps) {
40928
40509
  }
40929
40510
  };
40930
40511
  };
40512
+ const personalCapGet = async () => {
40513
+ const { token, capability } = getPersonalCapability();
40514
+ return {
40515
+ response: {
40516
+ type: "personal-cap:get:ok",
40517
+ token,
40518
+ capability: stripSecretHash(capability),
40519
+ shareBaseUrl: getShareBaseUrl()
40520
+ }
40521
+ };
40522
+ };
40931
40523
  return {
40932
40524
  "capability:list": list,
40933
- "capability:delete": del
40525
+ "capability:delete": del,
40526
+ "personal-cap:get": personalCapGet
40934
40527
  };
40935
40528
  }
40936
40529
 
40937
40530
  // src/handlers/inbox.ts
40938
40531
  init_protocol();
40939
- function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
40532
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40940
40533
  if (ctx.principal.kind === "owner") {
40941
- if (!argsPeerDeviceId) {
40534
+ if (!argsPeerOwnerId) {
40942
40535
  throw new ClawdError(
40943
40536
  ERROR_CODES.VALIDATION_ERROR,
40944
- "inbox: owner ctx requires peerDeviceId"
40537
+ "inbox: owner ctx requires peerOwnerId"
40945
40538
  );
40946
40539
  }
40947
- return argsPeerDeviceId;
40540
+ return argsPeerOwnerId;
40541
+ }
40542
+ if (!ctx.capabilityId) {
40543
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40544
+ }
40545
+ let resolved = ctx.peerOwnerPrincipalId;
40546
+ if (!resolved) {
40547
+ const cap = capManager.findById(ctx.capabilityId);
40548
+ resolved = cap?.peerOwnerId;
40549
+ }
40550
+ if (!resolved) {
40551
+ throw new ClawdError(
40552
+ ERROR_CODES.UNAUTHORIZED,
40553
+ `inbox: guest cap ${ctx.capabilityId} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40554
+ );
40948
40555
  }
40949
- const resolved = ctx.principal.id;
40950
- if (argsPeerDeviceId && argsPeerDeviceId !== resolved) {
40556
+ if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
40951
40557
  throw new ClawdError(
40952
40558
  ERROR_CODES.UNAUTHORIZED,
40953
- `inbox: guest args.peerDeviceId mismatch (resolved=${resolved}, args=${argsPeerDeviceId})`
40559
+ `inbox: guest args.peerOwnerId mismatch (resolved=${resolved}, args=${argsPeerOwnerId})`
40954
40560
  );
40955
40561
  }
40956
40562
  return resolved;
40957
40563
  }
40958
40564
  function buildInboxHandlers(deps) {
40959
- const { manager } = deps;
40565
+ const { manager, capManager } = deps;
40960
40566
  const postMessage = async (frame, _client, ctx) => {
40961
40567
  const { type: _t, requestId: _r, ...rest } = frame;
40962
40568
  const args = InboxPostMessageArgsSchema.parse(rest);
40963
40569
  if (!ctx) {
40964
40570
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40965
40571
  }
40966
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40572
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40967
40573
  const message = manager.postMessage({
40968
- peerDeviceId,
40969
- senderDeviceId: ctx.principal.id,
40574
+ peerOwnerId,
40575
+ senderPrincipalId: ctx.principal.id,
40970
40576
  text: args.text,
40971
40577
  id: args.id,
40972
40578
  createdAt: args.createdAt
@@ -40981,10 +40587,10 @@ function buildInboxHandlers(deps) {
40981
40587
  if (!ctx) {
40982
40588
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40983
40589
  }
40984
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40985
- const messages = manager.list(peerDeviceId, args.sinceCreatedAt);
40590
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40591
+ const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40986
40592
  return {
40987
- response: { type: "inbox:list:ok", peerDeviceId, messages }
40593
+ response: { type: "inbox:list:ok", peerOwnerId, messages }
40988
40594
  };
40989
40595
  };
40990
40596
  const markRead = async (frame, _client, ctx) => {
@@ -40993,16 +40599,16 @@ function buildInboxHandlers(deps) {
40993
40599
  if (!ctx) {
40994
40600
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40995
40601
  }
40996
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40602
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40997
40603
  const updated = manager.markRead({
40998
- peerDeviceId,
40999
- readerDeviceId: ctx.principal.id,
40604
+ peerOwnerId,
40605
+ principalId: ctx.principal.id,
41000
40606
  upToCreatedAt: args.upToCreatedAt
41001
40607
  });
41002
40608
  return {
41003
40609
  response: {
41004
40610
  type: "inbox:markRead:ok",
41005
- peerDeviceId,
40611
+ peerOwnerId,
41006
40612
  upToCreatedAt: args.upToCreatedAt,
41007
40613
  updated
41008
40614
  }
@@ -41015,45 +40621,116 @@ function buildInboxHandlers(deps) {
41015
40621
  };
41016
40622
  }
41017
40623
 
41018
- // src/handlers/contact.ts
40624
+ // src/handlers/received-capability.ts
41019
40625
  init_protocol();
41020
40626
  function ensureOwner(ctx) {
41021
40627
  if (!ctx || ctx.principal.kind !== "owner") {
41022
40628
  throw new ClawdError(
41023
40629
  ERROR_CODES.UNAUTHORIZED,
41024
- "UNAUTHORIZED: contact:* requires owner ctx"
40630
+ "UNAUTHORIZED: received-capability:* requires owner ctx"
40631
+ );
40632
+ }
40633
+ }
40634
+ function ensureGuestCtx(ctx) {
40635
+ if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40636
+ throw new ClawdError(
40637
+ ERROR_CODES.UNAUTHORIZED,
40638
+ "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
41025
40639
  );
41026
40640
  }
41027
40641
  }
41028
- function buildContactHandlers(deps) {
40642
+ function buildReceivedCapabilityHandlers(deps) {
40643
+ const now = deps.now ?? Date.now;
41029
40644
  const list = async (_frame, _client, ctx) => {
41030
40645
  ensureOwner(ctx);
41031
40646
  return {
41032
40647
  response: {
41033
- type: "contact:list:ok",
41034
- contacts: deps.store.list()
40648
+ type: "received-capability:list:ok",
40649
+ receivedCaps: deps.store.list()
41035
40650
  }
41036
40651
  };
41037
40652
  };
40653
+ const add = async (frame, _client, ctx) => {
40654
+ ensureOwner(ctx);
40655
+ const { type: _t, requestId: _r, ...rest } = frame;
40656
+ const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40657
+ let conn;
40658
+ try {
40659
+ conn = await deps.connectRemote({
40660
+ url: args.remoteUrl,
40661
+ token: args.token,
40662
+ selfPrincipalId: deps.selfPrincipalId(),
40663
+ selfDisplayName: deps.selfDisplayName()
40664
+ });
40665
+ } catch (e) {
40666
+ throw new ClawdError(
40667
+ ERROR_CODES.VALIDATION_ERROR,
40668
+ `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40669
+ );
40670
+ }
40671
+ try {
40672
+ const wh = await conn.whoami();
40673
+ const rc = {
40674
+ ownerPrincipalId: wh.ownerId,
40675
+ ownerDisplayName: wh.displayName,
40676
+ remoteUrl: args.remoteUrl,
40677
+ capabilityId: wh.capabilityId,
40678
+ capabilityToken: args.token,
40679
+ grants: wh.grants,
40680
+ receivedAt: now()
40681
+ };
40682
+ deps.store.upsert(rc);
40683
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40684
+ return {
40685
+ response: { type: "received-capability:add:ok", receivedCap: rc }
40686
+ };
40687
+ } finally {
40688
+ try {
40689
+ conn.close();
40690
+ } catch {
40691
+ }
40692
+ }
40693
+ };
41038
40694
  const remove = async (frame, _client, ctx) => {
41039
40695
  ensureOwner(ctx);
41040
40696
  const { type: _t, requestId: _r, ...rest } = frame;
41041
- const args = ContactRemoveArgsSchema.parse(rest);
41042
- deps.store.removeByDeviceId(args.deviceId);
40697
+ const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40698
+ deps.store.remove(args.ownerPrincipalId);
41043
40699
  deps.broadcast({
41044
- type: "contact:removed",
41045
- deviceId: args.deviceId
40700
+ type: "received-capability:removed",
40701
+ ownerPrincipalId: args.ownerPrincipalId
41046
40702
  });
41047
40703
  return {
41048
40704
  response: {
41049
- type: "contact:remove:ok",
41050
- deviceId: args.deviceId
40705
+ type: "received-capability:remove:ok",
40706
+ ownerPrincipalId: args.ownerPrincipalId
41051
40707
  }
41052
40708
  };
41053
40709
  };
40710
+ const autoAttach = async (frame, _client, ctx) => {
40711
+ ensureGuestCtx(ctx);
40712
+ const { type: _t, requestId: _r, ...rest } = frame;
40713
+ const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40714
+ const rc = {
40715
+ ownerPrincipalId: args.ownerPrincipalId,
40716
+ ownerDisplayName: args.ownerDisplayName,
40717
+ remoteUrl: args.remoteUrl,
40718
+ capabilityId: args.capabilityId,
40719
+ capabilityToken: args.token,
40720
+ grants: args.grants,
40721
+ receivedAt: now()
40722
+ };
40723
+ deps.store.upsert(rc);
40724
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40725
+ return {
40726
+ response: { type: "received-capability:autoAttach:ok" }
40727
+ };
40728
+ };
41054
40729
  return {
41055
- "contact:list": list,
41056
- "contact:remove": remove
40730
+ "received-capability:list": list,
40731
+ "received-capability:add": add,
40732
+ "received-capability:remove": remove,
40733
+ "received-capability:autoAttach": autoAttach
41057
40734
  };
41058
40735
  }
41059
40736
 
@@ -41064,11 +40741,7 @@ function buildWhoamiHandler(deps) {
41064
40741
  if (!ctx) {
41065
40742
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
41066
40743
  }
41067
- const owner = {
41068
- ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
41069
- ownerId: deps.ownerId,
41070
- provider: deps.ownerProvider
41071
- };
40744
+ const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
41072
40745
  let capability;
41073
40746
  if (ctx.principal.kind === "owner") {
41074
40747
  capability = {
@@ -41079,13 +40752,14 @@ function buildWhoamiHandler(deps) {
41079
40752
  usedCount: 0
41080
40753
  };
41081
40754
  } else {
41082
- capability = {
41083
- id: ctx.principal.id,
41084
- displayName: ctx.principal.displayName ?? "guest",
41085
- grants: ctx.grants,
41086
- issuedAt: 0,
41087
- usedCount: 0
41088
- };
40755
+ if (!ctx.capabilityId) {
40756
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40757
+ }
40758
+ const cap = deps.capabilityManager.findById(ctx.capabilityId);
40759
+ if (!cap) {
40760
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40761
+ }
40762
+ capability = stripSecretHash(cap);
41089
40763
  }
41090
40764
  const grantedPersonas = [];
41091
40765
  const isGuest = ctx.principal.kind === "guest";
@@ -41116,123 +40790,6 @@ function buildWhoamiHandler(deps) {
41116
40790
  };
41117
40791
  }
41118
40792
 
41119
- // src/handlers/feishu-auth.ts
41120
- function buildFeishuAuthHandlers(deps) {
41121
- const loginStart = async () => {
41122
- const { authUrl, state } = deps.loginFlow.start();
41123
- return {
41124
- response: { type: "auth:login:start:ok", authUrl, state }
41125
- };
41126
- };
41127
- const getIdentity = async () => {
41128
- const record = deps.ownerIdentityStore.read();
41129
- return {
41130
- response: {
41131
- type: "auth:getIdentity:ok",
41132
- identity: record ? record.identity : null,
41133
- // 决策 #15:设备属性,与登录态无关——UI"分享我的 Hub 标识"用
41134
- deviceId: deps.deviceId
41135
- }
41136
- };
41137
- };
41138
- const logout = async () => {
41139
- deps.ownerIdentityStore.clear();
41140
- return {
41141
- response: { type: "auth:logout:ok" }
41142
- };
41143
- };
41144
- return {
41145
- "auth:login:start": loginStart,
41146
- "auth:getIdentity": getIdentity,
41147
- "auth:logout": logout
41148
- };
41149
- }
41150
-
41151
- // src/handlers/device.ts
41152
- init_protocol();
41153
- function ensureOwner2(ctx) {
41154
- if (!ctx || ctx.principal.kind !== "owner") {
41155
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
41156
- }
41157
- }
41158
- function buildDeviceHandlers(deps) {
41159
- const now = deps.now ?? Date.now;
41160
- const list = async (_frame, _client, ctx) => {
41161
- ensureOwner2(ctx);
41162
- const devices = await deps.listDevices();
41163
- return {
41164
- response: { type: "device:list:ok", devices }
41165
- };
41166
- };
41167
- const connect = async (frame, _client, ctx) => {
41168
- ensureOwner2(ctx);
41169
- const { type: _t, requestId: _r, ...rest } = frame;
41170
- const args = DeviceConnectArgsSchema.parse(rest);
41171
- const exchanged = await deps.exchange(args.deviceId);
41172
- const url = args.url ?? exchanged.deviceUrl;
41173
- if (!url) {
41174
- throw new ClawdError(
41175
- ERROR_CODES.VALIDATION_ERROR,
41176
- "DEVICE_URL_UNKNOWN: \u5BF9\u65B9\u8BBE\u5907\u672A\u4E0A\u62A5\u5730\u5740\uFF08\u672A\u767B\u5F55\u6216\u672A\u5F00 tunnel\uFF09\uFF0C\u8BF7\u663E\u5F0F\u63D0\u4F9B url \u6216\u7B49\u5BF9\u65B9\u4E0A\u7EBF"
41177
- );
41178
- }
41179
- let conn;
41180
- try {
41181
- conn = await deps.connectRemote({
41182
- url,
41183
- token: exchanged.token,
41184
- // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
41185
- // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
41186
- // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
41187
- selfUrl: deps.selfUrl() ?? void 0
41188
- });
41189
- } catch (e) {
41190
- throw new ClawdError(
41191
- ERROR_CODES.VALIDATION_ERROR,
41192
- `DEVICE_UNREACHABLE: ${e.message}`
41193
- );
41194
- }
41195
- try {
41196
- const wh = await conn.whoami();
41197
- const contact = {
41198
- deviceId: args.deviceId,
41199
- ownerId: wh.ownerId,
41200
- provider: wh.provider,
41201
- // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
41202
- // 如"公共1");"输入标识"连接不带 name → 回落 whoami 报的对方 owner 真名。
41203
- // 归属人身份不丢(ownerId/provider 仍来自 whoami 背书)。
41204
- displayName: args.name || wh.displayName || args.deviceId,
41205
- remoteUrl: url,
41206
- // connectToken 存自包含签名 token(断线重连复用,30 天有效;
41207
- // 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
41208
- connectToken: exchanged.token,
41209
- grants: wh.grants,
41210
- addedAt: now()
41211
- };
41212
- deps.store.upsert(contact);
41213
- deps.broadcast({ type: "contact:added", contact });
41214
- return {
41215
- response: {
41216
- type: "device:connect:ok",
41217
- // 返回连接的设备标识(= 请求的 args.deviceId)
41218
- deviceId: args.deviceId,
41219
- name: contact.displayName,
41220
- url
41221
- }
41222
- };
41223
- } finally {
41224
- try {
41225
- conn.close();
41226
- } catch {
41227
- }
41228
- }
41229
- };
41230
- return {
41231
- "device:list": list,
41232
- "device:connect": connect
41233
- };
41234
- }
41235
-
41236
40793
  // src/handlers/meta.ts
41237
40794
  var import_node_os15 = __toESM(require("os"), 1);
41238
40795
  init_protocol();
@@ -43108,25 +42665,27 @@ function buildMethodHandlers(deps) {
43108
42665
  personaRegistry: deps.personaRegistry
43109
42666
  }),
43110
42667
  ...buildCapabilityHandlers({
43111
- manager: deps.capabilityManager
42668
+ manager: deps.capabilityManager,
42669
+ getShareBaseUrl: deps.getShareBaseUrl,
42670
+ getPersonalCapability: deps.getPersonalCapability
43112
42671
  }),
43113
42672
  ...buildInboxHandlers({
43114
- manager: deps.inboxManager
42673
+ manager: deps.inboxManager,
42674
+ capManager: deps.capabilityManager
43115
42675
  }),
43116
- ...buildContactHandlers({
43117
- store: deps.contactStore,
43118
- broadcast: deps.broadcastToOwners
42676
+ ...buildReceivedCapabilityHandlers({
42677
+ store: deps.receivedCapabilityStore,
42678
+ connectRemote: deps.connectRemote,
42679
+ broadcast: deps.broadcastToOwners,
42680
+ selfPrincipalId: () => deps.ownerPrincipalId,
42681
+ selfDisplayName: () => deps.ownerDisplayName
43119
42682
  }),
43120
42683
  whoami: buildWhoamiHandler({
43121
42684
  ownerDisplayName: deps.ownerDisplayName,
43122
42685
  ownerPrincipalId: deps.ownerPrincipalId,
43123
- ownerId: deps.ownerId,
43124
- ownerProvider: deps.ownerProvider,
43125
42686
  personaStore: deps.personaStore,
43126
42687
  capabilityManager: deps.capabilityManager
43127
42688
  }),
43128
- ...buildFeishuAuthHandlers(deps.feishuAuth),
43129
- ...buildDeviceHandlers(deps.feishuDevice),
43130
42689
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
43131
42690
  ...buildExtensionHandlers({
43132
42691
  loadAll,
@@ -43628,22 +43187,23 @@ var METHOD_GRANT_MAP = {
43628
43187
  // "查 capabilityManager 拿 caller 的 guest capability"。
43629
43188
  "whoami": { kind: "public" },
43630
43189
  // ---- capability platform(admin-only) ----
43631
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
43632
- // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
43633
- // 撤销级联 + 清旧 per-peer cap。
43190
+ // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
43191
+ // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
43634
43192
  "capability:list": ADMIN_ANY,
43635
43193
  "capability:delete": ADMIN_ANY,
43636
- // ---- inbox: 双投 P2P IM ----
43637
- // 三条都 'public' 能连上 = 已通过本地验票. handler 内额外校验 guest 的
43638
- // args.peerDeviceId ctx.principal.id(=deviceId) 一致, guest 越权操作别的 DM 桶.
43194
+ "personal-cap:get": ADMIN_ANY,
43195
+ // ---- inbox: channel-based IM (capability platform v3) ----
43196
+ // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
43197
+ // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
43639
43198
  "inbox:list": { kind: "public" },
43640
43199
  "inbox:markRead": { kind: "public" },
43641
43200
  "inbox:postMessage": { kind: "public" },
43642
- // ---- contact:* (联系人列表) ----
43643
- // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
43644
- // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
43645
- "contact:list": ADMIN_ANY,
43646
- "contact:remove": ADMIN_ANY,
43201
+ // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
43202
+ // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
43203
+ "received-capability:list": ADMIN_ANY,
43204
+ "received-capability:add": ADMIN_ANY,
43205
+ "received-capability:remove": ADMIN_ANY,
43206
+ "received-capability:autoAttach": { kind: "public" },
43647
43207
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
43648
43208
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
43649
43209
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -43684,7 +43244,7 @@ var METHOD_GRANT_MAP = {
43684
43244
  "history:list": CAPABILITY_SCOPED,
43685
43245
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
43686
43246
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
43687
- // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
43247
+ // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
43688
43248
  "history:read": CAPABILITY_SCOPED,
43689
43249
  "history:subagents": CAPABILITY_SCOPED,
43690
43250
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -43741,15 +43301,6 @@ var METHOD_GRANT_MAP = {
43741
43301
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
43742
43302
  "extension.install-from-channel": ADMIN_ANY,
43743
43303
  "extension.update-from-channel": ADMIN_ANY,
43744
- // ---- auth:* 飞书统一身份 Phase 1(owner-only) ----
43745
- // 登录/登出/查身份都是 owner 本机操作,guest 不可调
43746
- "auth:login:start": ADMIN_ANY,
43747
- "auth:getIdentity": ADMIN_ANY,
43748
- "auth:logout": ADMIN_ANY,
43749
- // ---- device:* 设备目录 + 连接(决策 #15,owner-only) ----
43750
- // 设备目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
43751
- "device:list": ADMIN_ANY,
43752
- "device:connect": ADMIN_ANY,
43753
43304
  // ---- app-builder Project picker(spec 2026-06-01-app-builder-project-picker-design) ----
43754
43305
  // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
43755
43306
  // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
@@ -44160,23 +43711,19 @@ async function startDaemon(config) {
44160
43711
  } else if (config.tunnel) {
44161
43712
  resolvedAuthToken = authFile.token;
44162
43713
  }
44163
- const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
44164
- const feishuIdentity = ownerIdentityStore.read();
44165
- let feishuActive = feishuIdentity !== null;
44166
- const ownerPrincipalId = authFile.deviceId;
44167
- let ownerId = feishuIdentity?.identity.ownerId ?? "";
44168
- let ownerProvider = feishuIdentity?.identity.provider ?? "";
44169
- let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
43714
+ const ownerPrincipalId = authFile.ownerPrincipalId;
43715
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
44170
43716
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
44171
43717
  const inboxStore = new InboxStore(config.dataDir);
44172
- const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
43718
+ const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
44173
43719
  wsServer?.broadcastToOwners(frame);
44174
43720
  });
44175
- const contactStore = new ContactStore(config.dataDir);
44176
- contactStore.load();
44177
- const serverKeyStore = new ServerKeyStore(config.dataDir);
43721
+ const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
43722
+ receivedCapabilityStore.load();
44178
43723
  const capabilityStore = new CapabilityStore(config.dataDir);
44179
43724
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
43725
+ const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
43726
+ capabilityRegistry.upsertCapability(personalCapability.capability);
44180
43727
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
44181
43728
  onDeleted: (cap) => {
44182
43729
  const deletedAt = Date.now();
@@ -44185,7 +43732,7 @@ async function startDaemon(config) {
44185
43732
  capabilityId: cap.id,
44186
43733
  deletedAt
44187
43734
  });
44188
- wsServer?.closeConnectionsByGuestId(cap.id);
43735
+ wsServer?.closeConnectionsByCapability(cap.id);
44189
43736
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
44190
43737
  if (cleanup.removed.length > 0) {
44191
43738
  logger.info("capability delete cascade: guest sessions removed", {
@@ -44202,60 +43749,15 @@ async function startDaemon(config) {
44202
43749
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
44203
43750
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
44204
43751
  expectedToken: resolvedAuthToken,
44205
- authenticate: async (t, selfUrl) => {
44206
- const result = await authenticate(t, {
43752
+ authenticate: (t, selfPrincipalId, selfDisplayName) => {
43753
+ return authenticate(t, {
44207
43754
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
44208
43755
  ownerPrincipalId,
44209
43756
  ownerDisplayName,
44210
- // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入验签函数
44211
- // → 一律 BAD_TOKEN)。已激活时用缓存的 server 公钥本地验签;
44212
- // 公钥缓存缺失(登录后拉取失败)→ fail-closed 拒绝 guest。
44213
- ...feishuActive ? {
44214
- verifyConnectToken: (token) => {
44215
- const publicKeyPem = serverKeyStore.read();
44216
- if (!publicKeyPem) return { ok: false, reason: "BAD_SIGNATURE" };
44217
- return verifyConnectToken({
44218
- token,
44219
- publicKeyPem,
44220
- expectedDeviceId: authFile.deviceId
44221
- });
44222
- }
44223
- } : {}
43757
+ capabilityRegistry,
43758
+ selfPrincipalId,
43759
+ selfDisplayName
44224
43760
  });
44225
- if (result.ok && result.context.principal.kind === "guest") {
44226
- void autoReverseContact({
44227
- store: contactStore,
44228
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
44229
- deviceId: result.context.principal.id,
44230
- ownerId: result.context.ownerId ?? "",
44231
- provider: result.context.provider ?? "",
44232
- displayName: result.context.principal.displayName ?? result.context.principal.id,
44233
- selfUrl,
44234
- // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
44235
- // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
44236
- exchangeToken: async (targetDeviceId) => {
44237
- const record = ownerIdentityStore.read();
44238
- if (!record) return null;
44239
- try {
44240
- const r = await centralExchange({
44241
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44242
- jwt: record.ttcToken,
44243
- provider: record.identity.provider,
44244
- deviceId: targetDeviceId,
44245
- selfDeviceId: authFile.deviceId
44246
- });
44247
- return r.token;
44248
- } catch (e) {
44249
- logger.warn("auto-reverse exchange token failed", {
44250
- targetDeviceId,
44251
- err: e.message
44252
- });
44253
- return null;
44254
- }
44255
- }
44256
- });
44257
- }
44258
- return result;
44259
43761
  },
44260
43762
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
44261
43763
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -44411,52 +43913,10 @@ async function startDaemon(config) {
44411
43913
  }
44412
43914
  return `http://${config.host}:${config.port}`;
44413
43915
  };
44414
- const reportDevice = async () => {
44415
- if (!feishuActive) return;
44416
- const record = ownerIdentityStore.read();
44417
- if (!record) return;
44418
- try {
44419
- await centralUpsertDevice({
44420
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44421
- ttcJwt: record.ttcToken,
44422
- provider: record.identity.provider,
44423
- deviceId: authFile.deviceId,
44424
- url: currentTunnelUrl ?? void 0,
44425
- name: ownerDisplayName
44426
- });
44427
- logger.info("device registered to central server", {
44428
- deviceId: authFile.deviceId,
44429
- url: currentTunnelUrl ?? "(none)"
44430
- });
44431
- } catch (err) {
44432
- logger.warn("device register failed (best-effort)", { err: err.message });
44433
- }
44434
- };
44435
- const fetchServerKey = async () => {
44436
- if (!feishuActive) return;
44437
- if (serverKeyStore.read()) return;
44438
- try {
44439
- const { publicKeyPem } = await centralGetPublicKey({
44440
- api: config.clawosApi ?? DEFAULT_CLAWOS_API
44441
- });
44442
- serverKeyStore.write(publicKeyPem);
44443
- logger.info("server signing key cached");
44444
- } catch (err) {
44445
- logger.warn("server signing key fetch failed (guest \u5165\u7AD9\u5C06\u88AB\u62D2\u7EDD)", {
44446
- err: err.message
44447
- });
44448
- }
44449
- };
44450
43916
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
44451
43917
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
44452
43918
  await publishedChannelStore.load();
44453
43919
  const bundleCache = new BundleCache(bundleCacheRoot());
44454
- const loginFlow = new LoginFlow({
44455
- store: ownerIdentityStore,
44456
- fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
44457
- ttcAuthBase: TTC_HOSTS.auth,
44458
- getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
44459
- });
44460
43920
  const appBuilderStore = new ProjectStore(
44461
43921
  import_node_path47.default.join(config.dataDir, "personas/persona-app-builder")
44462
43922
  );
@@ -44472,7 +43932,7 @@ async function startDaemon(config) {
44472
43932
  const effectivePreviewPorts = Array.from(
44473
43933
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
44474
43934
  );
44475
- const makeHandlers = () => buildMethodHandlers({
43935
+ const handlers = buildMethodHandlers({
44476
43936
  manager,
44477
43937
  workspace,
44478
43938
  skills,
@@ -44517,12 +43977,15 @@ async function startDaemon(config) {
44517
43977
  personaRoot: import_node_path47.default.join(config.dataDir, "personas"),
44518
43978
  // capability:list / delete handler 依赖
44519
43979
  capabilityManager,
43980
+ // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
43981
+ // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
43982
+ getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
43983
+ // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
43984
+ getPersonalCapability: () => personalCapability,
44520
43985
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
44521
43986
  ownerDisplayName,
44522
- // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
43987
+ // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
44523
43988
  ownerPrincipalId,
44524
- ownerId,
44525
- ownerProvider,
44526
43989
  personaStore,
44527
43990
  // capability handler 也用 (capability:issue 走 registry / capability:list)
44528
43991
  capabilityRegistry,
@@ -44530,56 +43993,18 @@ async function startDaemon(config) {
44530
43993
  // cascade 接 store (list 统计 deletedInboxEvents).
44531
43994
  inboxManager,
44532
43995
  inboxStore,
44533
- // 联系人列表 store(device:connect / 自动反向落同一 store
44534
- contactStore,
44535
- // contact:removed broadcast;复用 capability:tokenIssued 同款通路
43996
+ // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
43997
+ receivedCapabilityStore,
43998
+ // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
44536
43999
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
44000
+ // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
44001
+ connectRemote,
44537
44002
  // extension runtime (v1: local-mode only). Instance owned by daemon top
44538
44003
  // level so stopAll() runs in the shutdown hook below.
44539
44004
  extensionRuntime,
44540
44005
  // extension sharing v1 — owner-side published-channels store.
44541
44006
  publishedChannelStore,
44542
44007
  bundleCache,
44543
- // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
44544
- // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
44545
- feishuAuth: { loginFlow, ownerIdentityStore, deviceId: authFile.deviceId },
44546
- // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
44547
- // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
44548
- // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
44549
- // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
44550
- feishuDevice: {
44551
- store: contactStore,
44552
- exchange: async (deviceId) => {
44553
- const record = ownerIdentityStore.read();
44554
- if (!record) {
44555
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
44556
- }
44557
- return centralExchange({
44558
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44559
- jwt: record.ttcToken,
44560
- provider: record.identity.provider,
44561
- deviceId,
44562
- selfDeviceId: authFile.deviceId
44563
- });
44564
- },
44565
- listDevices: async () => {
44566
- const record = ownerIdentityStore.read();
44567
- if (!record) {
44568
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
44569
- }
44570
- return centralListDevices({
44571
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
44572
- ttcJwt: record.ttcToken,
44573
- provider: record.identity.provider
44574
- });
44575
- },
44576
- connectRemote,
44577
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
44578
- // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
44579
- // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
44580
- // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
44581
- selfUrl: () => currentTunnelUrl
44582
- },
44583
44008
  // app-builder
44584
44009
  appBuilderStore,
44585
44010
  devServerLifecycle: devServerSupervisor,
@@ -44609,7 +44034,6 @@ async function startDaemon(config) {
44609
44034
  },
44610
44035
  logger
44611
44036
  });
44612
- let handlers = makeHandlers();
44613
44037
  const authResolver = new AuthContextResolver({
44614
44038
  ownerToken: resolvedAuthToken
44615
44039
  });
@@ -44647,31 +44071,6 @@ async function startDaemon(config) {
44647
44071
  },
44648
44072
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
44649
44073
  extensionsRoot: () => extensionsRoot(),
44650
- // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
44651
- // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
44652
- // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
44653
- // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
44654
- // People 立即解锁,不需要重启 daemon
44655
- // 失败则广播 auth:login:failed。
44656
- feishuLogin: {
44657
- handleLoginCallback: async (params) => {
44658
- const result = await loginFlow.handleCallback(params);
44659
- if (result.ok) {
44660
- feishuActive = true;
44661
- ownerId = result.identity.ownerId;
44662
- ownerProvider = result.identity.provider;
44663
- ownerDisplayName = result.identity.displayName;
44664
- handlers = makeHandlers();
44665
- wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
44666
- setImmediate(() => wsServer?.closeAllClients());
44667
- void reportDevice();
44668
- void fetchServerKey();
44669
- } else {
44670
- wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
44671
- }
44672
- return result;
44673
- }
44674
- },
44675
44074
  // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
44676
44075
  previewPorts: effectivePreviewPorts
44677
44076
  });
@@ -44679,6 +44078,8 @@ async function startDaemon(config) {
44679
44078
  host: config.host,
44680
44079
  port: config.port,
44681
44080
  logger,
44081
+ // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
44082
+ ownerPrincipalId,
44682
44083
  readyFrameBuilder: (ctx) => buildReadyFrame(
44683
44084
  {
44684
44085
  manager,
@@ -44698,12 +44099,14 @@ async function startDaemon(config) {
44698
44099
  // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
44699
44100
  // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
44700
44101
  // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
44701
- tryVerifyCapabilityToken: (token) => {
44102
+ tryVerifyCapabilityToken: (token, selfPrincipalId, _selfDisplayName) => {
44702
44103
  const v2 = capabilityRegistry.verifyToken(token);
44703
44104
  if (!v2.ok) return null;
44704
44105
  return {
44705
44106
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
44706
- grants: v2.capability.grants
44107
+ grants: v2.capability.grants,
44108
+ capabilityId: v2.capability.id,
44109
+ ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
44707
44110
  };
44708
44111
  },
44709
44112
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
@@ -44754,12 +44157,6 @@ async function startDaemon(config) {
44754
44157
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
44755
44158
  const handler = handlers[type];
44756
44159
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
44757
- if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
44758
- throw new ClawdError(
44759
- ERROR_CODES.FEISHU_LOGIN_REQUIRED,
44760
- `${type} requires feishu login (People/remote identity)`
44761
- );
44762
- }
44763
44160
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
44764
44161
  const verdict = computeGrantForFrame(type, frame);
44765
44162
  if (verdict.kind === "check") {
@@ -44877,8 +44274,6 @@ ${bar}
44877
44274
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
44878
44275
  }
44879
44276
  }
44880
- void reportDevice();
44881
- void fetchServerKey();
44882
44277
  const tickAttachmentGc = () => {
44883
44278
  try {
44884
44279
  runAttachmentGc({