@clawos-dev/clawd 0.2.117 → 0.2.119-beta.236.8805685

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.cjs +860 -1469
  2. package/package.json +1 -1
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
@@ -270,10 +265,6 @@ var init_errors = __esm({
270
265
  UNAUTHORIZED: "UNAUTHORIZED",
271
266
  FORBIDDEN: "FORBIDDEN",
272
267
  INVALID_PARAM: "INVALID_PARAM",
273
- // 飞书统一身份(spec 2026-06-01 决策 #9):People/远程体系强制飞书登录。
274
- // daemon 进程身份未激活为飞书 unionId 时,FEISHU_GATED_METHODS 内的 RPC 一律返回此错误;
275
- // UI 据此显示登录引导(区别于 UNAUTHORIZED 的越权语义)。
276
- FEISHU_LOGIN_REQUIRED: "FEISHU_LOGIN_REQUIRED",
277
268
  // app-builder 单栏默认 refactor (spec 2026-06-02-app-builder-single-pane-default-design):
278
269
  // appBuilder:createProject 加 personaId + session 未绑校验后抛的两个错码。
279
270
  WRONG_PERSONA: "WRONG_PERSONA",
@@ -707,8 +698,8 @@ var init_parseUtil = __esm({
707
698
  init_errors2();
708
699
  init_en();
709
700
  makeIssue = (params) => {
710
- const { data, path: path60, errorMaps, issueData } = params;
711
- const fullPath = [...path60, ...issueData.path || []];
701
+ const { data, path: path59, errorMaps, issueData } = params;
702
+ const fullPath = [...path59, ...issueData.path || []];
712
703
  const fullIssue = {
713
704
  ...issueData,
714
705
  path: fullPath
@@ -1019,11 +1010,11 @@ var init_types = __esm({
1019
1010
  init_parseUtil();
1020
1011
  init_util();
1021
1012
  ParseInputLazyPath = class {
1022
- constructor(parent, value, path60, key) {
1013
+ constructor(parent, value, path59, key) {
1023
1014
  this._cachedPath = [];
1024
1015
  this.parent = parent;
1025
1016
  this.data = value;
1026
- this._path = path60;
1017
+ this._path = path59;
1027
1018
  this._key = key;
1028
1019
  }
1029
1020
  get path() {
@@ -4664,41 +4655,53 @@ var init_schemas = __esm({
4664
4655
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4665
4656
  chatId: external_exports.string().min(1).optional(),
4666
4657
  /**
4667
- * 创建该 session 的设备 id(决策 #16:从 ctx.principal.id 派生 = deviceId)。
4668
- * UI self 判定收敛为单一比较 `!creatorPrincipalId || creatorPrincipalId === 本机 deviceId`
4669
- * 「我自己创建」vs「别的设备接入创建」。MyPersonas 二级分组按 creatorPrincipalId(设备)
4670
- * 分桶、按 contact(deviceId→ownerName) 反查显示名。
4658
+ * 创建该 session principal id(owner uuid 或 guest cap.id),从 session:create handler
4659
+ * ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4660
+ * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4661
+ * 后者再通过 PersonAlias 表反查 personId 二级分组。
4671
4662
  *
4672
- * 命名保留 creatorPrincipalId(命名债,语义已是 creatorDeviceId);值由 connect token
4673
- * subDevice 背书(guest ctx)或本机 deviceId(owner ctx)。optional 兼容老 session。
4674
- *
4675
- * 决策 #16 删除了 creatorOwnerPrincipalId(personal token #735 遗留)——当年 guest 的
4676
- * principal.id 都是同张 cap.id 区分不了人才加它;现在 principal.id = 设备 deviceId、
4677
- * 归属人由 contact(deviceId→ownerId) 反查,该字段理由消失。
4663
+ * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4678
4664
  */
4679
4665
  creatorPrincipalId: external_exports.string().min(1).optional(),
4680
4666
  /**
4681
- * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上创建的 session**,
4682
- * 远端是 source of truth,本机是持久化副本,方便 sidebar 跨设备聚合。
4683
- * 字段值 = 远端 daemon 的 deviceId(决策 #16:朋友设备标识,即我接入的那台机器)。
4667
+ * 创建该 session 的接入者 owner identity(auth selfPrincipalId)。global personal token
4668
+ * (#735) creatorPrincipalId guest ctx 永远是同一张 personal cap.id,UI 端无法区分
4669
+ * 接入者;这个字段把"是哪个人"的信息固定下来。
4670
+ *
4671
+ * 派生规则:
4672
+ * - owner ctx 创建:= 本机 ownerPrincipalId
4673
+ * - guest ctx + auth 帧带 selfPrincipalId:= ctx.peerOwnerPrincipalId
4674
+ * - 旧 per-peer cap 路径 / 老 session:undefined(UI 视作 self 兼容)
4675
+ *
4676
+ * UI 用法:
4677
+ * - Recents/Active/Pinned 过滤:`!creatorOwnerPrincipalId || === ownerPrincipalId` 才显示
4678
+ * - MyPersonas 二级分组:non-self → 按 contacts 反查 displayName 分桶
4679
+ *
4680
+ * 当前为 caller 自报,daemon 不密码学验证真伪;强身份验签是独立工作(B 方案)。
4681
+ */
4682
+ creatorOwnerPrincipalId: external_exports.string().min(1).optional(),
4683
+ /**
4684
+ * Mirror peer sessions (2026-05-26): mirror 标识。**我在朋友 daemon 上以 guest ctx
4685
+ * 创建的 session**,远端是 source of truth,本机是该 session 的持久化副本,方便
4686
+ * sidebar 跨设备聚合。字段值是远端 daemon 的 ownerPrincipalId(即我接入的那位朋友)。
4684
4687
  *
4685
4688
  * 语义关键澄清:mirror **不是朋友创建给我的会话**——它是我自己的会话,只是在朋友
4686
4689
  * 的机器上跑。sidebar chip 显示 `@<朋友>` 暗示「位置」,不是「来源」。
4687
4690
  *
4688
4691
  * - undefined / 缺省 → 本机 native session(在我自己机器上跑)
4689
- * - 非空 → 我的会话在该 deviceId 朋友的 daemon 上跑,本机为持久化副本
4692
+ * - 非空 → 我的会话在该 ownerPrincipalId 朋友的 daemon 上跑,本机为持久化副本
4690
4693
  *
4691
- * 四类 session 取值矩阵(决策 #16 deviceId 版,creatorOwnerPrincipalId 列已删):
4692
- * | 形态 | creatorPrincipalId(=设备) | originOwnerPrincipalId(=朋友设备) |
4693
- * |-------------------------------------|--------------------------|----------------------------------|
4694
- * | 本机 owner 自创 | 本机 deviceId | undefined |
4695
- * | 本机被别的设备接入创建 | 接入者 deviceId(token 背书) | undefined |
4696
- * | 我在朋友 daemon 上创建(mirror 回本机) | 我的 deviceId | 朋友 deviceId |
4694
+ * `creatorPrincipalId` / `creatorOwnerPrincipalId` 在四类 session 下取值矩阵:
4695
+ * | 形态 | creatorPrincipalId | creatorOwnerPrincipalId | originOwnerPrincipalId |
4696
+ * |-------------------------------------|--------------------------|-------------------------|------------------------|
4697
+ * | 本机 owner 自创 | 本机 ownerPrincipalId | 本机 ownerPrincipalId | undefined |
4698
+ * | 本机被别人通过 personal token 接入创建 | 远端接入者 cap.id | 远端接入者 ownerPrincipalId | undefined |
4699
+ * | 我在朋友 daemon 上以 guest ctx 创建(mirror 回本机) | 朋友 daemon personal cap.id | 我自己 ownerPrincipalId | 朋友 ownerPrincipalId |
4697
4700
  *
4698
4701
  * UI 用法:
4699
4702
  * - sidebar Active/Pinned/Recents 过滤:`origin 非空 → 一律视作 self`(绕过 #739 隔离)
4700
4703
  * - 行内显示 `@<朋友>` chip 暗示位置
4701
- * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端(按 deviceId)
4704
+ * - 进 ChatPanel 时按 origin 路由到 `pool.ensureRemoteForOwner(origin)` 走远端
4702
4705
  *
4703
4706
  * daemon 用法:
4704
4707
  * - `peerSession:upsert` / `peerSession:remove` 一族 RPC 专管 mirror 写删;
@@ -5132,14 +5135,18 @@ var init_schemas = __esm({
5132
5135
  token: external_exports.string().min(1),
5133
5136
  scheme: external_exports.literal("bearer").optional(),
5134
5137
  /**
5135
- * 决策 #16:自报身份字段(selfPrincipalId / selfDisplayName / selfDeviceId)全部删除——
5136
- * guest 的设备 id / 人 id / 来源全部由 connect token 的 subDevice/subOwner/provider 签名背书
5137
- * (server 签、目标设备本地验签,不可伪造),不再接受任何自报身份。
5138
- *
5139
- * selfUrl 保留:它只是"可达地址"(auto-reverse 反向连接用),不是身份——填错只影响
5140
- * 反向连通性,不构成伪造风险,且 url 不在 token 里(token 只签身份不签地址)。
5138
+ * guest ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
5139
+ * 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
5140
+ * cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
5141
+ * tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
5141
5142
  */
5142
- selfUrl: external_exports.string().min(1).optional()
5143
+ selfPrincipalId: external_exports.string().min(1).optional(),
5144
+ /**
5145
+ * 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
5146
+ * 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
5147
+ * 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
5148
+ */
5149
+ selfDisplayName: external_exports.string().optional()
5143
5150
  });
5144
5151
  AuthOkFrameSchema = external_exports.object({
5145
5152
  type: external_exports.literal("auth:ok"),
@@ -5352,7 +5359,7 @@ function stripSecretHash(cap) {
5352
5359
  const { secretHash: _hash, ...wire } = cap;
5353
5360
  return wire;
5354
5361
  }
5355
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS;
5362
+ var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, PERSONAL_CAP_GRANTS, PersonalCapGetResponseSchema;
5356
5363
  var init_capability = __esm({
5357
5364
  "../protocol/src/capability.ts"() {
5358
5365
  "use strict";
@@ -5404,11 +5411,7 @@ var init_capability = __esm({
5404
5411
  owner: external_exports.object({
5405
5412
  id: external_exports.string(),
5406
5413
  kind: external_exports.enum(["owner", "guest"]),
5407
- displayName: external_exports.string(),
5408
- /** 归属人 id(决策 #16:TTC unique_id 等,落 contact.ownerId) */
5409
- ownerId: external_exports.string(),
5410
- /** 身份来源(决策 #16:'ttc'/'google',落 contact.provider) */
5411
- provider: external_exports.string()
5414
+ displayName: external_exports.string()
5412
5415
  }).strict(),
5413
5416
  capability: CapabilityWireSchema,
5414
5417
  grantedPersonas: external_exports.array(
@@ -5424,6 +5427,13 @@ var init_capability = __esm({
5424
5427
  actions: Object.freeze(["read", "send"])
5425
5428
  })
5426
5429
  ]);
5430
+ PersonalCapGetResponseSchema = external_exports.object({
5431
+ type: external_exports.literal("personal-cap:get:ok"),
5432
+ token: external_exports.string().min(1),
5433
+ capability: CapabilityWireSchema,
5434
+ /** ws/wss base URL,UI deriveInviteBase 用来拼 inviteUrl(tunnel 优先 / 否则本机 ws) */
5435
+ shareBaseUrl: external_exports.string().min(1)
5436
+ }).strict();
5427
5437
  }
5428
5438
  });
5429
5439
 
@@ -5435,80 +5445,83 @@ var init_inbox = __esm({
5435
5445
  init_zod();
5436
5446
  InboxMessageSchema = external_exports.object({
5437
5447
  id: external_exports.string().min(1),
5438
- peerDeviceId: external_exports.string().min(1),
5439
- senderDeviceId: external_exports.string().min(1),
5448
+ peerOwnerId: external_exports.string().min(1),
5449
+ senderPrincipalId: external_exports.string().min(1),
5440
5450
  text: external_exports.string().min(1),
5441
5451
  createdAt: external_exports.number().int().nonnegative(),
5442
5452
  readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5443
5453
  }).strict();
5444
5454
  InboxPostMessageArgsSchema = external_exports.object({
5445
- peerDeviceId: external_exports.string().min(1).optional(),
5455
+ peerOwnerId: external_exports.string().min(1).optional(),
5446
5456
  id: external_exports.string().min(1),
5447
5457
  text: external_exports.string().min(1),
5448
5458
  createdAt: external_exports.number().int().nonnegative()
5449
5459
  }).strict();
5450
5460
  InboxListArgsSchema = external_exports.object({
5451
- peerDeviceId: external_exports.string().min(1),
5461
+ peerOwnerId: external_exports.string().min(1),
5452
5462
  sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5453
5463
  }).strict();
5454
5464
  InboxMarkReadArgsSchema = external_exports.object({
5455
- peerDeviceId: external_exports.string().min(1),
5465
+ peerOwnerId: external_exports.string().min(1),
5456
5466
  upToCreatedAt: external_exports.number().int().nonnegative()
5457
5467
  }).strict();
5458
5468
  }
5459
5469
  });
5460
5470
 
5461
- // ../protocol/src/contact.ts
5462
- var ContactSchema, ContactWireSchema, ContactRemoveArgsSchema, ContactRemoveOkSchema, ContactListOkSchema, ContactAddedFrameSchema, ContactRemovedFrameSchema;
5463
- var init_contact = __esm({
5464
- "../protocol/src/contact.ts"() {
5471
+ // ../protocol/src/received-capability.ts
5472
+ var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
5473
+ var init_received_capability = __esm({
5474
+ "../protocol/src/received-capability.ts"() {
5465
5475
  "use strict";
5466
5476
  init_zod();
5467
5477
  init_capability();
5468
- ContactSchema = external_exports.object({
5469
- /**
5470
- * 对方设备标识(主键):对方 daemon 自己生成并持久化的标识(auth.json deviceId,
5471
- * 如 dev-<uuid>),provider 无关。同 deviceId 重复 upsert 覆盖。
5472
- */
5473
- deviceId: external_exports.string().min(1),
5474
- /**
5475
- * 归属人 id(决策 #16):TTC unique_id 等,由 connect token subOwner 签名背书。
5476
- * 与 provider 一起唯一标识一个人。DM 不再按它路由(路由用 deviceId),仅显示/分组用。
5477
- */
5478
- ownerId: external_exports.string().min(1),
5479
- /** 身份来源(决策 #16):'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...,token 背书 */
5480
- provider: external_exports.string().min(1),
5481
- /** 联系人显示名 */
5482
- displayName: external_exports.string(),
5483
- /** 该设备可达的 ws/tunnel 地址 */
5478
+ ReceivedCapabilitySchema = external_exports.object({
5479
+ ownerPrincipalId: external_exports.string().min(1),
5480
+ ownerDisplayName: external_exports.string(),
5484
5481
  remoteUrl: external_exports.string().min(1),
5485
- // 自动反向建立联系人(spec 决策 #11):被连接方本地验票成功后自动落 A 为
5486
- // 联系人,但它不持有 A 颁发的 token(A 从未给它换票)→ connectToken 为空串。
5487
- // device:connect 出站路径仍存中心 server 签发的真实 connect token(断线重连复用)。
5488
- // 不用 .min(1):自动反向路径合法地存空串,禁止填假 token 占位(不造假数据)。
5489
- connectToken: external_exports.string(),
5482
+ capabilityId: external_exports.string().min(1),
5483
+ capabilityToken: external_exports.string().min(1),
5490
5484
  grants: external_exports.array(GrantSchema),
5491
- addedAt: external_exports.number().int().nonnegative()
5485
+ receivedAt: external_exports.number().int().nonnegative()
5486
+ }).strict();
5487
+ ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
5488
+ ReceivedCapabilityAddArgsSchema = external_exports.object({
5489
+ remoteUrl: external_exports.string().min(1),
5490
+ token: external_exports.string().min(1)
5491
+ }).strict();
5492
+ ReceivedCapabilityAddOkSchema = external_exports.object({
5493
+ type: external_exports.literal("received-capability:add:ok"),
5494
+ receivedCap: ReceivedCapabilityWireSchema
5492
5495
  }).strict();
5493
- ContactWireSchema = ContactSchema;
5494
- ContactRemoveArgsSchema = external_exports.object({
5495
- deviceId: external_exports.string().min(1)
5496
+ ReceivedCapabilityRemoveArgsSchema = external_exports.object({
5497
+ ownerPrincipalId: external_exports.string().min(1)
5496
5498
  }).strict();
5497
- ContactRemoveOkSchema = external_exports.object({
5498
- type: external_exports.literal("contact:remove:ok"),
5499
- deviceId: external_exports.string().min(1)
5499
+ ReceivedCapabilityRemoveOkSchema = external_exports.object({
5500
+ type: external_exports.literal("received-capability:remove:ok"),
5501
+ ownerPrincipalId: external_exports.string().min(1)
5502
+ }).strict();
5503
+ ReceivedCapabilityListOkSchema = external_exports.object({
5504
+ type: external_exports.literal("received-capability:list:ok"),
5505
+ receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
5506
+ }).strict();
5507
+ ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
5508
+ ownerPrincipalId: external_exports.string().min(1),
5509
+ ownerDisplayName: external_exports.string(),
5510
+ remoteUrl: external_exports.string().min(1),
5511
+ capabilityId: external_exports.string().min(1),
5512
+ token: external_exports.string().min(1),
5513
+ grants: external_exports.array(GrantSchema)
5500
5514
  }).strict();
5501
- ContactListOkSchema = external_exports.object({
5502
- type: external_exports.literal("contact:list:ok"),
5503
- contacts: external_exports.array(ContactWireSchema)
5515
+ ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
5516
+ type: external_exports.literal("received-capability:autoAttach:ok")
5504
5517
  }).strict();
5505
- ContactAddedFrameSchema = external_exports.object({
5506
- type: external_exports.literal("contact:added"),
5507
- contact: ContactWireSchema
5518
+ ReceivedCapabilityAddedFrameSchema = external_exports.object({
5519
+ type: external_exports.literal("received-capability:added"),
5520
+ receivedCap: ReceivedCapabilityWireSchema
5508
5521
  }).strict();
5509
- ContactRemovedFrameSchema = external_exports.object({
5510
- type: external_exports.literal("contact:removed"),
5511
- deviceId: external_exports.string().min(1)
5522
+ ReceivedCapabilityRemovedFrameSchema = external_exports.object({
5523
+ type: external_exports.literal("received-capability:removed"),
5524
+ ownerPrincipalId: external_exports.string().min(1)
5512
5525
  }).strict();
5513
5526
  }
5514
5527
  });
@@ -5712,107 +5725,6 @@ var init_extension = __esm({
5712
5725
  }
5713
5726
  });
5714
5727
 
5715
- // ../protocol/src/feishu-auth.ts
5716
- var FEISHU_GATED_METHODS, DeviceEntrySchema, DeviceListResponseSchema, DeviceConnectArgsSchema, DeviceConnectResponseSchema, FeishuIdentitySchema, AuthLoginStartResponseSchema, AuthGetIdentityResponseSchema, AuthLogoutResponseSchema, AuthLoginDoneEventSchema, AuthLoginFailedEventSchema;
5717
- var init_feishu_auth = __esm({
5718
- "../protocol/src/feishu-auth.ts"() {
5719
- "use strict";
5720
- init_zod();
5721
- FEISHU_GATED_METHODS = [
5722
- // capability(调试 / 撤销,仍属远程身份体系)
5723
- "capability:list",
5724
- "capability:delete",
5725
- // 联系人列表(device:connect 落同一 store;list 读 / remove 删)
5726
- "contact:list",
5727
- "contact:remove",
5728
- // DM / 跨用户通知
5729
- "inbox:list",
5730
- "inbox:markRead",
5731
- "inbox:postMessage",
5732
- // mirror peer sessions(远端会话镜像)
5733
- "peerSession:upsert",
5734
- "peerSession:remove",
5735
- // 设备目录 + 连接(决策 #15:daemon 代理中心 server,需要 owner 的 ttcJwt)
5736
- "device:list",
5737
- "device:connect"
5738
- ];
5739
- DeviceEntrySchema = external_exports.object({
5740
- /**
5741
- * 设备标识(决策 #15:daemon 本地生成的 dev-<uuid>,与人的身份 unionId 无关)。
5742
- * 中心 server clawd_devices 注册表仅作服务发现(url 下发 + 目录),不在安全路径上。
5743
- */
5744
- deviceId: external_exports.string().min(1),
5745
- /** 显示名 */
5746
- name: external_exports.string().min(1),
5747
- /** ws/tunnel 地址 */
5748
- url: external_exports.string().min(1)
5749
- });
5750
- DeviceListResponseSchema = external_exports.object({
5751
- type: external_exports.literal("device:list:ok"),
5752
- devices: external_exports.array(DeviceEntrySchema)
5753
- });
5754
- DeviceConnectArgsSchema = external_exports.object({
5755
- /** 目标设备的 deviceId(对方 daemon 生成的 dev-<uuid>) */
5756
- deviceId: external_exports.string().min(1),
5757
- /**
5758
- * 目标设备的 ws/tunnel 地址(可选):
5759
- * - 公开目录点击:带 url(device:list 已下发,省一次 server 查询)
5760
- * - 凭 deviceId 连接:缺省 url → daemon exchange 时由中心 server 下发该设备上报的 url
5761
- * - 注册表不在安全路径上:知道对方 url 时显式传入即可连接(不依赖中心 server 服务发现)
5762
- */
5763
- url: external_exports.string().min(1).optional(),
5764
- /** 显示名(公共设备来自目录;缺省时连上后用 whoami 回填) */
5765
- name: external_exports.string().optional()
5766
- });
5767
- DeviceConnectResponseSchema = external_exports.object({
5768
- type: external_exports.literal("device:connect:ok"),
5769
- /** 连接的目标设备标识(= 请求 args.deviceId) */
5770
- deviceId: external_exports.string(),
5771
- name: external_exports.string(),
5772
- url: external_exports.string()
5773
- });
5774
- FeishuIdentitySchema = external_exports.object({
5775
- /** 归属人 id(决策 #16:TTC unique_id,取代飞书 union_id;跨登录方式稳定) */
5776
- ownerId: external_exports.string().min(1),
5777
- /** 身份来源:'ttc'(飞书/手机/邮箱登入 TTC)| 'google' | ...(决策 #16,拓展用) */
5778
- provider: external_exports.string().min(1),
5779
- /** 显示名。TTC user_info 接口 data.name */
5780
- displayName: external_exports.string().min(1),
5781
- /** TTC user_info 接口 data.avatar_url,可缺省 */
5782
- avatarUrl: external_exports.string().optional()
5783
- });
5784
- AuthLoginStartResponseSchema = external_exports.object({
5785
- type: external_exports.literal("auth:login:start:ok"),
5786
- /** 完整 TTC 授权页 URL(含 callback_url + state + auto=1),UI 直接 window.open */
5787
- authUrl: external_exports.string().min(1),
5788
- /** 防 CSRF 的一次性 nonce;回调时 daemon 校验 */
5789
- state: external_exports.string().min(1)
5790
- });
5791
- AuthGetIdentityResponseSchema = external_exports.object({
5792
- type: external_exports.literal("auth:getIdentity:ok"),
5793
- /** null = 未登录 */
5794
- identity: FeishuIdentitySchema.nullable(),
5795
- /**
5796
- * 本机设备标识(决策 #15:daemon 生成并持久化在 auth.json,如 dev-<uuid>)。
5797
- * 设备属性,与登录态无关、登录登出不变。UI"我的设备标识"用它拼
5798
- * clawos://device/<deviceId>(替代旧的 unionId——不再向外暴露人的身份标识)。
5799
- */
5800
- deviceId: external_exports.string().min(1)
5801
- });
5802
- AuthLogoutResponseSchema = external_exports.object({
5803
- type: external_exports.literal("auth:logout:ok")
5804
- });
5805
- AuthLoginDoneEventSchema = external_exports.object({
5806
- type: external_exports.literal("auth:login:done"),
5807
- identity: FeishuIdentitySchema
5808
- });
5809
- AuthLoginFailedEventSchema = external_exports.object({
5810
- type: external_exports.literal("auth:login:failed"),
5811
- reason: external_exports.string()
5812
- });
5813
- }
5814
- });
5815
-
5816
5728
  // ../protocol/src/runtime.ts
5817
5729
  var init_runtime = __esm({
5818
5730
  "../protocol/src/runtime.ts"() {
@@ -5828,9 +5740,8 @@ var init_runtime = __esm({
5828
5740
  init_principal();
5829
5741
  init_capability();
5830
5742
  init_inbox();
5831
- init_contact();
5743
+ init_received_capability();
5832
5744
  init_extension();
5833
- init_feishu_auth();
5834
5745
  }
5835
5746
  });
5836
5747
 
@@ -6105,8 +6016,8 @@ var require_req = __commonJS({
6105
6016
  if (req.originalUrl) {
6106
6017
  _req.url = req.originalUrl;
6107
6018
  } else {
6108
- const path60 = req.path;
6109
- _req.url = typeof path60 === "string" ? path60 : req.url ? req.url.path || req.url : void 0;
6019
+ const path59 = req.path;
6020
+ _req.url = typeof path59 === "string" ? path59 : req.url ? req.url.path || req.url : void 0;
6110
6021
  }
6111
6022
  if (req.query) {
6112
6023
  _req.query = req.query;
@@ -6271,14 +6182,14 @@ var require_redact = __commonJS({
6271
6182
  }
6272
6183
  return obj;
6273
6184
  }
6274
- function parsePath(path60) {
6185
+ function parsePath(path59) {
6275
6186
  const parts = [];
6276
6187
  let current = "";
6277
6188
  let inBrackets = false;
6278
6189
  let inQuotes = false;
6279
6190
  let quoteChar = "";
6280
- for (let i = 0; i < path60.length; i++) {
6281
- const char = path60[i];
6191
+ for (let i = 0; i < path59.length; i++) {
6192
+ const char = path59[i];
6282
6193
  if (!inBrackets && char === ".") {
6283
6194
  if (current) {
6284
6195
  parts.push(current);
@@ -6409,10 +6320,10 @@ var require_redact = __commonJS({
6409
6320
  return current;
6410
6321
  }
6411
6322
  function redactPaths(obj, paths, censor, remove = false) {
6412
- for (const path60 of paths) {
6413
- const parts = parsePath(path60);
6323
+ for (const path59 of paths) {
6324
+ const parts = parsePath(path59);
6414
6325
  if (parts.includes("*")) {
6415
- redactWildcardPath(obj, parts, censor, path60, remove);
6326
+ redactWildcardPath(obj, parts, censor, path59, remove);
6416
6327
  } else {
6417
6328
  if (remove) {
6418
6329
  removeKey(obj, parts);
@@ -6497,8 +6408,8 @@ var require_redact = __commonJS({
6497
6408
  }
6498
6409
  } else {
6499
6410
  if (afterWildcard.includes("*")) {
6500
- const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6501
- const fullPath = [...pathArray.slice(0, pathLength), ...path60];
6411
+ const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6412
+ const fullPath = [...pathArray.slice(0, pathLength), ...path59];
6502
6413
  return censor(value, fullPath);
6503
6414
  } : censor;
6504
6415
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6533,8 +6444,8 @@ var require_redact = __commonJS({
6533
6444
  return null;
6534
6445
  }
6535
6446
  const pathStructure = /* @__PURE__ */ new Map();
6536
- for (const path60 of pathsToClone) {
6537
- const parts = parsePath(path60);
6447
+ for (const path59 of pathsToClone) {
6448
+ const parts = parsePath(path59);
6538
6449
  let current = pathStructure;
6539
6450
  for (let i = 0; i < parts.length; i++) {
6540
6451
  const part = parts[i];
@@ -6586,24 +6497,24 @@ var require_redact = __commonJS({
6586
6497
  }
6587
6498
  return cloneSelectively(obj, pathStructure);
6588
6499
  }
6589
- function validatePath(path60) {
6590
- if (typeof path60 !== "string") {
6500
+ function validatePath(path59) {
6501
+ if (typeof path59 !== "string") {
6591
6502
  throw new Error("Paths must be (non-empty) strings");
6592
6503
  }
6593
- if (path60 === "") {
6504
+ if (path59 === "") {
6594
6505
  throw new Error("Invalid redaction path ()");
6595
6506
  }
6596
- if (path60.includes("..")) {
6597
- throw new Error(`Invalid redaction path (${path60})`);
6507
+ if (path59.includes("..")) {
6508
+ throw new Error(`Invalid redaction path (${path59})`);
6598
6509
  }
6599
- if (path60.includes(",")) {
6600
- throw new Error(`Invalid redaction path (${path60})`);
6510
+ if (path59.includes(",")) {
6511
+ throw new Error(`Invalid redaction path (${path59})`);
6601
6512
  }
6602
6513
  let bracketCount = 0;
6603
6514
  let inQuotes = false;
6604
6515
  let quoteChar = "";
6605
- for (let i = 0; i < path60.length; i++) {
6606
- const char = path60[i];
6516
+ for (let i = 0; i < path59.length; i++) {
6517
+ const char = path59[i];
6607
6518
  if ((char === '"' || char === "'") && bracketCount > 0) {
6608
6519
  if (!inQuotes) {
6609
6520
  inQuotes = true;
@@ -6617,20 +6528,20 @@ var require_redact = __commonJS({
6617
6528
  } else if (char === "]" && !inQuotes) {
6618
6529
  bracketCount--;
6619
6530
  if (bracketCount < 0) {
6620
- throw new Error(`Invalid redaction path (${path60})`);
6531
+ throw new Error(`Invalid redaction path (${path59})`);
6621
6532
  }
6622
6533
  }
6623
6534
  }
6624
6535
  if (bracketCount !== 0) {
6625
- throw new Error(`Invalid redaction path (${path60})`);
6536
+ throw new Error(`Invalid redaction path (${path59})`);
6626
6537
  }
6627
6538
  }
6628
6539
  function validatePaths(paths) {
6629
6540
  if (!Array.isArray(paths)) {
6630
6541
  throw new TypeError("paths must be an array");
6631
6542
  }
6632
- for (const path60 of paths) {
6633
- validatePath(path60);
6543
+ for (const path59 of paths) {
6544
+ validatePath(path59);
6634
6545
  }
6635
6546
  }
6636
6547
  function slowRedact(options = {}) {
@@ -6798,8 +6709,8 @@ var require_redaction = __commonJS({
6798
6709
  if (shape[k2] === null) {
6799
6710
  o[k2] = (value) => topCensor(value, [k2]);
6800
6711
  } else {
6801
- const wrappedCensor = typeof censor === "function" ? (value, path60) => {
6802
- return censor(value, [k2, ...path60]);
6712
+ const wrappedCensor = typeof censor === "function" ? (value, path59) => {
6713
+ return censor(value, [k2, ...path59]);
6803
6714
  } : censor;
6804
6715
  o[k2] = Redact({
6805
6716
  paths: shape[k2],
@@ -7017,10 +6928,10 @@ var require_atomic_sleep = __commonJS({
7017
6928
  var require_sonic_boom = __commonJS({
7018
6929
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
7019
6930
  "use strict";
7020
- var fs49 = require("fs");
6931
+ var fs48 = require("fs");
7021
6932
  var EventEmitter3 = require("events");
7022
6933
  var inherits = require("util").inherits;
7023
- var path60 = require("path");
6934
+ var path59 = require("path");
7024
6935
  var sleep = require_atomic_sleep();
7025
6936
  var assert = require("assert");
7026
6937
  var BUSY_WRITE_TIMEOUT = 100;
@@ -7074,20 +6985,20 @@ var require_sonic_boom = __commonJS({
7074
6985
  const mode = sonic.mode;
7075
6986
  if (sonic.sync) {
7076
6987
  try {
7077
- if (sonic.mkdir) fs49.mkdirSync(path60.dirname(file), { recursive: true });
7078
- const fd = fs49.openSync(file, flags, mode);
6988
+ if (sonic.mkdir) fs48.mkdirSync(path59.dirname(file), { recursive: true });
6989
+ const fd = fs48.openSync(file, flags, mode);
7079
6990
  fileOpened(null, fd);
7080
6991
  } catch (err) {
7081
6992
  fileOpened(err);
7082
6993
  throw err;
7083
6994
  }
7084
6995
  } else if (sonic.mkdir) {
7085
- fs49.mkdir(path60.dirname(file), { recursive: true }, (err) => {
6996
+ fs48.mkdir(path59.dirname(file), { recursive: true }, (err) => {
7086
6997
  if (err) return fileOpened(err);
7087
- fs49.open(file, flags, mode, fileOpened);
6998
+ fs48.open(file, flags, mode, fileOpened);
7088
6999
  });
7089
7000
  } else {
7090
- fs49.open(file, flags, mode, fileOpened);
7001
+ fs48.open(file, flags, mode, fileOpened);
7091
7002
  }
7092
7003
  }
7093
7004
  function SonicBoom(opts) {
@@ -7128,8 +7039,8 @@ var require_sonic_boom = __commonJS({
7128
7039
  this.flush = flushBuffer;
7129
7040
  this.flushSync = flushBufferSync;
7130
7041
  this._actualWrite = actualWriteBuffer;
7131
- fsWriteSync = () => fs49.writeSync(this.fd, this._writingBuf);
7132
- fsWrite = () => fs49.write(this.fd, this._writingBuf, this.release);
7042
+ fsWriteSync = () => fs48.writeSync(this.fd, this._writingBuf);
7043
+ fsWrite = () => fs48.write(this.fd, this._writingBuf, this.release);
7133
7044
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
7134
7045
  this._writingBuf = "";
7135
7046
  this.write = write;
@@ -7138,15 +7049,15 @@ var require_sonic_boom = __commonJS({
7138
7049
  this._actualWrite = actualWrite;
7139
7050
  fsWriteSync = () => {
7140
7051
  if (Buffer.isBuffer(this._writingBuf)) {
7141
- return fs49.writeSync(this.fd, this._writingBuf);
7052
+ return fs48.writeSync(this.fd, this._writingBuf);
7142
7053
  }
7143
- return fs49.writeSync(this.fd, this._writingBuf, "utf8");
7054
+ return fs48.writeSync(this.fd, this._writingBuf, "utf8");
7144
7055
  };
7145
7056
  fsWrite = () => {
7146
7057
  if (Buffer.isBuffer(this._writingBuf)) {
7147
- return fs49.write(this.fd, this._writingBuf, this.release);
7058
+ return fs48.write(this.fd, this._writingBuf, this.release);
7148
7059
  }
7149
- return fs49.write(this.fd, this._writingBuf, "utf8", this.release);
7060
+ return fs48.write(this.fd, this._writingBuf, "utf8", this.release);
7150
7061
  };
7151
7062
  } else {
7152
7063
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -7203,7 +7114,7 @@ var require_sonic_boom = __commonJS({
7203
7114
  }
7204
7115
  }
7205
7116
  if (this._fsync) {
7206
- fs49.fsyncSync(this.fd);
7117
+ fs48.fsyncSync(this.fd);
7207
7118
  }
7208
7119
  const len = this._len;
7209
7120
  if (this._reopening) {
@@ -7317,7 +7228,7 @@ var require_sonic_boom = __commonJS({
7317
7228
  const onDrain = () => {
7318
7229
  if (!this._fsync) {
7319
7230
  try {
7320
- fs49.fsync(this.fd, (err) => {
7231
+ fs48.fsync(this.fd, (err) => {
7321
7232
  this._flushPending = false;
7322
7233
  cb(err);
7323
7234
  });
@@ -7419,7 +7330,7 @@ var require_sonic_boom = __commonJS({
7419
7330
  const fd = this.fd;
7420
7331
  this.once("ready", () => {
7421
7332
  if (fd !== this.fd) {
7422
- fs49.close(fd, (err) => {
7333
+ fs48.close(fd, (err) => {
7423
7334
  if (err) {
7424
7335
  return this.emit("error", err);
7425
7336
  }
@@ -7468,7 +7379,7 @@ var require_sonic_boom = __commonJS({
7468
7379
  buf = this._bufs[0];
7469
7380
  }
7470
7381
  try {
7471
- const n = Buffer.isBuffer(buf) ? fs49.writeSync(this.fd, buf) : fs49.writeSync(this.fd, buf, "utf8");
7382
+ const n = Buffer.isBuffer(buf) ? fs48.writeSync(this.fd, buf) : fs48.writeSync(this.fd, buf, "utf8");
7472
7383
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
7473
7384
  buf = releasedBufObj.writingBuf;
7474
7385
  this._len = releasedBufObj.len;
@@ -7484,7 +7395,7 @@ var require_sonic_boom = __commonJS({
7484
7395
  }
7485
7396
  }
7486
7397
  try {
7487
- fs49.fsyncSync(this.fd);
7398
+ fs48.fsyncSync(this.fd);
7488
7399
  } catch {
7489
7400
  }
7490
7401
  }
@@ -7505,7 +7416,7 @@ var require_sonic_boom = __commonJS({
7505
7416
  buf = mergeBuf(this._bufs[0], this._lens[0]);
7506
7417
  }
7507
7418
  try {
7508
- const n = fs49.writeSync(this.fd, buf);
7419
+ const n = fs48.writeSync(this.fd, buf);
7509
7420
  buf = buf.subarray(n);
7510
7421
  this._len = Math.max(this._len - n, 0);
7511
7422
  if (buf.length <= 0) {
@@ -7533,13 +7444,13 @@ var require_sonic_boom = __commonJS({
7533
7444
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7534
7445
  if (this.sync) {
7535
7446
  try {
7536
- const written = Buffer.isBuffer(this._writingBuf) ? fs49.writeSync(this.fd, this._writingBuf) : fs49.writeSync(this.fd, this._writingBuf, "utf8");
7447
+ const written = Buffer.isBuffer(this._writingBuf) ? fs48.writeSync(this.fd, this._writingBuf) : fs48.writeSync(this.fd, this._writingBuf, "utf8");
7537
7448
  release(null, written);
7538
7449
  } catch (err) {
7539
7450
  release(err);
7540
7451
  }
7541
7452
  } else {
7542
- fs49.write(this.fd, this._writingBuf, release);
7453
+ fs48.write(this.fd, this._writingBuf, release);
7543
7454
  }
7544
7455
  }
7545
7456
  function actualWriteBuffer() {
@@ -7548,7 +7459,7 @@ var require_sonic_boom = __commonJS({
7548
7459
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7549
7460
  if (this.sync) {
7550
7461
  try {
7551
- const written = fs49.writeSync(this.fd, this._writingBuf);
7462
+ const written = fs48.writeSync(this.fd, this._writingBuf);
7552
7463
  release(null, written);
7553
7464
  } catch (err) {
7554
7465
  release(err);
@@ -7557,7 +7468,7 @@ var require_sonic_boom = __commonJS({
7557
7468
  if (kCopyBuffer) {
7558
7469
  this._writingBuf = Buffer.from(this._writingBuf);
7559
7470
  }
7560
- fs49.write(this.fd, this._writingBuf, release);
7471
+ fs48.write(this.fd, this._writingBuf, release);
7561
7472
  }
7562
7473
  }
7563
7474
  function actualClose(sonic) {
@@ -7573,12 +7484,12 @@ var require_sonic_boom = __commonJS({
7573
7484
  sonic._lens = [];
7574
7485
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7575
7486
  try {
7576
- fs49.fsync(sonic.fd, closeWrapped);
7487
+ fs48.fsync(sonic.fd, closeWrapped);
7577
7488
  } catch {
7578
7489
  }
7579
7490
  function closeWrapped() {
7580
7491
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7581
- fs49.close(sonic.fd, done);
7492
+ fs48.close(sonic.fd, done);
7582
7493
  } else {
7583
7494
  done();
7584
7495
  }
@@ -7835,7 +7746,7 @@ var require_thread_stream = __commonJS({
7835
7746
  var { version: version2 } = require_package();
7836
7747
  var { EventEmitter: EventEmitter3 } = require("events");
7837
7748
  var { Worker } = require("worker_threads");
7838
- var { join: join13 } = require("path");
7749
+ var { join: join12 } = require("path");
7839
7750
  var { pathToFileURL } = require("url");
7840
7751
  var { wait } = require_wait();
7841
7752
  var {
@@ -7871,7 +7782,7 @@ var require_thread_stream = __commonJS({
7871
7782
  function createWorker(stream, opts) {
7872
7783
  const { filename, workerData } = opts;
7873
7784
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7874
- const toExecute = bundlerOverrides["thread-stream-worker"] || join13(__dirname, "lib", "worker.js");
7785
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join12(__dirname, "lib", "worker.js");
7875
7786
  const worker = new Worker(toExecute, {
7876
7787
  ...opts.workerOpts,
7877
7788
  trackUnmanagedFds: false,
@@ -8257,7 +8168,7 @@ var require_transport = __commonJS({
8257
8168
  "use strict";
8258
8169
  var { createRequire } = require("module");
8259
8170
  var getCallers = require_caller();
8260
- var { join: join13, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8171
+ var { join: join12, isAbsolute: isAbsolute2, sep: sep3 } = require("path");
8261
8172
  var sleep = require_atomic_sleep();
8262
8173
  var onExit = require_on_exit_leak_free();
8263
8174
  var ThreadStream = require_thread_stream();
@@ -8320,7 +8231,7 @@ var require_transport = __commonJS({
8320
8231
  throw new Error("only one of target or targets can be specified");
8321
8232
  }
8322
8233
  if (targets) {
8323
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8234
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8324
8235
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
8325
8236
  return {
8326
8237
  ...dest,
@@ -8338,7 +8249,7 @@ var require_transport = __commonJS({
8338
8249
  });
8339
8250
  });
8340
8251
  } else if (pipeline3) {
8341
- target = bundlerOverrides["pino-worker"] || join13(__dirname, "worker.js");
8252
+ target = bundlerOverrides["pino-worker"] || join12(__dirname, "worker.js");
8342
8253
  options.pipelines = [pipeline3.map((dest) => {
8343
8254
  return {
8344
8255
  ...dest,
@@ -8360,7 +8271,7 @@ var require_transport = __commonJS({
8360
8271
  return origin;
8361
8272
  }
8362
8273
  if (origin === "pino/file") {
8363
- return join13(__dirname, "..", "file.js");
8274
+ return join12(__dirname, "..", "file.js");
8364
8275
  }
8365
8276
  let fixTarget2;
8366
8277
  for (const filePath of callers) {
@@ -9350,7 +9261,7 @@ var require_safe_stable_stringify = __commonJS({
9350
9261
  return circularValue;
9351
9262
  }
9352
9263
  let res = "";
9353
- let join13 = ",";
9264
+ let join12 = ",";
9354
9265
  const originalIndentation = indentation;
9355
9266
  if (Array.isArray(value)) {
9356
9267
  if (value.length === 0) {
@@ -9364,7 +9275,7 @@ var require_safe_stable_stringify = __commonJS({
9364
9275
  indentation += spacer;
9365
9276
  res += `
9366
9277
  ${indentation}`;
9367
- join13 = `,
9278
+ join12 = `,
9368
9279
  ${indentation}`;
9369
9280
  }
9370
9281
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9372,13 +9283,13 @@ ${indentation}`;
9372
9283
  for (; i < maximumValuesToStringify - 1; i++) {
9373
9284
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9374
9285
  res += tmp2 !== void 0 ? tmp2 : "null";
9375
- res += join13;
9286
+ res += join12;
9376
9287
  }
9377
9288
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
9378
9289
  res += tmp !== void 0 ? tmp : "null";
9379
9290
  if (value.length - 1 > maximumBreadth) {
9380
9291
  const removedKeys = value.length - maximumBreadth - 1;
9381
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9292
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9382
9293
  }
9383
9294
  if (spacer !== "") {
9384
9295
  res += `
@@ -9399,7 +9310,7 @@ ${originalIndentation}`;
9399
9310
  let separator = "";
9400
9311
  if (spacer !== "") {
9401
9312
  indentation += spacer;
9402
- join13 = `,
9313
+ join12 = `,
9403
9314
  ${indentation}`;
9404
9315
  whitespace = " ";
9405
9316
  }
@@ -9413,13 +9324,13 @@ ${indentation}`;
9413
9324
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
9414
9325
  if (tmp !== void 0) {
9415
9326
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9416
- separator = join13;
9327
+ separator = join12;
9417
9328
  }
9418
9329
  }
9419
9330
  if (keyLength > maximumBreadth) {
9420
9331
  const removedKeys = keyLength - maximumBreadth;
9421
9332
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
9422
- separator = join13;
9333
+ separator = join12;
9423
9334
  }
9424
9335
  if (spacer !== "" && separator.length > 1) {
9425
9336
  res = `
@@ -9460,7 +9371,7 @@ ${originalIndentation}`;
9460
9371
  }
9461
9372
  const originalIndentation = indentation;
9462
9373
  let res = "";
9463
- let join13 = ",";
9374
+ let join12 = ",";
9464
9375
  if (Array.isArray(value)) {
9465
9376
  if (value.length === 0) {
9466
9377
  return "[]";
@@ -9473,7 +9384,7 @@ ${originalIndentation}`;
9473
9384
  indentation += spacer;
9474
9385
  res += `
9475
9386
  ${indentation}`;
9476
- join13 = `,
9387
+ join12 = `,
9477
9388
  ${indentation}`;
9478
9389
  }
9479
9390
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -9481,13 +9392,13 @@ ${indentation}`;
9481
9392
  for (; i < maximumValuesToStringify - 1; i++) {
9482
9393
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9483
9394
  res += tmp2 !== void 0 ? tmp2 : "null";
9484
- res += join13;
9395
+ res += join12;
9485
9396
  }
9486
9397
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
9487
9398
  res += tmp !== void 0 ? tmp : "null";
9488
9399
  if (value.length - 1 > maximumBreadth) {
9489
9400
  const removedKeys = value.length - maximumBreadth - 1;
9490
- res += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9401
+ res += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
9491
9402
  }
9492
9403
  if (spacer !== "") {
9493
9404
  res += `
@@ -9500,7 +9411,7 @@ ${originalIndentation}`;
9500
9411
  let whitespace = "";
9501
9412
  if (spacer !== "") {
9502
9413
  indentation += spacer;
9503
- join13 = `,
9414
+ join12 = `,
9504
9415
  ${indentation}`;
9505
9416
  whitespace = " ";
9506
9417
  }
@@ -9509,7 +9420,7 @@ ${indentation}`;
9509
9420
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
9510
9421
  if (tmp !== void 0) {
9511
9422
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
9512
- separator = join13;
9423
+ separator = join12;
9513
9424
  }
9514
9425
  }
9515
9426
  if (spacer !== "" && separator.length > 1) {
@@ -9567,20 +9478,20 @@ ${originalIndentation}`;
9567
9478
  indentation += spacer;
9568
9479
  let res2 = `
9569
9480
  ${indentation}`;
9570
- const join14 = `,
9481
+ const join13 = `,
9571
9482
  ${indentation}`;
9572
9483
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9573
9484
  let i = 0;
9574
9485
  for (; i < maximumValuesToStringify - 1; i++) {
9575
9486
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9576
9487
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9577
- res2 += join14;
9488
+ res2 += join13;
9578
9489
  }
9579
9490
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9580
9491
  res2 += tmp !== void 0 ? tmp : "null";
9581
9492
  if (value.length - 1 > maximumBreadth) {
9582
9493
  const removedKeys = value.length - maximumBreadth - 1;
9583
- res2 += `${join14}"... ${getItemCount(removedKeys)} not stringified"`;
9494
+ res2 += `${join13}"... ${getItemCount(removedKeys)} not stringified"`;
9584
9495
  }
9585
9496
  res2 += `
9586
9497
  ${originalIndentation}`;
@@ -9596,16 +9507,16 @@ ${originalIndentation}`;
9596
9507
  return '"[Object]"';
9597
9508
  }
9598
9509
  indentation += spacer;
9599
- const join13 = `,
9510
+ const join12 = `,
9600
9511
  ${indentation}`;
9601
9512
  let res = "";
9602
9513
  let separator = "";
9603
9514
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9604
9515
  if (isTypedArrayWithEntries(value)) {
9605
- res += stringifyTypedArray(value, join13, maximumBreadth);
9516
+ res += stringifyTypedArray(value, join12, maximumBreadth);
9606
9517
  keys = keys.slice(value.length);
9607
9518
  maximumPropertiesToStringify -= value.length;
9608
- separator = join13;
9519
+ separator = join12;
9609
9520
  }
9610
9521
  if (deterministic) {
9611
9522
  keys = sort(keys, comparator);
@@ -9616,13 +9527,13 @@ ${indentation}`;
9616
9527
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9617
9528
  if (tmp !== void 0) {
9618
9529
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9619
- separator = join13;
9530
+ separator = join12;
9620
9531
  }
9621
9532
  }
9622
9533
  if (keyLength > maximumBreadth) {
9623
9534
  const removedKeys = keyLength - maximumBreadth;
9624
9535
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9625
- separator = join13;
9536
+ separator = join12;
9626
9537
  }
9627
9538
  if (separator !== "") {
9628
9539
  res = `
@@ -10713,11 +10624,11 @@ var init_lib = __esm({
10713
10624
  }
10714
10625
  }
10715
10626
  },
10716
- addToPath: function addToPath(path60, added, removed, oldPosInc, options) {
10717
- var last = path60.lastComponent;
10627
+ addToPath: function addToPath(path59, added, removed, oldPosInc, options) {
10628
+ var last = path59.lastComponent;
10718
10629
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10719
10630
  return {
10720
- oldPos: path60.oldPos + oldPosInc,
10631
+ oldPos: path59.oldPos + oldPosInc,
10721
10632
  lastComponent: {
10722
10633
  count: last.count + 1,
10723
10634
  added,
@@ -10727,7 +10638,7 @@ var init_lib = __esm({
10727
10638
  };
10728
10639
  } else {
10729
10640
  return {
10730
- oldPos: path60.oldPos + oldPosInc,
10641
+ oldPos: path59.oldPos + oldPosInc,
10731
10642
  lastComponent: {
10732
10643
  count: 1,
10733
10644
  added,
@@ -11173,10 +11084,10 @@ function attachmentToHistoryMessage(o, ts) {
11173
11084
  const memories = raw.map((m2) => {
11174
11085
  if (!m2 || typeof m2 !== "object") return null;
11175
11086
  const rec = m2;
11176
- const path60 = typeof rec.path === "string" ? rec.path : null;
11087
+ const path59 = typeof rec.path === "string" ? rec.path : null;
11177
11088
  const content = typeof rec.content === "string" ? rec.content : null;
11178
- if (!path60 || content == null) return null;
11179
- const entry = { path: path60, content };
11089
+ if (!path59 || content == null) return null;
11090
+ const entry = { path: path59, content };
11180
11091
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
11181
11092
  return entry;
11182
11093
  }).filter((m2) => m2 !== null);
@@ -12007,10 +11918,10 @@ function parseAttachment(obj) {
12007
11918
  const memories = raw.map((m2) => {
12008
11919
  if (!m2 || typeof m2 !== "object") return null;
12009
11920
  const rec = m2;
12010
- const path60 = typeof rec.path === "string" ? rec.path : null;
11921
+ const path59 = typeof rec.path === "string" ? rec.path : null;
12011
11922
  const content = typeof rec.content === "string" ? rec.content : null;
12012
- if (!path60 || content == null) return null;
12013
- const out = { path: path60, content };
11923
+ if (!path59 || content == null) return null;
11924
+ const out = { path: path59, content };
12014
11925
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
12015
11926
  return out;
12016
11927
  }).filter((m2) => m2 !== null);
@@ -21209,7 +21120,7 @@ var require_BufferList = __commonJS({
21209
21120
  this.head = this.tail = null;
21210
21121
  this.length = 0;
21211
21122
  };
21212
- BufferList.prototype.join = function join13(s) {
21123
+ BufferList.prototype.join = function join12(s) {
21213
21124
  if (this.length === 0) return "";
21214
21125
  var p2 = this.head;
21215
21126
  var ret = "" + p2.data;
@@ -23799,8 +23710,8 @@ var require_utils = __commonJS({
23799
23710
  var result = transform[inputType][outputType](input);
23800
23711
  return result;
23801
23712
  };
23802
- exports2.resolve = function(path60) {
23803
- var parts = path60.split("/");
23713
+ exports2.resolve = function(path59) {
23714
+ var parts = path59.split("/");
23804
23715
  var result = [];
23805
23716
  for (var index = 0; index < parts.length; index++) {
23806
23717
  var part = parts[index];
@@ -29653,18 +29564,18 @@ var require_object = __commonJS({
29653
29564
  var object = new ZipObject(name, zipObjectContent, o);
29654
29565
  this.files[name] = object;
29655
29566
  };
29656
- var parentFolder = function(path60) {
29657
- if (path60.slice(-1) === "/") {
29658
- path60 = path60.substring(0, path60.length - 1);
29567
+ var parentFolder = function(path59) {
29568
+ if (path59.slice(-1) === "/") {
29569
+ path59 = path59.substring(0, path59.length - 1);
29659
29570
  }
29660
- var lastSlash = path60.lastIndexOf("/");
29661
- return lastSlash > 0 ? path60.substring(0, lastSlash) : "";
29571
+ var lastSlash = path59.lastIndexOf("/");
29572
+ return lastSlash > 0 ? path59.substring(0, lastSlash) : "";
29662
29573
  };
29663
- var forceTrailingSlash = function(path60) {
29664
- if (path60.slice(-1) !== "/") {
29665
- path60 += "/";
29574
+ var forceTrailingSlash = function(path59) {
29575
+ if (path59.slice(-1) !== "/") {
29576
+ path59 += "/";
29666
29577
  }
29667
- return path60;
29578
+ return path59;
29668
29579
  };
29669
29580
  var folderAdd = function(name, createFolders) {
29670
29581
  createFolders = typeof createFolders !== "undefined" ? createFolders : defaults.createFolders;
@@ -33142,7 +33053,7 @@ var SessionManager = class {
33142
33053
  }
33143
33054
  }
33144
33055
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
33145
- create(args, creatorPrincipalId) {
33056
+ create(args, creatorPrincipalId, creatorOwnerPrincipalId) {
33146
33057
  let cwd;
33147
33058
  if (args.ownerPersonaId) {
33148
33059
  cwd = derivePersonaSpawnCwd(
@@ -33185,6 +33096,7 @@ var SessionManager = class {
33185
33096
  ownerPersonaId: args.ownerPersonaId,
33186
33097
  appBuilderProject: args.appBuilderProject,
33187
33098
  creatorPrincipalId,
33099
+ ...creatorOwnerPrincipalId ? { creatorOwnerPrincipalId } : {},
33188
33100
  createdAt: iso,
33189
33101
  updatedAt: iso
33190
33102
  };
@@ -36625,10 +36537,9 @@ var LocalWsServer = class {
36625
36537
  httpServer = null;
36626
36538
  frameHandler = null;
36627
36539
  clients = /* @__PURE__ */ new Map();
36628
- // guest principal id → Set<clientId>,撤销时 O(1) 找到该 guest 的所有活跃连接做关闭级联。
36629
- // 同一 guest 可能多端同时连(多设备 / 多 tab)。
36630
- // Phase 3 (决策 #14): 索引键从 ctx.capabilityId 改为 guest principal.id(两者恒等,纯正名)。
36631
- guestIdToClients = /* @__PURE__ */ new Map();
36540
+ // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap
36541
+ // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
36542
+ capabilityIdToClients = /* @__PURE__ */ new Map();
36632
36543
  logger;
36633
36544
  pingIntervalMs;
36634
36545
  async start() {
@@ -36746,6 +36657,38 @@ var LocalWsServer = class {
36746
36657
  this.safeSend(c.ws, frame);
36747
36658
  }
36748
36659
  }
36660
+ // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
36661
+ // - 所有 owner ws (owner 看自家所有 channel)
36662
+ // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
36663
+ // 一次调用同时推两端, inbox:event push 帧由此走.
36664
+ broadcastToCapabilityChannel(capabilityId, frame) {
36665
+ this.broadcastToOwners(frame);
36666
+ const set = this.capabilityIdToClients.get(capabilityId);
36667
+ if (!set) return;
36668
+ for (const id of set) {
36669
+ const c = this.clients.get(id);
36670
+ if (!c) continue;
36671
+ this.safeSend(c.ws, frame);
36672
+ }
36673
+ }
36674
+ // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
36675
+ // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
36676
+ // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
36677
+ // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
36678
+ // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
36679
+ broadcastToPrincipal(principalId, frame) {
36680
+ if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
36681
+ this.broadcastToOwners(frame);
36682
+ return;
36683
+ }
36684
+ const set = this.capabilityIdToClients.get(principalId);
36685
+ if (!set) return;
36686
+ for (const id of set) {
36687
+ const c = this.clients.get(id);
36688
+ if (!c) continue;
36689
+ this.safeSend(c.ws, frame);
36690
+ }
36691
+ }
36749
36692
  firstSubscriber(sessionId) {
36750
36693
  for (const c of this.clients.values()) {
36751
36694
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -36768,16 +36711,16 @@ var LocalWsServer = class {
36768
36711
  this.safeSend(c.ws, frame);
36769
36712
  }
36770
36713
  // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
36771
- // 绑到 client;guest 连接同时进 guest 索引(撤销级联用)。
36714
+ // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
36772
36715
  attachClientContext(clientId, ctx) {
36773
36716
  const c = this.clients.get(clientId);
36774
36717
  if (!c) return;
36775
36718
  c.ctx = ctx;
36776
- if (ctx.principal.kind === "guest") {
36777
- let set = this.guestIdToClients.get(ctx.principal.id);
36719
+ if (ctx.capabilityId) {
36720
+ let set = this.capabilityIdToClients.get(ctx.capabilityId);
36778
36721
  if (!set) {
36779
36722
  set = /* @__PURE__ */ new Set();
36780
- this.guestIdToClients.set(ctx.principal.id, set);
36723
+ this.capabilityIdToClients.set(ctx.capabilityId, set);
36781
36724
  }
36782
36725
  set.add(clientId);
36783
36726
  }
@@ -36786,27 +36729,10 @@ var LocalWsServer = class {
36786
36729
  getClientContext(clientId) {
36787
36730
  return this.clients.get(clientId)?.ctx ?? null;
36788
36731
  }
36789
- // 飞书热切换(spec 决策 #9):身份切换后踢掉所有连接强制重连——在线连接的 ctx
36790
- // (attachClientContext 时绑定)持有旧身份,重连后用新身份重建。
36791
- // close code 4408(非 4401!4401 = token 撤销,客户端会停止重连):
36792
- // 客户端按普通断连处理 → 自动重连 → 新身份 ctx。
36793
- closeAllClients(code = 4408, reason = "IDENTITY_SWITCHED") {
36794
- const ids = [...this.clients.keys()];
36795
- for (const id of ids) {
36796
- const c = this.clients.get(id);
36797
- if (!c) continue;
36798
- try {
36799
- c.ws.close(code, reason);
36800
- } catch {
36801
- }
36802
- }
36803
- }
36804
- // Task 1.10 撤销级联:关闭某 guest principal 的所有活跃 ws 连接。close code 4401
36732
+ // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
36805
36733
  // 让客户端识别 'capability revoked' 而非普通断连。
36806
- // capability:delete 级联用 cap.id 调本方法——noAuth per-peer cap guest 的
36807
- // principal.id cap.id,级联语义不变(Phase 3 决策 #14 正名)。
36808
- closeConnectionsByGuestId(guestPrincipalId, code = 4401, reason = "TOKEN_REVOKED") {
36809
- const set = this.guestIdToClients.get(guestPrincipalId);
36734
+ closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
36735
+ const set = this.capabilityIdToClients.get(capabilityId);
36810
36736
  if (!set) return;
36811
36737
  const ids = [...set];
36812
36738
  for (const id of ids) {
@@ -36871,18 +36797,16 @@ var LocalWsServer = class {
36871
36797
  this.logger?.warn("ready frame build failed", { err: err.message });
36872
36798
  }
36873
36799
  socket.on("message", (data) => {
36874
- void this.onMessage(handle, data, remoteAddress).catch((err) => {
36875
- this.logger?.warn("onMessage failed", { err: err.message });
36876
- });
36800
+ this.onMessage(handle, data, remoteAddress);
36877
36801
  });
36878
36802
  socket.on("close", () => {
36879
36803
  const c = this.clients.get(id);
36880
36804
  if (c?.pingTimer) clearInterval(c.pingTimer);
36881
- const guestId = c?.ctx?.principal.kind === "guest" ? c.ctx.principal.id : void 0;
36882
- if (guestId) {
36883
- const set = this.guestIdToClients.get(guestId);
36805
+ const capId = c?.ctx?.capabilityId;
36806
+ if (capId) {
36807
+ const set = this.capabilityIdToClients.get(capId);
36884
36808
  set?.delete(id);
36885
- if (set && set.size === 0) this.guestIdToClients.delete(guestId);
36809
+ if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
36886
36810
  }
36887
36811
  this.clients.delete(id);
36888
36812
  authGate?.unregister(id);
@@ -36892,7 +36816,7 @@ var LocalWsServer = class {
36892
36816
  this.logger?.warn("client socket error", { clientId: id, err: err.message });
36893
36817
  });
36894
36818
  }
36895
- async onMessage(client, data, remoteAddress) {
36819
+ onMessage(client, data, remoteAddress) {
36896
36820
  let frame;
36897
36821
  try {
36898
36822
  frame = JSON.parse(data.toString());
@@ -36903,7 +36827,7 @@ var LocalWsServer = class {
36903
36827
  const authGate = this.opts.authGate;
36904
36828
  if (authGate) {
36905
36829
  const wasAuthed = authGate.isAuthed(client.id);
36906
- const verdict = await authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36830
+ const verdict = authGate.handleFrame({ id: client.id, remoteAddress }, frame);
36907
36831
  if (verdict !== "pass") {
36908
36832
  if (!wasAuthed && authGate.isAuthed(client.id)) {
36909
36833
  try {
@@ -36918,8 +36842,14 @@ var LocalWsServer = class {
36918
36842
  }
36919
36843
  } else if (frame.type === "auth") {
36920
36844
  const token = typeof frame.token === "string" ? frame.token ?? "" : "";
36845
+ const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
36846
+ const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
36921
36847
  if (token && this.opts.tryVerifyCapabilityToken) {
36922
- const guestCtx = this.opts.tryVerifyCapabilityToken(token);
36848
+ const guestCtx = this.opts.tryVerifyCapabilityToken(
36849
+ token,
36850
+ selfPrincipalId || void 0,
36851
+ selfDisplayName || void 0
36852
+ );
36923
36853
  if (guestCtx) this.attachClientContext(client.id, guestCtx);
36924
36854
  }
36925
36855
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
@@ -37000,7 +36930,7 @@ var AuthGate = class {
37000
36930
  registerConnection(handle) {
37001
36931
  const enforce = this.opts.shouldEnforce(handle.remoteAddress);
37002
36932
  if (!enforce) {
37003
- this.states.set(handle.id, { authed: true, enforce: false, timer: null, pending: false });
36933
+ this.states.set(handle.id, { authed: true, enforce: false, timer: null });
37004
36934
  return true;
37005
36935
  }
37006
36936
  const setT = this.opts.setTimer ?? ((cb, ms) => setTimeout(cb, ms));
@@ -37009,7 +36939,7 @@ var AuthGate = class {
37009
36939
  if (!st || st.authed) return;
37010
36940
  this.opts.closeConnection(handle, 1008, "auth timeout");
37011
36941
  }, this.opts.timeoutMs);
37012
- this.states.set(handle.id, { authed: false, enforce: true, timer, pending: false });
36942
+ this.states.set(handle.id, { authed: false, enforce: true, timer });
37013
36943
  return false;
37014
36944
  }
37015
36945
  unregister(handleId) {
@@ -37025,15 +36955,12 @@ var AuthGate = class {
37025
36955
  }
37026
36956
  // 收到一帧时调;返回 'consumed' / 'reject' / 'pass'
37027
36957
  // - consumed:这一帧是 auth 帧(成功或失败),调用方不要再走业务路由
37028
- // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接 / 验证进行中)
36958
+ // - reject:未通过 auth 但帧不是 auth 帧,调用方应放弃此帧(gate 已关连接)
37029
36959
  // - pass:已 authed,调用方继续走业务路由
37030
- //
37031
- // Phase 2:async(authenticate 注入路径走中心 server introspect HTTP 调用)。
37032
- async handleFrame(handle, frame) {
36960
+ handleFrame(handle, frame) {
37033
36961
  const st = this.states.get(handle.id);
37034
36962
  if (!st) return "reject";
37035
36963
  if (st.authed) return "pass";
37036
- if (st.pending) return "reject";
37037
36964
  const parsed = AuthRequestFrameSchema.safeParse(frame);
37038
36965
  if (!parsed.success) {
37039
36966
  const reason = frame.type === "auth" ? "auth failed" : "auth required";
@@ -37043,14 +36970,11 @@ var AuthGate = class {
37043
36970
  }
37044
36971
  let ctx = null;
37045
36972
  if (this.opts.authenticate) {
37046
- st.pending = true;
37047
- let r;
37048
- try {
37049
- r = await this.opts.authenticate(parsed.data.token, parsed.data.selfUrl);
37050
- } finally {
37051
- st.pending = false;
37052
- }
37053
- if (!this.states.has(handle.id)) return "reject";
36973
+ const r = this.opts.authenticate(
36974
+ parsed.data.token,
36975
+ parsed.data.selfPrincipalId,
36976
+ parsed.data.selfDisplayName
36977
+ );
37054
36978
  if (!r.ok) {
37055
36979
  this.opts.closeConnection(handle, 4401, r.code);
37056
36980
  this.markFailed(handle.id);
@@ -37153,33 +37077,26 @@ function ownerContext(ownerPrincipalId, displayName) {
37153
37077
  grants: [{ resource: { type: "*" }, actions: ["admin"] }]
37154
37078
  };
37155
37079
  }
37156
- function connectGuestContext(subDevice, subOwner, provider, displayName) {
37080
+ function guestContext(cap, peerOwnerPrincipalId) {
37157
37081
  return {
37158
- principal: { id: subDevice, kind: "guest", displayName },
37159
- grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37160
- resource: { ...g2.resource },
37161
- actions: [...g2.actions]
37162
- })),
37163
- ownerId: subOwner,
37164
- provider
37082
+ principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
37083
+ grants: cap.grants,
37084
+ capabilityId: cap.id,
37085
+ ...peerOwnerPrincipalId ? { peerOwnerPrincipalId } : {}
37165
37086
  };
37166
37087
  }
37167
- async function authenticate(token, deps) {
37088
+ function authenticate(token, deps) {
37168
37089
  if (!token) return { ok: false, code: "NO_TOKEN" };
37169
37090
  if (deps.isOwnerToken(token)) {
37170
37091
  return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
37171
37092
  }
37172
- if (!deps.verifyConnectToken) return { ok: false, code: "BAD_TOKEN" };
37173
- const result = deps.verifyConnectToken(token);
37174
- if (!result.ok) {
37175
- if (result.reason === "EXPIRED") return { ok: false, code: "TOKEN_EXPIRED" };
37176
- if (result.reason === "WRONG_AUDIENCE") return { ok: false, code: "TOKEN_REVOKED" };
37177
- return { ok: false, code: "BAD_TOKEN" };
37093
+ if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
37094
+ const v2 = deps.capabilityRegistry.verifyToken(token);
37095
+ if (!v2.ok) {
37096
+ if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
37097
+ return { ok: false, code: v2.code };
37178
37098
  }
37179
- return {
37180
- ok: true,
37181
- context: connectGuestContext(result.subDevice, result.subOwner, result.provider, result.displayName)
37182
- };
37099
+ return { ok: true, context: guestContext(v2.capability, deps.selfPrincipalId) };
37183
37100
  }
37184
37101
 
37185
37102
  // src/permission/capability-store.ts
@@ -37325,8 +37242,8 @@ var CapabilityManager = class {
37325
37242
  list() {
37326
37243
  return this.registry.list();
37327
37244
  }
37328
- // 调试 / per-peer cap 反查用(Phase 3 决策 #14 后无生产调用方,保留属三件套范围)。
37329
- // 返回 Capability 持久化形态;调用方需 stripSecretHash 后再上 wire。
37245
+ // whoami handler 用:根据 capabilityId 反查 caller capability 实体。
37246
+ // 返回 Capability 持久化形态;handler stripSecretHash 后再上 wire。
37330
37247
  findById(id) {
37331
37248
  return this.registry.findById(id);
37332
37249
  }
@@ -37359,14 +37276,89 @@ function cleanupGuestSessionsForCapability(cap, factory) {
37359
37276
  return { removed };
37360
37277
  }
37361
37278
 
37279
+ // src/permission/personal-capability.ts
37280
+ var import_node_fs15 = __toESM(require("fs"), 1);
37281
+ var import_node_path16 = __toESM(require("path"), 1);
37282
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
37283
+ var PERSONAL_CAP_FILE_NAME = "personal-capability.json";
37284
+ var PERSONAL_CAP_DISPLAY_NAME = "personal";
37285
+ function personalCapFilePath(dataDir) {
37286
+ return import_node_path16.default.join(dataDir, PERSONAL_CAP_FILE_NAME);
37287
+ }
37288
+ function loadOrCreatePersonalCapability(opts) {
37289
+ const file = personalCapFilePath(opts.dataDir);
37290
+ const generateToken = opts.generateToken ?? defaultGenerateToken;
37291
+ const generateId = opts.generateId ?? defaultGenerateId;
37292
+ const now = opts.now ?? Date.now;
37293
+ const existing = readFile(file);
37294
+ if (existing) return existing;
37295
+ const token = generateToken();
37296
+ const id = generateId();
37297
+ const capability = {
37298
+ id,
37299
+ secretHash: sha256Hex2(token),
37300
+ displayName: PERSONAL_CAP_DISPLAY_NAME,
37301
+ // CapabilitySchema 期待 mutable Grant[],protocol 常量是 readonly,落盘前拷一份
37302
+ grants: PERSONAL_CAP_GRANTS.map((g2) => ({
37303
+ resource: { ...g2.resource },
37304
+ actions: [...g2.actions]
37305
+ })),
37306
+ issuedAt: now(),
37307
+ usedCount: 0
37308
+ };
37309
+ const content = { token, capability };
37310
+ writeFile(file, content);
37311
+ return content;
37312
+ }
37313
+ function readFile(file) {
37314
+ let raw;
37315
+ try {
37316
+ raw = import_node_fs15.default.readFileSync(file, "utf8");
37317
+ } catch (err) {
37318
+ const code = err?.code;
37319
+ if (code === "ENOENT") return null;
37320
+ return null;
37321
+ }
37322
+ let parsed;
37323
+ try {
37324
+ parsed = JSON.parse(raw);
37325
+ } catch {
37326
+ return null;
37327
+ }
37328
+ if (!parsed || typeof parsed !== "object") return null;
37329
+ const obj = parsed;
37330
+ if (typeof obj.token !== "string" || obj.token.length === 0) return null;
37331
+ const capParsed = CapabilitySchema.safeParse(obj.capability);
37332
+ if (!capParsed.success) return null;
37333
+ if (sha256Hex2(obj.token) !== capParsed.data.secretHash) return null;
37334
+ return { token: obj.token, capability: capParsed.data };
37335
+ }
37336
+ function writeFile(file, content) {
37337
+ import_node_fs15.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
37338
+ import_node_fs15.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
37339
+ try {
37340
+ import_node_fs15.default.chmodSync(file, 384);
37341
+ } catch {
37342
+ }
37343
+ }
37344
+ function defaultGenerateToken() {
37345
+ return import_node_crypto4.default.randomBytes(24).toString("base64url");
37346
+ }
37347
+ function defaultGenerateId() {
37348
+ return "personal_" + import_node_crypto4.default.randomBytes(6).toString("base64url");
37349
+ }
37350
+ function sha256Hex2(s) {
37351
+ return import_node_crypto4.default.createHash("sha256").update(s).digest("hex");
37352
+ }
37353
+
37362
37354
  // src/inbox/inbox-store.ts
37363
- var fs19 = __toESM(require("fs"), 1);
37364
- var path21 = __toESM(require("path"), 1);
37355
+ var fs20 = __toESM(require("fs"), 1);
37356
+ var path22 = __toESM(require("path"), 1);
37365
37357
  var INBOX_SUBDIR = "inbox";
37366
37358
  var InboxStore = class {
37367
37359
  constructor(dataDir) {
37368
37360
  this.dataDir = dataDir;
37369
- fs19.mkdirSync(this.dirPath(), { recursive: true });
37361
+ fs20.mkdirSync(this.dirPath(), { recursive: true });
37370
37362
  }
37371
37363
  dataDir;
37372
37364
  /**
@@ -37374,11 +37366,11 @@ var InboxStore = class {
37374
37366
  * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
37375
37367
  * 文件不存在时返回 [] (不抛, 不创建).
37376
37368
  */
37377
- list(peerDeviceId, sinceCreatedAt) {
37378
- const file = this.filePath(peerDeviceId);
37369
+ list(peerOwnerId, sinceCreatedAt) {
37370
+ const file = this.filePath(peerOwnerId);
37379
37371
  let raw;
37380
37372
  try {
37381
- raw = fs19.readFileSync(file, "utf8");
37373
+ raw = fs20.readFileSync(file, "utf8");
37382
37374
  } catch (err) {
37383
37375
  if (err?.code === "ENOENT") return [];
37384
37376
  return [];
@@ -37388,13 +37380,13 @@ var InboxStore = class {
37388
37380
  return all.filter((m2) => m2.createdAt > sinceCreatedAt);
37389
37381
  }
37390
37382
  /**
37391
- * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
37383
+ * 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
37392
37384
  */
37393
- listAllPeerDeviceIds() {
37385
+ listAllPeerOwnerIds() {
37394
37386
  const dir = this.dirPath();
37395
37387
  let entries;
37396
37388
  try {
37397
- entries = fs19.readdirSync(dir);
37389
+ entries = fs20.readdirSync(dir);
37398
37390
  } catch (err) {
37399
37391
  if (err?.code === "ENOENT") return [];
37400
37392
  return [];
@@ -37406,63 +37398,63 @@ var InboxStore = class {
37406
37398
  * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
37407
37399
  */
37408
37400
  append(message) {
37409
- const existing = this.list(message.peerDeviceId);
37401
+ const existing = this.list(message.peerOwnerId);
37410
37402
  if (existing.some((m2) => m2.id === message.id)) return;
37411
- const file = this.filePath(message.peerDeviceId);
37403
+ const file = this.filePath(message.peerOwnerId);
37412
37404
  const line = JSON.stringify(message) + "\n";
37413
- fs19.appendFileSync(file, line, { mode: 384 });
37405
+ fs20.appendFileSync(file, line, { mode: 384 });
37414
37406
  try {
37415
- fs19.chmodSync(file, 384);
37407
+ fs20.chmodSync(file, 384);
37416
37408
  } catch {
37417
37409
  }
37418
37410
  }
37419
37411
  /**
37420
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
37421
- * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37412
+ * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
37413
+ * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
37422
37414
  * 返写入了几条.
37423
37415
  */
37424
- markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
37425
- const messages = this.list(peerDeviceId);
37416
+ markRead(peerOwnerId, principalId, upToCreatedAt) {
37417
+ const messages = this.list(peerOwnerId);
37426
37418
  let changed = 0;
37427
37419
  const next = messages.map((m2) => {
37428
- if (m2.senderDeviceId === readerDeviceId) return m2;
37420
+ if (m2.senderPrincipalId === principalId) return m2;
37429
37421
  if (m2.createdAt > upToCreatedAt) return m2;
37430
- const cur = m2.readBy[readerDeviceId];
37422
+ const cur = m2.readBy[principalId];
37431
37423
  if (cur !== void 0 && cur >= upToCreatedAt) return m2;
37432
37424
  changed++;
37433
- return { ...m2, readBy: { ...m2.readBy, [readerDeviceId]: upToCreatedAt } };
37425
+ return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
37434
37426
  });
37435
37427
  if (changed === 0) return 0;
37436
- this.rewriteFile(peerDeviceId, next);
37428
+ this.rewriteFile(peerOwnerId, next);
37437
37429
  return changed;
37438
37430
  }
37439
37431
  /**
37440
37432
  * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
37441
37433
  */
37442
- removeByPeerDeviceId(peerDeviceId) {
37443
- const file = this.filePath(peerDeviceId);
37434
+ removeByPeerOwnerId(peerOwnerId) {
37435
+ const file = this.filePath(peerOwnerId);
37444
37436
  try {
37445
- fs19.unlinkSync(file);
37437
+ fs20.unlinkSync(file);
37446
37438
  } catch (err) {
37447
37439
  if (err?.code === "ENOENT") return;
37448
37440
  }
37449
37441
  }
37450
- rewriteFile(peerDeviceId, messages) {
37451
- const file = this.filePath(peerDeviceId);
37442
+ rewriteFile(peerOwnerId, messages) {
37443
+ const file = this.filePath(peerOwnerId);
37452
37444
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37453
37445
  const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
37454
- fs19.writeFileSync(tmp, content, { mode: 384 });
37455
- fs19.renameSync(tmp, file);
37446
+ fs20.writeFileSync(tmp, content, { mode: 384 });
37447
+ fs20.renameSync(tmp, file);
37456
37448
  try {
37457
- fs19.chmodSync(file, 384);
37449
+ fs20.chmodSync(file, 384);
37458
37450
  } catch {
37459
37451
  }
37460
37452
  }
37461
37453
  dirPath() {
37462
- return path21.join(this.dataDir, INBOX_SUBDIR);
37454
+ return path22.join(this.dataDir, INBOX_SUBDIR);
37463
37455
  }
37464
- filePath(peerDeviceId) {
37465
- return path21.join(this.dirPath(), `${peerDeviceId}.jsonl`);
37456
+ filePath(peerOwnerId) {
37457
+ return path22.join(this.dirPath(), `${peerOwnerId}.jsonl`);
37466
37458
  }
37467
37459
  };
37468
37460
  function parseAllLines(raw) {
@@ -37498,72 +37490,72 @@ var InboxManager = class {
37498
37490
  * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
37499
37491
  */
37500
37492
  postMessage(args) {
37501
- const beforeLen = this.store.list(args.peerDeviceId).length;
37493
+ const beforeLen = this.store.list(args.peerOwnerId).length;
37502
37494
  const msg = {
37503
37495
  id: args.id,
37504
- peerDeviceId: args.peerDeviceId,
37505
- senderDeviceId: args.senderDeviceId,
37496
+ peerOwnerId: args.peerOwnerId,
37497
+ senderPrincipalId: args.senderPrincipalId,
37506
37498
  text: args.text,
37507
37499
  createdAt: args.createdAt,
37508
37500
  readBy: {}
37509
37501
  };
37510
37502
  this.store.append(msg);
37511
- const afterLen = this.store.list(args.peerDeviceId).length;
37503
+ const afterLen = this.store.list(args.peerOwnerId).length;
37512
37504
  if (afterLen === beforeLen) return msg;
37513
- this.broadcast(args.peerDeviceId, {
37505
+ this.broadcast(args.peerOwnerId, {
37514
37506
  type: "inbox:event",
37515
- peerDeviceId: args.peerDeviceId,
37507
+ peerOwnerId: args.peerOwnerId,
37516
37508
  message: msg
37517
37509
  });
37518
37510
  return msg;
37519
37511
  }
37520
- list(peerDeviceId, sinceCreatedAt) {
37521
- return this.store.list(peerDeviceId, sinceCreatedAt);
37512
+ list(peerOwnerId, sinceCreatedAt) {
37513
+ return this.store.list(peerOwnerId, sinceCreatedAt);
37522
37514
  }
37523
37515
  /**
37524
37516
  * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
37525
37517
  * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
37526
37518
  */
37527
37519
  markRead(args) {
37528
- const before = this.store.list(args.peerDeviceId);
37529
- const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
37520
+ const before = this.store.list(args.peerOwnerId);
37521
+ const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
37530
37522
  if (changed === 0) return 0;
37531
- const after = this.store.list(args.peerDeviceId);
37523
+ const after = this.store.list(args.peerOwnerId);
37532
37524
  const beforeById = new Map(before.map((m2) => [m2.id, m2]));
37533
37525
  for (const m2 of after) {
37534
37526
  const prev = beforeById.get(m2.id);
37535
37527
  if (!prev) continue;
37536
- if (prev.readBy[args.readerDeviceId] === m2.readBy[args.readerDeviceId]) continue;
37537
- this.broadcast(args.peerDeviceId, {
37528
+ if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
37529
+ this.broadcast(args.peerOwnerId, {
37538
37530
  type: "inbox:event",
37539
- peerDeviceId: args.peerDeviceId,
37531
+ peerOwnerId: args.peerOwnerId,
37540
37532
  message: m2
37541
37533
  });
37542
37534
  }
37543
37535
  return changed;
37544
37536
  }
37545
37537
  /** peer:remove cascade. 删整个 peer 的 jsonl. */
37546
- removeChannel(peerDeviceId) {
37547
- this.store.removeByPeerDeviceId(peerDeviceId);
37538
+ removeChannel(peerOwnerId) {
37539
+ this.store.removeByPeerOwnerId(peerOwnerId);
37548
37540
  }
37549
37541
  };
37550
37542
 
37551
- // src/state/contact-store.ts
37552
- var fs20 = __toESM(require("fs"), 1);
37553
- var path22 = __toESM(require("path"), 1);
37554
- var FILE_NAME = "contacts.json";
37555
- var ContactStore = class {
37543
+ // src/state/received-capability-store.ts
37544
+ var fs21 = __toESM(require("fs"), 1);
37545
+ var path23 = __toESM(require("path"), 1);
37546
+ var FILE_NAME = "received-capabilities.json";
37547
+ var ReceivedCapabilityStore = class {
37556
37548
  constructor(dataDir) {
37557
37549
  this.dataDir = dataDir;
37558
37550
  }
37559
37551
  dataDir;
37560
- contacts = /* @__PURE__ */ new Map();
37552
+ caps = /* @__PURE__ */ new Map();
37561
37553
  load() {
37562
- this.contacts.clear();
37563
- const file = path22.join(this.dataDir, FILE_NAME);
37554
+ this.caps.clear();
37555
+ const file = path23.join(this.dataDir, FILE_NAME);
37564
37556
  let raw;
37565
37557
  try {
37566
- raw = fs20.readFileSync(file, "utf8");
37558
+ raw = fs21.readFileSync(file, "utf8");
37567
37559
  } catch (err) {
37568
37560
  if (err?.code !== "ENOENT") this.renameBak(file);
37569
37561
  return;
@@ -37575,58 +37567,52 @@ var ContactStore = class {
37575
37567
  this.renameBak(file);
37576
37568
  return;
37577
37569
  }
37578
- const arr = parsed?.contacts;
37570
+ const arr = parsed?.receivedCaps;
37579
37571
  if (!Array.isArray(arr)) return;
37580
37572
  for (const entry of arr) {
37581
- const r = ContactSchema.safeParse(entry);
37582
- if (r.success) this.contacts.set(r.data.deviceId, r.data);
37573
+ const r = ReceivedCapabilitySchema.safeParse(entry);
37574
+ if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
37583
37575
  }
37584
37576
  }
37585
37577
  list() {
37586
- return Array.from(this.contacts.values());
37587
- }
37588
- get(deviceId) {
37589
- return this.contacts.get(deviceId) ?? null;
37578
+ return Array.from(this.caps.values());
37590
37579
  }
37591
- has(deviceId) {
37592
- return this.contacts.has(deviceId);
37580
+ get(ownerPrincipalId) {
37581
+ return this.caps.get(ownerPrincipalId) ?? null;
37593
37582
  }
37594
- /** 某个人(ownerId, provider)的所有设备记录——人级视图(显示/分组用,不参与路由) */
37595
- findByOwner(ownerId, provider) {
37596
- return this.list().filter((c) => c.ownerId === ownerId && c.provider === provider);
37583
+ hasOwner(ownerPrincipalId) {
37584
+ return this.caps.has(ownerPrincipalId);
37597
37585
  }
37598
- upsert(contact) {
37599
- this.contacts.set(contact.deviceId, contact);
37586
+ upsert(cap) {
37587
+ this.caps.set(cap.ownerPrincipalId, cap);
37600
37588
  this.flush();
37601
37589
  }
37602
- /** 删单台设备(contact:remove 语义,决策 #16):删该 deviceId 一条,返回是否删到 */
37603
- removeByDeviceId(deviceId) {
37604
- const existed = this.contacts.delete(deviceId);
37605
- if (existed) this.flush();
37606
- return existed;
37590
+ remove(ownerPrincipalId) {
37591
+ if (!this.caps.delete(ownerPrincipalId)) return;
37592
+ this.flush();
37607
37593
  }
37608
37594
  flush() {
37609
- const file = path22.join(this.dataDir, FILE_NAME);
37595
+ const file = path23.join(this.dataDir, FILE_NAME);
37610
37596
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37611
37597
  const content = JSON.stringify(
37612
- { contacts: Array.from(this.contacts.values()) },
37598
+ { receivedCaps: Array.from(this.caps.values()) },
37613
37599
  null,
37614
37600
  2
37615
37601
  );
37616
- fs20.mkdirSync(this.dataDir, { recursive: true });
37617
- fs20.writeFileSync(tmp, content, { mode: 384 });
37618
- fs20.renameSync(tmp, file);
37602
+ fs21.mkdirSync(this.dataDir, { recursive: true });
37603
+ fs21.writeFileSync(tmp, content, { mode: 384 });
37604
+ fs21.renameSync(tmp, file);
37619
37605
  }
37620
37606
  renameBak(file) {
37621
37607
  try {
37622
- fs20.renameSync(file, `${file}.bak`);
37608
+ fs21.renameSync(file, `${file}.bak`);
37623
37609
  } catch {
37624
37610
  }
37625
37611
  }
37626
37612
  };
37627
37613
 
37628
- // src/contact/connect-remote.ts
37629
- var crypto5 = __toESM(require("crypto"), 1);
37614
+ // src/received-capability/connect-remote.ts
37615
+ var crypto6 = __toESM(require("crypto"), 1);
37630
37616
  var HANDSHAKE_TIMEOUT_MS = 5e3;
37631
37617
  var RPC_TIMEOUT_MS = 5e3;
37632
37618
  async function connectRemote(args) {
@@ -37643,11 +37629,8 @@ async function connectRemote(args) {
37643
37629
  JSON.stringify({
37644
37630
  type: "auth",
37645
37631
  token: args.token,
37646
- // 自动反向(spec 决策 #11):带本机可达地址时对方反向建联系人;
37647
- // 缺省时省略字段(不发空串),对方 schema selfUrl 是 .min(1).optional()。
37648
- // 决策 #16:身份字段(selfPrincipalId/selfDisplayName/selfDeviceId)已删——
37649
- // 对方从 connect token 签名背书取 deviceId/ownerId/provider,不信自报。
37650
- ...args.selfUrl ? { selfUrl: args.selfUrl } : {}
37632
+ selfPrincipalId: args.selfPrincipalId,
37633
+ selfDisplayName: args.selfDisplayName
37651
37634
  })
37652
37635
  );
37653
37636
  await new Promise((resolve6, reject) => {
@@ -37658,7 +37641,7 @@ async function connectRemote(args) {
37658
37641
  } catch {
37659
37642
  return;
37660
37643
  }
37661
- if (f.type === "auth:ok") {
37644
+ if (f.type === "auth:ok" || f.type === "ready") {
37662
37645
  ws.off("message", onMessage);
37663
37646
  resolve6();
37664
37647
  return;
@@ -37675,7 +37658,7 @@ async function connectRemote(args) {
37675
37658
  }, HANDSHAKE_TIMEOUT_MS);
37676
37659
  });
37677
37660
  function call(method, payload) {
37678
- const requestId = crypto5.randomUUID();
37661
+ const requestId = crypto6.randomUUID();
37679
37662
  return new Promise((resolve6, reject) => {
37680
37663
  const onMessage = (raw) => {
37681
37664
  let f;
@@ -37704,9 +37687,9 @@ async function connectRemote(args) {
37704
37687
  whoami: async () => {
37705
37688
  const f = await call("whoami", {});
37706
37689
  return {
37707
- ownerId: f.owner.ownerId,
37708
- provider: f.owner.provider,
37690
+ ownerId: f.owner.id,
37709
37691
  displayName: f.owner.displayName,
37692
+ capabilityId: f.capability.id,
37710
37693
  grants: f.capability.grants
37711
37694
  };
37712
37695
  },
@@ -37719,96 +37702,62 @@ async function connectRemote(args) {
37719
37702
  };
37720
37703
  }
37721
37704
 
37722
- // src/contact/auto-reverse.ts
37723
- init_protocol();
37724
- async function autoReverseContact(args) {
37725
- if (!args.selfUrl) return;
37726
- const now = args.now ?? Date.now;
37727
- const grants = PERSONAL_CAP_GRANTS.map((g2) => ({
37728
- resource: { ...g2.resource },
37729
- actions: [...g2.actions]
37730
- }));
37731
- const base = {
37732
- deviceId: args.deviceId,
37733
- ownerId: args.ownerId,
37734
- provider: args.provider,
37735
- displayName: args.displayName,
37736
- remoteUrl: args.selfUrl,
37737
- connectToken: "",
37738
- grants,
37739
- addedAt: now()
37740
- };
37741
- args.store.upsert(base);
37742
- args.broadcast({ type: "contact:added", contact: base });
37743
- if (!args.exchangeToken) return;
37744
- let token;
37745
- try {
37746
- token = await args.exchangeToken(args.deviceId);
37747
- } catch {
37748
- return;
37749
- }
37750
- if (!token) return;
37751
- const upgraded = { ...base, connectToken: token };
37752
- args.store.upsert(upgraded);
37753
- args.broadcast({ type: "contact:added", contact: upgraded });
37754
- }
37755
-
37756
37705
  // src/migrations/2026-05-20-flatten-sessions.ts
37757
- var fs21 = __toESM(require("fs"), 1);
37758
- var path23 = __toESM(require("path"), 1);
37706
+ var fs22 = __toESM(require("fs"), 1);
37707
+ var path24 = __toESM(require("path"), 1);
37759
37708
  var MIGRATION_FLAG_NAME = ".migration.v1.done";
37760
37709
  function migrateFlattenSessions(opts) {
37761
37710
  const dataDir = opts.dataDir;
37762
37711
  const now = opts.now ?? Date.now;
37763
- const sessionsDir = path23.join(dataDir, "sessions");
37764
- const flagPath = path23.join(sessionsDir, MIGRATION_FLAG_NAME);
37712
+ const sessionsDir = path24.join(dataDir, "sessions");
37713
+ const flagPath = path24.join(sessionsDir, MIGRATION_FLAG_NAME);
37765
37714
  if (existsSync3(flagPath)) {
37766
37715
  return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
37767
37716
  }
37768
37717
  let movedBare = 0;
37769
37718
  let movedVmOwner = 0;
37770
37719
  let archivedListener = 0;
37771
- const defaultDir = path23.join(sessionsDir, "default");
37720
+ const defaultDir = path24.join(sessionsDir, "default");
37772
37721
  if (existsSync3(defaultDir)) {
37773
37722
  for (const entry of readdirSafe(defaultDir)) {
37774
37723
  if (!entry.endsWith(".json")) continue;
37775
- const src = path23.join(defaultDir, entry);
37776
- const dst = path23.join(sessionsDir, entry);
37777
- fs21.renameSync(src, dst);
37724
+ const src = path24.join(defaultDir, entry);
37725
+ const dst = path24.join(sessionsDir, entry);
37726
+ fs22.renameSync(src, dst);
37778
37727
  movedBare += 1;
37779
37728
  }
37780
37729
  rmdirIfEmpty(defaultDir);
37781
37730
  }
37782
37731
  for (const pid of readdirSafe(sessionsDir)) {
37783
- const personaDir = path23.join(sessionsDir, pid);
37732
+ const personaDir = path24.join(sessionsDir, pid);
37784
37733
  if (!isDir(personaDir)) continue;
37785
37734
  if (pid === "default") continue;
37786
- const ownerSrc = path23.join(personaDir, "owner");
37735
+ const ownerSrc = path24.join(personaDir, "owner");
37787
37736
  if (existsSync3(ownerSrc) && isDir(ownerSrc)) {
37788
- const ownerDst = path23.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37789
- fs21.mkdirSync(ownerDst, { recursive: true });
37737
+ const ownerDst = path24.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
37738
+ fs22.mkdirSync(ownerDst, { recursive: true });
37790
37739
  for (const file of readdirSafe(ownerSrc)) {
37791
37740
  if (!file.endsWith(".json")) continue;
37792
- fs21.renameSync(path23.join(ownerSrc, file), path23.join(ownerDst, file));
37741
+ fs22.renameSync(path24.join(ownerSrc, file), path24.join(ownerDst, file));
37793
37742
  movedVmOwner += 1;
37794
37743
  }
37795
37744
  rmdirIfEmpty(ownerSrc);
37796
37745
  }
37797
- const listenerSrc = path23.join(personaDir, "listener");
37746
+ const listenerSrc = path24.join(personaDir, "listener");
37798
37747
  if (existsSync3(listenerSrc) && isDir(listenerSrc)) {
37799
- const archiveDst = path23.join(dataDir, ".legacy", `listener-${pid}`);
37800
- fs21.mkdirSync(archiveDst, { recursive: true });
37748
+ const archiveDst = path24.join(dataDir, ".legacy", `listener-${pid}`);
37749
+ fs22.mkdirSync(archiveDst, { recursive: true });
37801
37750
  for (const file of readdirSafe(listenerSrc)) {
37802
37751
  if (!file.endsWith(".json")) continue;
37803
- fs21.renameSync(path23.join(listenerSrc, file), path23.join(archiveDst, file));
37752
+ fs22.renameSync(path24.join(listenerSrc, file), path24.join(archiveDst, file));
37804
37753
  archivedListener += 1;
37805
37754
  }
37806
37755
  rmdirIfEmpty(listenerSrc);
37807
37756
  }
37808
37757
  rmdirIfEmpty(personaDir);
37809
37758
  }
37810
- fs21.mkdirSync(sessionsDir, { recursive: true });
37811
- fs21.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37759
+ fs22.mkdirSync(sessionsDir, { recursive: true });
37760
+ fs22.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
37812
37761
  return {
37813
37762
  skipped: false,
37814
37763
  flagWritten: true,
@@ -37819,7 +37768,7 @@ function migrateFlattenSessions(opts) {
37819
37768
  }
37820
37769
  function existsSync3(p2) {
37821
37770
  try {
37822
- fs21.statSync(p2);
37771
+ fs22.statSync(p2);
37823
37772
  return true;
37824
37773
  } catch {
37825
37774
  return false;
@@ -37827,31 +37776,31 @@ function existsSync3(p2) {
37827
37776
  }
37828
37777
  function isDir(p2) {
37829
37778
  try {
37830
- return fs21.statSync(p2).isDirectory();
37779
+ return fs22.statSync(p2).isDirectory();
37831
37780
  } catch {
37832
37781
  return false;
37833
37782
  }
37834
37783
  }
37835
37784
  function readdirSafe(p2) {
37836
37785
  try {
37837
- return fs21.readdirSync(p2);
37786
+ return fs22.readdirSync(p2);
37838
37787
  } catch {
37839
37788
  return [];
37840
37789
  }
37841
37790
  }
37842
37791
  function rmdirIfEmpty(p2) {
37843
37792
  try {
37844
- fs21.rmdirSync(p2);
37793
+ fs22.rmdirSync(p2);
37845
37794
  } catch {
37846
37795
  }
37847
37796
  }
37848
37797
 
37849
37798
  // src/transport/http-router.ts
37850
- var import_node_fs16 = __toESM(require("fs"), 1);
37851
- var import_node_path19 = __toESM(require("path"), 1);
37799
+ var import_node_fs17 = __toESM(require("fs"), 1);
37800
+ var import_node_path20 = __toESM(require("path"), 1);
37852
37801
 
37853
37802
  // src/attachment/mime.ts
37854
- var import_node_path16 = __toESM(require("path"), 1);
37803
+ var import_node_path17 = __toESM(require("path"), 1);
37855
37804
  var TEXT_PLAIN = "text/plain; charset=utf-8";
37856
37805
  var EXT_TO_NATIVE_MIME = {
37857
37806
  // 图片
@@ -37958,14 +37907,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
37958
37907
  ".mk"
37959
37908
  ]);
37960
37909
  function lookupMime(filePathOrName) {
37961
- const ext = import_node_path16.default.extname(filePathOrName).toLowerCase();
37910
+ const ext = import_node_path17.default.extname(filePathOrName).toLowerCase();
37962
37911
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
37963
37912
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
37964
37913
  return "application/octet-stream";
37965
37914
  }
37966
37915
 
37967
37916
  // src/attachment/sign-url.ts
37968
- var import_node_crypto4 = __toESM(require("crypto"), 1);
37917
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
37969
37918
  var HMAC_ALGO = "sha256";
37970
37919
  function base64urlEncode(buf) {
37971
37920
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -37982,7 +37931,7 @@ function decodeAbsPathFromUrl(encoded) {
37982
37931
  }
37983
37932
  function computeSig(secret, absPath, e) {
37984
37933
  const msg = e === null ? absPath : `${absPath}|${e}`;
37985
- return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37934
+ return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
37986
37935
  }
37987
37936
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
37988
37937
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -38017,7 +37966,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38017
37966
  if (provided.length !== expected.length) {
38018
37967
  return { ok: false, code: "BAD_SIG" };
38019
37968
  }
38020
- if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
37969
+ if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
38021
37970
  return { ok: false, code: "BAD_SIG" };
38022
37971
  }
38023
37972
  if (e !== null && now() / 1e3 > e) {
@@ -38027,9 +37976,9 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
38027
37976
  }
38028
37977
 
38029
37978
  // src/attachment/upload.ts
38030
- var import_node_fs15 = __toESM(require("fs"), 1);
38031
- var import_node_path17 = __toESM(require("path"), 1);
38032
- var import_node_crypto5 = __toESM(require("crypto"), 1);
37979
+ var import_node_fs16 = __toESM(require("fs"), 1);
37980
+ var import_node_path18 = __toESM(require("path"), 1);
37981
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
38033
37982
  var import_promises = require("stream/promises");
38034
37983
  var UploadError = class extends Error {
38035
37984
  constructor(code, message) {
@@ -38040,24 +37989,24 @@ var UploadError = class extends Error {
38040
37989
  code;
38041
37990
  };
38042
37991
  function assertValidFileName(fileName) {
38043
- if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path17.default.basename(fileName)) {
37992
+ if (fileName.length === 0 || fileName === "." || fileName === ".." || fileName.startsWith(".") || fileName.includes("/") || fileName.includes("\\") || fileName !== import_node_path18.default.basename(fileName)) {
38044
37993
  throw new UploadError("INVALID_FILENAME", `fileName must be a plain basename, got: ${fileName}`);
38045
37994
  }
38046
37995
  }
38047
37996
  var HASH_PREFIX_LEN = 16;
38048
37997
  async function writeUploadedAttachment(args) {
38049
37998
  assertValidFileName(args.fileName);
38050
- const attachmentsRoot = import_node_path17.default.join(args.sessionDir, ".attachments");
37999
+ const attachmentsRoot = import_node_path18.default.join(args.sessionDir, ".attachments");
38051
38000
  try {
38052
- import_node_fs15.default.mkdirSync(attachmentsRoot, { recursive: true });
38001
+ import_node_fs16.default.mkdirSync(attachmentsRoot, { recursive: true });
38053
38002
  } catch (err) {
38054
38003
  throw new UploadError("STORAGE_ERROR", `mkdir failed: ${err.message}`);
38055
38004
  }
38056
- const hasher = import_node_crypto5.default.createHash("sha256");
38005
+ const hasher = import_node_crypto6.default.createHash("sha256");
38057
38006
  let actualSize = 0;
38058
- const tmpPath = import_node_path17.default.join(
38007
+ const tmpPath = import_node_path18.default.join(
38059
38008
  attachmentsRoot,
38060
- `.upload-${process.pid}-${Date.now()}-${import_node_crypto5.default.randomBytes(4).toString("hex")}`
38009
+ `.upload-${process.pid}-${Date.now()}-${import_node_crypto6.default.randomBytes(4).toString("hex")}`
38061
38010
  );
38062
38011
  try {
38063
38012
  await (0, import_promises.pipeline)(
@@ -38070,18 +38019,18 @@ async function writeUploadedAttachment(args) {
38070
38019
  yield buf;
38071
38020
  }
38072
38021
  },
38073
- import_node_fs15.default.createWriteStream(tmpPath, { mode: 384 })
38022
+ import_node_fs16.default.createWriteStream(tmpPath, { mode: 384 })
38074
38023
  );
38075
38024
  } catch (err) {
38076
38025
  try {
38077
- import_node_fs15.default.unlinkSync(tmpPath);
38026
+ import_node_fs16.default.unlinkSync(tmpPath);
38078
38027
  } catch {
38079
38028
  }
38080
38029
  throw new UploadError("STORAGE_ERROR", `write failed: ${err.message}`);
38081
38030
  }
38082
38031
  if (actualSize !== args.contentLength) {
38083
38032
  try {
38084
- import_node_fs15.default.unlinkSync(tmpPath);
38033
+ import_node_fs16.default.unlinkSync(tmpPath);
38085
38034
  } catch {
38086
38035
  }
38087
38036
  throw new UploadError(
@@ -38090,35 +38039,35 @@ async function writeUploadedAttachment(args) {
38090
38039
  );
38091
38040
  }
38092
38041
  const attachmentId = hasher.digest("hex").slice(0, HASH_PREFIX_LEN);
38093
- const hashDir = import_node_path17.default.join(attachmentsRoot, attachmentId);
38042
+ const hashDir = import_node_path18.default.join(attachmentsRoot, attachmentId);
38094
38043
  let finalFileName;
38095
38044
  let hashDirExists = false;
38096
38045
  try {
38097
- hashDirExists = import_node_fs15.default.statSync(hashDir).isDirectory();
38046
+ hashDirExists = import_node_fs16.default.statSync(hashDir).isDirectory();
38098
38047
  } catch {
38099
38048
  }
38100
38049
  if (hashDirExists) {
38101
- const existing = import_node_fs15.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38050
+ const existing = import_node_fs16.default.readdirSync(hashDir).filter((n) => !n.startsWith("."));
38102
38051
  finalFileName = existing[0] ?? args.fileName;
38103
38052
  try {
38104
- import_node_fs15.default.unlinkSync(tmpPath);
38053
+ import_node_fs16.default.unlinkSync(tmpPath);
38105
38054
  } catch {
38106
38055
  }
38107
38056
  } else {
38108
38057
  try {
38109
- import_node_fs15.default.mkdirSync(hashDir, { recursive: true });
38058
+ import_node_fs16.default.mkdirSync(hashDir, { recursive: true });
38110
38059
  finalFileName = args.fileName;
38111
- import_node_fs15.default.renameSync(tmpPath, import_node_path17.default.join(hashDir, finalFileName));
38060
+ import_node_fs16.default.renameSync(tmpPath, import_node_path18.default.join(hashDir, finalFileName));
38112
38061
  } catch (err) {
38113
38062
  try {
38114
- import_node_fs15.default.unlinkSync(tmpPath);
38063
+ import_node_fs16.default.unlinkSync(tmpPath);
38115
38064
  } catch {
38116
38065
  }
38117
38066
  throw new UploadError("STORAGE_ERROR", `rename failed: ${err.message}`);
38118
38067
  }
38119
38068
  }
38120
- const absPath = import_node_path17.default.join(hashDir, finalFileName);
38121
- const relPath = import_node_path17.default.relative(args.sessionDir, absPath);
38069
+ const absPath = import_node_path18.default.join(hashDir, finalFileName);
38070
+ const relPath = import_node_path18.default.relative(args.sessionDir, absPath);
38122
38071
  args.groupFileStore.upsert(args.scope, args.sessionId, {
38123
38072
  relPath: absPath,
38124
38073
  // 存绝对路径,与现有 agent 入清单的形态一致(attachment.ts:144 双形态兼容)
@@ -38132,7 +38081,7 @@ async function writeUploadedAttachment(args) {
38132
38081
 
38133
38082
  // src/extension/import.ts
38134
38083
  var import_promises2 = __toESM(require("fs/promises"), 1);
38135
- var import_node_path18 = __toESM(require("path"), 1);
38084
+ var import_node_path19 = __toESM(require("path"), 1);
38136
38085
  var import_node_os10 = __toESM(require("os"), 1);
38137
38086
  var import_jszip = __toESM(require_lib3(), 1);
38138
38087
  var ImportError = class extends Error {
@@ -38150,7 +38099,7 @@ async function importZip(buf, root) {
38150
38099
  throw new ImportError("ZIP_INVALID", `failed to load zip: ${e.message}`);
38151
38100
  }
38152
38101
  for (const name of Object.keys(zip.files)) {
38153
- if (name.includes("..") || name.startsWith("/") || import_node_path18.default.isAbsolute(name)) {
38102
+ if (name.includes("..") || name.startsWith("/") || import_node_path19.default.isAbsolute(name)) {
38154
38103
  throw new ImportError("ZIP_INVALID", `unsafe zip entry path: ${name}`);
38155
38104
  }
38156
38105
  }
@@ -38183,7 +38132,7 @@ async function importZip(buf, root) {
38183
38132
  );
38184
38133
  }
38185
38134
  }
38186
- const destDir = import_node_path18.default.join(root, manifest.id);
38135
+ const destDir = import_node_path19.default.join(root, manifest.id);
38187
38136
  let destExists = false;
38188
38137
  try {
38189
38138
  await import_promises2.default.access(destDir);
@@ -38193,15 +38142,15 @@ async function importZip(buf, root) {
38193
38142
  if (destExists) {
38194
38143
  throw new ImportError("ALREADY_EXISTS", `extension ${manifest.id} already installed`);
38195
38144
  }
38196
- const stage = await import_promises2.default.mkdtemp(import_node_path18.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38145
+ const stage = await import_promises2.default.mkdtemp(import_node_path19.default.join(import_node_os10.default.tmpdir(), `clawd-ext-stage-${manifest.id}-`));
38197
38146
  try {
38198
38147
  for (const [name, entry] of Object.entries(zip.files)) {
38199
- const dest = import_node_path18.default.join(stage, name);
38148
+ const dest = import_node_path19.default.join(stage, name);
38200
38149
  if (entry.dir) {
38201
38150
  await import_promises2.default.mkdir(dest, { recursive: true });
38202
38151
  continue;
38203
38152
  }
38204
- await import_promises2.default.mkdir(import_node_path18.default.dirname(dest), { recursive: true });
38153
+ await import_promises2.default.mkdir(import_node_path19.default.dirname(dest), { recursive: true });
38205
38154
  const content = await entry.async("nodebuffer");
38206
38155
  await import_promises2.default.writeFile(dest, content);
38207
38156
  }
@@ -38315,7 +38264,7 @@ function isValidUploadFileName(fileName) {
38315
38264
  if (fileName === "." || fileName === "..") return false;
38316
38265
  if (fileName.startsWith(".")) return false;
38317
38266
  if (fileName.includes("/") || fileName.includes("\\")) return false;
38318
- return fileName === import_node_path19.default.basename(fileName);
38267
+ return fileName === import_node_path20.default.basename(fileName);
38319
38268
  }
38320
38269
  function createHttpRouter(deps) {
38321
38270
  return async (req, res) => {
@@ -38330,25 +38279,6 @@ function createHttpRouter(deps) {
38330
38279
  });
38331
38280
  return true;
38332
38281
  }
38333
- if (url.pathname === "/auth/callback") {
38334
- if (req.method !== "GET") {
38335
- sendJson(res, 404, { code: "NOT_FOUND", message: `no route for ${req.method} ${url.pathname}` });
38336
- return true;
38337
- }
38338
- if (!deps.feishuLogin) {
38339
- sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "feishu login not wired" });
38340
- return true;
38341
- }
38342
- const result = await deps.feishuLogin.handleLoginCallback({
38343
- token: url.searchParams.get("token") ?? "",
38344
- state: url.searchParams.get("state") ?? "",
38345
- expiresAt: url.searchParams.get("expires_at")
38346
- });
38347
- 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>`;
38348
- res.writeHead(result.ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
38349
- res.end(html);
38350
- return true;
38351
- }
38352
38282
  if (url.pathname.startsWith(RPC_ROUTE_PREFIX)) {
38353
38283
  if (!deps.rpcDispatcher) {
38354
38284
  sendJson(res, 501, { code: "NOT_IMPLEMENTED", message: "HTTP RPC adapter not wired" });
@@ -38544,7 +38474,7 @@ function createHttpRouter(deps) {
38544
38474
  return true;
38545
38475
  }
38546
38476
  let absPath;
38547
- if (import_node_path19.default.isAbsolute(pathParam)) {
38477
+ if (import_node_path20.default.isAbsolute(pathParam)) {
38548
38478
  absPath = pathParam;
38549
38479
  } else if (deps.sessionStore) {
38550
38480
  const file = deps.sessionStore.read(sid);
@@ -38552,7 +38482,7 @@ function createHttpRouter(deps) {
38552
38482
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
38553
38483
  return true;
38554
38484
  }
38555
- absPath = import_node_path19.default.join(file.cwd, pathParam);
38485
+ absPath = import_node_path20.default.join(file.cwd, pathParam);
38556
38486
  } else {
38557
38487
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
38558
38488
  return true;
@@ -38619,9 +38549,6 @@ function parseUrl(rawUrl) {
38619
38549
  return null;
38620
38550
  }
38621
38551
  }
38622
- function escapeHtml(s) {
38623
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
38624
- }
38625
38552
  function sendJson(res, status, body, extraHeaders) {
38626
38553
  res.writeHead(status, {
38627
38554
  "Content-Type": "application/json; charset=utf-8",
@@ -38635,7 +38562,7 @@ function withCtx(ctx, body) {
38635
38562
  function streamFile(res, absPath, logger) {
38636
38563
  let stat;
38637
38564
  try {
38638
- stat = import_node_fs16.default.statSync(absPath);
38565
+ stat = import_node_fs17.default.statSync(absPath);
38639
38566
  } catch (err) {
38640
38567
  const code = err?.code;
38641
38568
  if (code === "ENOENT") {
@@ -38650,7 +38577,7 @@ function streamFile(res, absPath, logger) {
38650
38577
  return;
38651
38578
  }
38652
38579
  const mime = lookupMime(absPath);
38653
- const basename = import_node_path19.default.basename(absPath);
38580
+ const basename = import_node_path20.default.basename(absPath);
38654
38581
  res.writeHead(200, {
38655
38582
  "Content-Type": mime,
38656
38583
  "Content-Length": String(stat.size),
@@ -38658,7 +38585,7 @@ function streamFile(res, absPath, logger) {
38658
38585
  // 防止浏览器把任意 mime 当 html 渲染
38659
38586
  "X-Content-Type-Options": "nosniff"
38660
38587
  });
38661
- const stream = import_node_fs16.default.createReadStream(absPath);
38588
+ const stream = import_node_fs17.default.createReadStream(absPath);
38662
38589
  stream.on("error", (err) => {
38663
38590
  logger?.warn("streamFile read error", { absPath, err: err.message });
38664
38591
  res.destroy();
@@ -38667,8 +38594,8 @@ function streamFile(res, absPath, logger) {
38667
38594
  }
38668
38595
 
38669
38596
  // src/attachment/gc.ts
38670
- var import_node_fs17 = __toESM(require("fs"), 1);
38671
- var import_node_path20 = __toESM(require("path"), 1);
38597
+ var import_node_fs18 = __toESM(require("fs"), 1);
38598
+ var import_node_path21 = __toESM(require("path"), 1);
38672
38599
  var DEFAULT_TTL_MS = 30 * 24 * 3600 * 1e3;
38673
38600
  function runAttachmentGc(args) {
38674
38601
  const now = (args.now ?? Date.now)();
@@ -38677,38 +38604,38 @@ function runAttachmentGc(args) {
38677
38604
  for (const { scope, sessionId } of args.sessionScopes) {
38678
38605
  for (const entry of args.groupFileStore.list(scope, sessionId)) {
38679
38606
  if (entry.stale) continue;
38680
- if (import_node_path20.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38607
+ if (import_node_path21.default.isAbsolute(entry.relPath)) liveAbs.add(entry.relPath);
38681
38608
  }
38682
38609
  }
38683
38610
  for (const { scope, sessionId } of args.sessionScopes) {
38684
- const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path20.default.join(
38611
+ const sessionDir = args.getSessionCwd?.(sessionId) ?? import_node_path21.default.join(
38685
38612
  args.dataDir,
38686
38613
  "sessions",
38687
38614
  ...scopeSubPath(scope).map(safeFileName),
38688
38615
  safeFileName(sessionId)
38689
38616
  );
38690
- const attRoot = import_node_path20.default.join(sessionDir, ".attachments");
38617
+ const attRoot = import_node_path21.default.join(sessionDir, ".attachments");
38691
38618
  let hashDirs;
38692
38619
  try {
38693
- hashDirs = import_node_fs17.default.readdirSync(attRoot);
38620
+ hashDirs = import_node_fs18.default.readdirSync(attRoot);
38694
38621
  } catch (err) {
38695
38622
  if (err.code === "ENOENT") continue;
38696
38623
  args.logger?.warn("attachment gc: readdir failed", { attRoot, err: err.message });
38697
38624
  continue;
38698
38625
  }
38699
38626
  for (const hashDir of hashDirs) {
38700
- const hashDirAbs = import_node_path20.default.join(attRoot, hashDir);
38627
+ const hashDirAbs = import_node_path21.default.join(attRoot, hashDir);
38701
38628
  let files;
38702
38629
  try {
38703
- files = import_node_fs17.default.readdirSync(hashDirAbs);
38630
+ files = import_node_fs18.default.readdirSync(hashDirAbs);
38704
38631
  } catch {
38705
38632
  continue;
38706
38633
  }
38707
38634
  for (const name of files) {
38708
- const file = import_node_path20.default.join(hashDirAbs, name);
38635
+ const file = import_node_path21.default.join(hashDirAbs, name);
38709
38636
  let stat;
38710
38637
  try {
38711
- stat = import_node_fs17.default.statSync(file);
38638
+ stat = import_node_fs18.default.statSync(file);
38712
38639
  } catch {
38713
38640
  continue;
38714
38641
  }
@@ -38717,27 +38644,27 @@ function runAttachmentGc(args) {
38717
38644
  if (age < ttlMs) continue;
38718
38645
  if (liveAbs.has(file)) continue;
38719
38646
  try {
38720
- import_node_fs17.default.unlinkSync(file);
38647
+ import_node_fs18.default.unlinkSync(file);
38721
38648
  } catch (err) {
38722
38649
  args.logger?.warn("attachment gc: unlink failed", { file, err: err.message });
38723
38650
  }
38724
38651
  }
38725
38652
  try {
38726
- if (import_node_fs17.default.readdirSync(hashDirAbs).length === 0) import_node_fs17.default.rmdirSync(hashDirAbs);
38653
+ if (import_node_fs18.default.readdirSync(hashDirAbs).length === 0) import_node_fs18.default.rmdirSync(hashDirAbs);
38727
38654
  } catch {
38728
38655
  }
38729
38656
  }
38730
38657
  try {
38731
- if (import_node_fs17.default.readdirSync(attRoot).length === 0) import_node_fs17.default.rmdirSync(attRoot);
38658
+ if (import_node_fs18.default.readdirSync(attRoot).length === 0) import_node_fs18.default.rmdirSync(attRoot);
38732
38659
  } catch {
38733
38660
  }
38734
38661
  }
38735
38662
  }
38736
38663
 
38737
38664
  // src/attachment/group.ts
38738
- var import_node_fs18 = __toESM(require("fs"), 1);
38739
- var import_node_path21 = __toESM(require("path"), 1);
38740
- var import_node_crypto6 = __toESM(require("crypto"), 1);
38665
+ var import_node_fs19 = __toESM(require("fs"), 1);
38666
+ var import_node_path22 = __toESM(require("path"), 1);
38667
+ var import_node_crypto7 = __toESM(require("crypto"), 1);
38741
38668
  init_protocol();
38742
38669
  var GroupFileStore = class {
38743
38670
  dataDir;
@@ -38748,11 +38675,11 @@ var GroupFileStore = class {
38748
38675
  this.logger = opts.logger;
38749
38676
  }
38750
38677
  rootForScope(scope) {
38751
- return import_node_path21.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38678
+ return import_node_path22.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
38752
38679
  }
38753
38680
  /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
38754
38681
  filePath(scope, sessionId) {
38755
- return import_node_path21.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38682
+ return import_node_path22.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
38756
38683
  }
38757
38684
  cacheKey(scope, sessionId) {
38758
38685
  return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
@@ -38761,7 +38688,7 @@ var GroupFileStore = class {
38761
38688
  readFile(scope, sessionId) {
38762
38689
  const file = this.filePath(scope, sessionId);
38763
38690
  try {
38764
- const raw = import_node_fs18.default.readFileSync(file, "utf8");
38691
+ const raw = import_node_fs19.default.readFileSync(file, "utf8");
38765
38692
  const parsed = JSON.parse(raw);
38766
38693
  if (!Array.isArray(parsed)) {
38767
38694
  this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
@@ -38787,10 +38714,10 @@ var GroupFileStore = class {
38787
38714
  }
38788
38715
  writeFile(scope, sessionId, entries) {
38789
38716
  const file = this.filePath(scope, sessionId);
38790
- import_node_fs18.default.mkdirSync(import_node_path21.default.dirname(file), { recursive: true });
38717
+ import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(file), { recursive: true });
38791
38718
  const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
38792
- import_node_fs18.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38793
- import_node_fs18.default.renameSync(tmp, file);
38719
+ import_node_fs19.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
38720
+ import_node_fs19.default.renameSync(tmp, file);
38794
38721
  }
38795
38722
  /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
38796
38723
  list(scope, sessionId) {
@@ -38826,7 +38753,7 @@ var GroupFileStore = class {
38826
38753
  entries[idx] = next;
38827
38754
  } else {
38828
38755
  next = {
38829
- id: `gf-${import_node_crypto6.default.randomBytes(6).toString("base64url")}`,
38756
+ id: `gf-${import_node_crypto7.default.randomBytes(6).toString("base64url")}`,
38830
38757
  relPath: input.relPath,
38831
38758
  from: input.from,
38832
38759
  label: input.label,
@@ -38876,10 +38803,10 @@ var GroupFileStore = class {
38876
38803
  };
38877
38804
 
38878
38805
  // src/discovery/state-file.ts
38879
- var import_node_fs19 = __toESM(require("fs"), 1);
38880
- var import_node_path22 = __toESM(require("path"), 1);
38806
+ var import_node_fs20 = __toESM(require("fs"), 1);
38807
+ var import_node_path23 = __toESM(require("path"), 1);
38881
38808
  function defaultStateFilePath(dataDir) {
38882
- return import_node_path22.default.join(dataDir, "state.json");
38809
+ return import_node_path23.default.join(dataDir, "state.json");
38883
38810
  }
38884
38811
  function isPidAlive(pid) {
38885
38812
  if (!Number.isFinite(pid) || pid <= 0) return false;
@@ -38901,7 +38828,7 @@ var StateFileManager = class {
38901
38828
  }
38902
38829
  read() {
38903
38830
  try {
38904
- const raw = import_node_fs19.default.readFileSync(this.file, "utf8");
38831
+ const raw = import_node_fs20.default.readFileSync(this.file, "utf8");
38905
38832
  const parsed = JSON.parse(raw);
38906
38833
  return parsed;
38907
38834
  } catch {
@@ -38915,34 +38842,34 @@ var StateFileManager = class {
38915
38842
  return { status: "stale", existing };
38916
38843
  }
38917
38844
  write(state) {
38918
- import_node_fs19.default.mkdirSync(import_node_path22.default.dirname(this.file), { recursive: true });
38845
+ import_node_fs20.default.mkdirSync(import_node_path23.default.dirname(this.file), { recursive: true });
38919
38846
  const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
38920
- import_node_fs19.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38921
- import_node_fs19.default.renameSync(tmp, this.file);
38847
+ import_node_fs20.default.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 384 });
38848
+ import_node_fs20.default.renameSync(tmp, this.file);
38922
38849
  if (process.platform !== "win32") {
38923
38850
  try {
38924
- import_node_fs19.default.chmodSync(this.file, 384);
38851
+ import_node_fs20.default.chmodSync(this.file, 384);
38925
38852
  } catch {
38926
38853
  }
38927
38854
  }
38928
38855
  }
38929
38856
  delete() {
38930
38857
  try {
38931
- import_node_fs19.default.unlinkSync(this.file);
38858
+ import_node_fs20.default.unlinkSync(this.file);
38932
38859
  } catch {
38933
38860
  }
38934
38861
  }
38935
38862
  };
38936
38863
 
38937
38864
  // src/tunnel/tunnel-manager.ts
38938
- var import_node_fs23 = __toESM(require("fs"), 1);
38939
- var import_node_path26 = __toESM(require("path"), 1);
38940
- var import_node_crypto7 = __toESM(require("crypto"), 1);
38865
+ var import_node_fs24 = __toESM(require("fs"), 1);
38866
+ var import_node_path27 = __toESM(require("path"), 1);
38867
+ var import_node_crypto8 = __toESM(require("crypto"), 1);
38941
38868
  var import_node_child_process5 = require("child_process");
38942
38869
 
38943
38870
  // src/tunnel/tunnel-store.ts
38944
- var import_node_fs20 = __toESM(require("fs"), 1);
38945
- var import_node_path23 = __toESM(require("path"), 1);
38871
+ var import_node_fs21 = __toESM(require("fs"), 1);
38872
+ var import_node_path24 = __toESM(require("path"), 1);
38946
38873
  var TunnelStore = class {
38947
38874
  constructor(filePath) {
38948
38875
  this.filePath = filePath;
@@ -38950,7 +38877,7 @@ var TunnelStore = class {
38950
38877
  filePath;
38951
38878
  async get() {
38952
38879
  try {
38953
- const raw = await import_node_fs20.default.promises.readFile(this.filePath, "utf8");
38880
+ const raw = await import_node_fs21.default.promises.readFile(this.filePath, "utf8");
38954
38881
  const obj = JSON.parse(raw);
38955
38882
  if (!isPersistedTunnel(obj)) return null;
38956
38883
  return obj;
@@ -38961,22 +38888,22 @@ var TunnelStore = class {
38961
38888
  }
38962
38889
  }
38963
38890
  async set(v2) {
38964
- const dir = import_node_path23.default.dirname(this.filePath);
38965
- await import_node_fs20.default.promises.mkdir(dir, { recursive: true });
38891
+ const dir = import_node_path24.default.dirname(this.filePath);
38892
+ await import_node_fs21.default.promises.mkdir(dir, { recursive: true });
38966
38893
  const data = JSON.stringify(v2, null, 2);
38967
38894
  const tmp = `${this.filePath}.tmp.${process.pid}.${Date.now()}`;
38968
- await import_node_fs20.default.promises.writeFile(tmp, data, { mode: 384 });
38895
+ await import_node_fs21.default.promises.writeFile(tmp, data, { mode: 384 });
38969
38896
  if (process.platform !== "win32") {
38970
38897
  try {
38971
- await import_node_fs20.default.promises.chmod(tmp, 384);
38898
+ await import_node_fs21.default.promises.chmod(tmp, 384);
38972
38899
  } catch {
38973
38900
  }
38974
38901
  }
38975
- await import_node_fs20.default.promises.rename(tmp, this.filePath);
38902
+ await import_node_fs21.default.promises.rename(tmp, this.filePath);
38976
38903
  }
38977
38904
  async clear() {
38978
38905
  try {
38979
- await import_node_fs20.default.promises.unlink(this.filePath);
38906
+ await import_node_fs21.default.promises.unlink(this.filePath);
38980
38907
  } catch (err) {
38981
38908
  const code = err?.code;
38982
38909
  if (code !== "ENOENT") throw err;
@@ -39071,9 +38998,9 @@ function escape(v2) {
39071
38998
  }
39072
38999
 
39073
39000
  // src/tunnel/frpc-binary.ts
39074
- var import_node_fs21 = __toESM(require("fs"), 1);
39001
+ var import_node_fs22 = __toESM(require("fs"), 1);
39075
39002
  var import_node_os11 = __toESM(require("os"), 1);
39076
- var import_node_path24 = __toESM(require("path"), 1);
39003
+ var import_node_path25 = __toESM(require("path"), 1);
39077
39004
  var import_node_child_process3 = require("child_process");
39078
39005
  var import_node_stream2 = require("stream");
39079
39006
  var import_promises3 = require("stream/promises");
@@ -39105,20 +39032,20 @@ function frpcDownloadUrl(version2, p2) {
39105
39032
  }
39106
39033
  async function ensureFrpcBinary(opts) {
39107
39034
  if (opts.override) {
39108
- if (!import_node_fs21.default.existsSync(opts.override)) {
39035
+ if (!import_node_fs22.default.existsSync(opts.override)) {
39109
39036
  throw new Error(`frpc binary not found at override path: ${opts.override}`);
39110
39037
  }
39111
39038
  return opts.override;
39112
39039
  }
39113
39040
  const version2 = opts.version ?? FRPC_VERSION;
39114
39041
  const platform = opts.platform ?? detectPlatform();
39115
- const binDir = import_node_path24.default.join(opts.dataDir, "bin");
39116
- import_node_fs21.default.mkdirSync(binDir, { recursive: true });
39042
+ const binDir = import_node_path25.default.join(opts.dataDir, "bin");
39043
+ import_node_fs22.default.mkdirSync(binDir, { recursive: true });
39117
39044
  cleanupStaleArtifacts(binDir);
39118
- const stableBin = import_node_path24.default.join(binDir, "frpc");
39119
- if (import_node_fs21.default.existsSync(stableBin)) return stableBin;
39045
+ const stableBin = import_node_path25.default.join(binDir, "frpc");
39046
+ if (import_node_fs22.default.existsSync(stableBin)) return stableBin;
39120
39047
  const partialBin = `${stableBin}.partial`;
39121
- const tarballPath = import_node_path24.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39048
+ const tarballPath = import_node_path25.default.join(binDir, `frp_${version2}_${platform.os}_${platform.arch}.tar.gz.partial`);
39122
39049
  try {
39123
39050
  const url = frpcDownloadUrl(version2, platform);
39124
39051
  await downloadToFile(url, tarballPath, opts.fetchImpl);
@@ -39127,8 +39054,8 @@ async function ensureFrpcBinary(opts) {
39127
39054
  } else {
39128
39055
  await extractFrpcFromTarball(tarballPath, binDir, version2, platform, partialBin);
39129
39056
  }
39130
- import_node_fs21.default.chmodSync(partialBin, 493);
39131
- import_node_fs21.default.renameSync(partialBin, stableBin);
39057
+ import_node_fs22.default.chmodSync(partialBin, 493);
39058
+ import_node_fs22.default.renameSync(partialBin, stableBin);
39132
39059
  } finally {
39133
39060
  safeUnlink(tarballPath);
39134
39061
  safeUnlink(partialBin);
@@ -39138,15 +39065,15 @@ async function ensureFrpcBinary(opts) {
39138
39065
  function cleanupStaleArtifacts(binDir) {
39139
39066
  let entries;
39140
39067
  try {
39141
- entries = import_node_fs21.default.readdirSync(binDir);
39068
+ entries = import_node_fs22.default.readdirSync(binDir);
39142
39069
  } catch {
39143
39070
  return;
39144
39071
  }
39145
39072
  for (const name of entries) {
39146
39073
  if (name.endsWith(".partial") || name.startsWith("extract-")) {
39147
- const full = import_node_path24.default.join(binDir, name);
39074
+ const full = import_node_path25.default.join(binDir, name);
39148
39075
  try {
39149
- import_node_fs21.default.rmSync(full, { recursive: true, force: true });
39076
+ import_node_fs22.default.rmSync(full, { recursive: true, force: true });
39150
39077
  } catch {
39151
39078
  }
39152
39079
  }
@@ -39154,7 +39081,7 @@ function cleanupStaleArtifacts(binDir) {
39154
39081
  }
39155
39082
  function safeUnlink(p2) {
39156
39083
  try {
39157
- import_node_fs21.default.unlinkSync(p2);
39084
+ import_node_fs22.default.unlinkSync(p2);
39158
39085
  } catch {
39159
39086
  }
39160
39087
  }
@@ -39165,13 +39092,13 @@ async function downloadToFile(url, dest, fetchImpl) {
39165
39092
  if (!res.ok || !res.body) {
39166
39093
  throw new Error(`download failed: ${res.status} ${res.statusText}`);
39167
39094
  }
39168
- const out = import_node_fs21.default.createWriteStream(dest);
39095
+ const out = import_node_fs22.default.createWriteStream(dest);
39169
39096
  const nodeStream = import_node_stream2.Readable.fromWeb(res.body);
39170
39097
  await (0, import_promises3.pipeline)(nodeStream, out);
39171
39098
  }
39172
39099
  async function extractFrpcFromTarball(tarball, binDir, version2, platform, destBin) {
39173
- const work = import_node_path24.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39174
- import_node_fs21.default.mkdirSync(work, { recursive: true });
39100
+ const work = import_node_path25.default.join(binDir, `extract-${process.pid}-${Date.now()}`);
39101
+ import_node_fs22.default.mkdirSync(work, { recursive: true });
39175
39102
  try {
39176
39103
  await new Promise((resolve6, reject) => {
39177
39104
  const proc = (0, import_node_child_process3.spawn)("tar", ["xzf", tarball, "-C", work], { stdio: "pipe" });
@@ -39179,32 +39106,32 @@ async function extractFrpcFromTarball(tarball, binDir, version2, platform, destB
39179
39106
  proc.on("exit", (code) => code === 0 ? resolve6() : reject(new Error(`tar exited ${code}`)));
39180
39107
  });
39181
39108
  const dirName = `frp_${version2}_${platform.os}_${platform.arch}`;
39182
- const src = import_node_path24.default.join(work, dirName, "frpc");
39183
- if (!import_node_fs21.default.existsSync(src)) {
39109
+ const src = import_node_path25.default.join(work, dirName, "frpc");
39110
+ if (!import_node_fs22.default.existsSync(src)) {
39184
39111
  throw new Error(`frpc not found inside tarball at ${src}`);
39185
39112
  }
39186
- import_node_fs21.default.copyFileSync(src, destBin);
39113
+ import_node_fs22.default.copyFileSync(src, destBin);
39187
39114
  } finally {
39188
- import_node_fs21.default.rmSync(work, { recursive: true, force: true });
39115
+ import_node_fs22.default.rmSync(work, { recursive: true, force: true });
39189
39116
  }
39190
39117
  }
39191
39118
 
39192
39119
  // src/tunnel/frpc-process.ts
39193
- var import_node_fs22 = __toESM(require("fs"), 1);
39194
- var import_node_path25 = __toESM(require("path"), 1);
39120
+ var import_node_fs23 = __toESM(require("fs"), 1);
39121
+ var import_node_path26 = __toESM(require("path"), 1);
39195
39122
  var import_node_child_process4 = require("child_process");
39196
39123
  function frpcPidFilePath(dataDir) {
39197
- return import_node_path25.default.join(dataDir, "frpc.pid");
39124
+ return import_node_path26.default.join(dataDir, "frpc.pid");
39198
39125
  }
39199
39126
  function writeFrpcPid(dataDir, pid) {
39200
39127
  try {
39201
- import_node_fs22.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39128
+ import_node_fs23.default.writeFileSync(frpcPidFilePath(dataDir), String(pid), { mode: 384 });
39202
39129
  } catch {
39203
39130
  }
39204
39131
  }
39205
39132
  function clearFrpcPid(dataDir) {
39206
39133
  try {
39207
- import_node_fs22.default.unlinkSync(frpcPidFilePath(dataDir));
39134
+ import_node_fs23.default.unlinkSync(frpcPidFilePath(dataDir));
39208
39135
  } catch {
39209
39136
  }
39210
39137
  }
@@ -39220,7 +39147,7 @@ function defaultIsPidAlive(pid) {
39220
39147
  }
39221
39148
  function defaultReadPidFile(file) {
39222
39149
  try {
39223
- return import_node_fs22.default.readFileSync(file, "utf8");
39150
+ return import_node_fs23.default.readFileSync(file, "utf8");
39224
39151
  } catch {
39225
39152
  return null;
39226
39153
  }
@@ -39236,7 +39163,7 @@ function defaultSleep(ms) {
39236
39163
  }
39237
39164
  async function killStaleFrpc(deps) {
39238
39165
  const pidFile = frpcPidFilePath(deps.dataDir);
39239
- const tomlPath = import_node_path25.default.join(deps.dataDir, "frpc.toml");
39166
+ const tomlPath = import_node_path26.default.join(deps.dataDir, "frpc.toml");
39240
39167
  const readPidFile = deps.readPidFileImpl ?? defaultReadPidFile;
39241
39168
  const isAlive = deps.isPidAliveImpl ?? defaultIsPidAlive;
39242
39169
  const killPid = deps.killPidImpl ?? defaultKillPid;
@@ -39260,7 +39187,7 @@ async function killStaleFrpc(deps) {
39260
39187
  }
39261
39188
  if (victims.size === 0) {
39262
39189
  try {
39263
- import_node_fs22.default.unlinkSync(pidFile);
39190
+ import_node_fs23.default.unlinkSync(pidFile);
39264
39191
  } catch {
39265
39192
  }
39266
39193
  return;
@@ -39271,7 +39198,7 @@ async function killStaleFrpc(deps) {
39271
39198
  }
39272
39199
  await sleep(deps.reapWaitMs ?? 300);
39273
39200
  try {
39274
- import_node_fs22.default.unlinkSync(pidFile);
39201
+ import_node_fs23.default.unlinkSync(pidFile);
39275
39202
  } catch {
39276
39203
  }
39277
39204
  }
@@ -39308,7 +39235,7 @@ var DEFAULT_TUNNEL_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
39308
39235
  var TunnelManager = class {
39309
39236
  constructor(deps) {
39310
39237
  this.deps = deps;
39311
- this.store = deps.store ?? new TunnelStore(import_node_path26.default.join(deps.dataDir, "tunnel.json"));
39238
+ this.store = deps.store ?? new TunnelStore(import_node_path27.default.join(deps.dataDir, "tunnel.json"));
39312
39239
  this.ttlMs = deps.ttlMs ?? DEFAULT_TUNNEL_TTL_MS;
39313
39240
  this.startupTimeoutMs = deps.startupTimeoutMs ?? 15e3;
39314
39241
  }
@@ -39435,8 +39362,8 @@ var TunnelManager = class {
39435
39362
  dataDir: this.deps.dataDir,
39436
39363
  override: this.deps.frpcBinaryOverride ?? void 0
39437
39364
  });
39438
- const tomlPath = import_node_path26.default.join(this.deps.dataDir, "frpc.toml");
39439
- const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto7.default.randomBytes(3).toString("hex")}`;
39365
+ const tomlPath = import_node_path27.default.join(this.deps.dataDir, "frpc.toml");
39366
+ const proxyName = `clawd-${t.subdomain}-${localPort}-${import_node_crypto8.default.randomBytes(3).toString("hex")}`;
39440
39367
  const toml = buildFrpcToml({
39441
39368
  serverAddr: t.frpsHost,
39442
39369
  serverPort: t.frpsPort,
@@ -39446,12 +39373,12 @@ var TunnelManager = class {
39446
39373
  localPort,
39447
39374
  logLevel: "info"
39448
39375
  });
39449
- await import_node_fs23.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39376
+ await import_node_fs24.default.promises.writeFile(tomlPath, toml, { mode: 384 });
39450
39377
  const proc = (this.deps.spawnImpl ?? import_node_child_process5.spawn)(frpcBin, ["-c", tomlPath], {
39451
39378
  stdio: ["ignore", "pipe", "pipe"]
39452
39379
  });
39453
- const logFilePath = import_node_path26.default.join(this.deps.dataDir, "frpc.log");
39454
- const logStream = import_node_fs23.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39380
+ const logFilePath = import_node_path27.default.join(this.deps.dataDir, "frpc.log");
39381
+ const logStream = import_node_fs24.default.createWriteStream(logFilePath, { flags: "a", mode: 384 });
39455
39382
  logStream.on("error", () => {
39456
39383
  });
39457
39384
  const tee = (chunk) => {
@@ -39534,65 +39461,59 @@ async function waitForFrpcReady(proc, timeoutMs) {
39534
39461
 
39535
39462
  // src/tunnel/device-key.ts
39536
39463
  var import_node_os12 = __toESM(require("os"), 1);
39537
- var import_node_path27 = __toESM(require("path"), 1);
39538
- var import_node_crypto8 = __toESM(require("crypto"), 1);
39464
+ var import_node_path28 = __toESM(require("path"), 1);
39465
+ var import_node_crypto9 = __toESM(require("crypto"), 1);
39539
39466
  var DERIVE_SALT = "clawd-tunnel-device-v1";
39540
39467
  function deriveStableDeviceKey(opts = {}) {
39541
39468
  const hostname = opts.hostname ?? import_node_os12.default.hostname();
39542
39469
  const uid = opts.uid ?? (typeof import_node_os12.default.userInfo === "function" ? import_node_os12.default.userInfo().uid : 0);
39543
39470
  const home = opts.home ?? import_node_os12.default.homedir();
39544
- const defaultDataDir = import_node_path27.default.resolve(import_node_path27.default.join(home, ".clawd"));
39545
- const normalizedDataDir = opts.dataDir ? import_node_path27.default.resolve(opts.dataDir) : null;
39471
+ const defaultDataDir = import_node_path28.default.resolve(import_node_path28.default.join(home, ".clawd"));
39472
+ const normalizedDataDir = opts.dataDir ? import_node_path28.default.resolve(opts.dataDir) : null;
39546
39473
  const isDefaultDir = normalizedDataDir == null || normalizedDataDir === defaultDataDir;
39547
39474
  const input = isDefaultDir ? `${hostname}::${uid}` : `${hostname}::${uid}::${normalizedDataDir}`;
39548
- return import_node_crypto8.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39475
+ return import_node_crypto9.default.createHmac("sha256", DERIVE_SALT).update(input).digest("hex").slice(0, 32);
39549
39476
  }
39550
39477
 
39551
39478
  // src/auth-store.ts
39552
- var import_node_fs24 = __toESM(require("fs"), 1);
39553
- var import_node_path28 = __toESM(require("path"), 1);
39554
- var import_node_crypto9 = __toESM(require("crypto"), 1);
39479
+ var import_node_fs25 = __toESM(require("fs"), 1);
39480
+ var import_node_path29 = __toESM(require("path"), 1);
39481
+ var import_node_crypto10 = __toESM(require("crypto"), 1);
39555
39482
  var AUTH_FILE_NAME = "auth.json";
39556
39483
  function authFilePath(dataDir) {
39557
- return import_node_path28.default.join(dataDir, AUTH_FILE_NAME);
39484
+ return import_node_path29.default.join(dataDir, AUTH_FILE_NAME);
39558
39485
  }
39559
39486
  function loadOrCreateAuthFile(opts) {
39560
39487
  const file = authFilePath(opts.dataDir);
39561
- const generate = opts.generate ?? defaultGenerateToken;
39488
+ const generate = opts.generate ?? defaultGenerateToken2;
39562
39489
  const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
39563
- const genDevice = opts.genDeviceId ?? defaultGenerateDeviceId;
39564
39490
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
39565
39491
  const existing = readAuthFile(file);
39566
- if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId && existing.deviceId) {
39492
+ if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
39567
39493
  return {
39568
39494
  token: existing.token,
39569
39495
  signSecret: existing.signSecret,
39570
39496
  ownerPrincipalId: existing.ownerPrincipalId,
39571
- deviceId: existing.deviceId,
39572
39497
  createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
39573
39498
  };
39574
39499
  }
39575
39500
  const token = existing?.token || generate();
39576
39501
  const signSecret = existing?.signSecret || generate();
39577
39502
  const ownerPrincipalId = existing?.ownerPrincipalId || genId();
39578
- const deviceId = existing?.deviceId || genDevice();
39579
39503
  const createdAt = existing?.createdAt || now().toISOString();
39580
- const next = { token, signSecret, ownerPrincipalId, deviceId, createdAt };
39504
+ const next = { token, signSecret, ownerPrincipalId, createdAt };
39581
39505
  writeAuthFile(file, next);
39582
39506
  return next;
39583
39507
  }
39584
- function defaultGenerateToken() {
39585
- return import_node_crypto9.default.randomBytes(32).toString("base64url");
39508
+ function defaultGenerateToken2() {
39509
+ return import_node_crypto10.default.randomBytes(32).toString("base64url");
39586
39510
  }
39587
39511
  function defaultGenerateOwnerPrincipalId() {
39588
- return `owner-${import_node_crypto9.default.randomUUID()}`;
39589
- }
39590
- function defaultGenerateDeviceId() {
39591
- return `dev-${import_node_crypto9.default.randomUUID()}`;
39512
+ return `owner-${import_node_crypto10.default.randomUUID()}`;
39592
39513
  }
39593
39514
  function readAuthFile(file) {
39594
39515
  try {
39595
- const raw = import_node_fs24.default.readFileSync(file, "utf8");
39516
+ const raw = import_node_fs25.default.readFileSync(file, "utf8");
39596
39517
  const parsed = JSON.parse(raw);
39597
39518
  if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
39598
39519
  return null;
@@ -39601,7 +39522,6 @@ function readAuthFile(file) {
39601
39522
  token: parsed.token,
39602
39523
  signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
39603
39524
  ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
39604
- deviceId: typeof parsed.deviceId === "string" && parsed.deviceId.length > 0 ? parsed.deviceId : void 0,
39605
39525
  createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
39606
39526
  };
39607
39527
  } catch (err) {
@@ -39611,25 +39531,25 @@ function readAuthFile(file) {
39611
39531
  }
39612
39532
  }
39613
39533
  function writeAuthFile(file, content) {
39614
- import_node_fs24.default.mkdirSync(import_node_path28.default.dirname(file), { recursive: true });
39615
- import_node_fs24.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39534
+ import_node_fs25.default.mkdirSync(import_node_path29.default.dirname(file), { recursive: true });
39535
+ import_node_fs25.default.writeFileSync(file, JSON.stringify(content, null, 2), { mode: 384 });
39616
39536
  try {
39617
- import_node_fs24.default.chmodSync(file, 384);
39537
+ import_node_fs25.default.chmodSync(file, 384);
39618
39538
  } catch {
39619
39539
  }
39620
39540
  }
39621
39541
 
39622
39542
  // src/owner-profile.ts
39623
- var import_node_fs25 = __toESM(require("fs"), 1);
39543
+ var import_node_fs26 = __toESM(require("fs"), 1);
39624
39544
  var import_node_os13 = __toESM(require("os"), 1);
39625
- var import_node_path29 = __toESM(require("path"), 1);
39545
+ var import_node_path30 = __toESM(require("path"), 1);
39626
39546
  var PROFILE_FILENAME = "profile.json";
39627
39547
  function loadOwnerDisplayName(dataDir) {
39628
39548
  const fallback = import_node_os13.default.userInfo().username;
39629
- const profilePath = import_node_path29.default.join(dataDir, PROFILE_FILENAME);
39549
+ const profilePath = import_node_path30.default.join(dataDir, PROFILE_FILENAME);
39630
39550
  let raw;
39631
39551
  try {
39632
- raw = import_node_fs25.default.readFileSync(profilePath, "utf8");
39552
+ raw = import_node_fs26.default.readFileSync(profilePath, "utf8");
39633
39553
  } catch {
39634
39554
  return fallback;
39635
39555
  }
@@ -39651,357 +39571,6 @@ function loadOwnerDisplayName(dataDir) {
39651
39571
  return displayName;
39652
39572
  }
39653
39573
 
39654
- // src/feishu-auth/owner-identity-store.ts
39655
- var import_node_fs26 = __toESM(require("fs"), 1);
39656
- var import_node_path30 = __toESM(require("path"), 1);
39657
- var OWNER_IDENTITY_FILE_NAME = "owner-identity.json";
39658
- var OwnerIdentityStore = class {
39659
- file;
39660
- constructor(dataDir) {
39661
- this.file = import_node_path30.default.join(dataDir, OWNER_IDENTITY_FILE_NAME);
39662
- }
39663
- read() {
39664
- let raw;
39665
- try {
39666
- raw = import_node_fs26.default.readFileSync(this.file, "utf8");
39667
- } catch {
39668
- return null;
39669
- }
39670
- let parsed;
39671
- try {
39672
- parsed = JSON.parse(raw);
39673
- } catch {
39674
- return null;
39675
- }
39676
- const r = parsed;
39677
- 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) {
39678
- return null;
39679
- }
39680
- return {
39681
- identity: {
39682
- ownerId: r.identity.ownerId,
39683
- provider: r.identity.provider,
39684
- displayName: r.identity.displayName,
39685
- ...typeof r.identity.avatarUrl === "string" ? { avatarUrl: r.identity.avatarUrl } : {}
39686
- },
39687
- ttcToken: r.ttcToken,
39688
- ttcTokenExpiresAt: typeof r.ttcTokenExpiresAt === "number" ? r.ttcTokenExpiresAt : null,
39689
- loginAt: typeof r.loginAt === "string" ? r.loginAt : (/* @__PURE__ */ new Date(0)).toISOString()
39690
- };
39691
- }
39692
- write(record) {
39693
- import_node_fs26.default.mkdirSync(import_node_path30.default.dirname(this.file), { recursive: true });
39694
- import_node_fs26.default.writeFileSync(this.file, JSON.stringify(record, null, 2), { mode: 384 });
39695
- try {
39696
- import_node_fs26.default.chmodSync(this.file, 384);
39697
- } catch {
39698
- }
39699
- }
39700
- clear() {
39701
- try {
39702
- import_node_fs26.default.unlinkSync(this.file);
39703
- } catch (err) {
39704
- const code = err?.code;
39705
- if (code !== "ENOENT") throw err;
39706
- }
39707
- }
39708
- };
39709
-
39710
- // src/feishu-auth/login-flow.ts
39711
- var import_node_crypto10 = __toESM(require("crypto"), 1);
39712
- var STATE_TTL_MS = 5 * 60 * 1e3;
39713
- var LoginFlow = class {
39714
- constructor(deps) {
39715
- this.deps = deps;
39716
- }
39717
- deps;
39718
- pendingStates = /* @__PURE__ */ new Map();
39719
- start() {
39720
- const state = import_node_crypto10.default.randomBytes(16).toString("base64url");
39721
- const now = (this.deps.now ?? Date.now)();
39722
- this.pendingStates.set(state, now);
39723
- this.gcExpired(now);
39724
- const url = new URL("/auth/authorize", this.deps.ttcAuthBase);
39725
- url.searchParams.set("callback_url", this.deps.getCallbackUrl());
39726
- url.searchParams.set("state", state);
39727
- url.searchParams.set("auto", "1");
39728
- return { authUrl: url.toString(), state };
39729
- }
39730
- async handleCallback(params) {
39731
- const now = (this.deps.now ?? Date.now)();
39732
- const issuedAt = this.pendingStates.get(params.state);
39733
- if (issuedAt === void 0) {
39734
- return { ok: false, reason: "state mismatch" };
39735
- }
39736
- this.pendingStates.delete(params.state);
39737
- if (now - issuedAt > STATE_TTL_MS) {
39738
- return { ok: false, reason: "state expired" };
39739
- }
39740
- if (!params.token) {
39741
- return { ok: false, reason: "missing token" };
39742
- }
39743
- let identity;
39744
- try {
39745
- identity = await this.deps.fetchUserInfo(params.token);
39746
- } catch (err) {
39747
- return { ok: false, reason: `user_info failed: ${err.message}` };
39748
- }
39749
- const expiresAtNum = params.expiresAt ? Number.parseInt(params.expiresAt, 10) : NaN;
39750
- this.deps.store.write({
39751
- identity,
39752
- ttcToken: params.token,
39753
- ttcTokenExpiresAt: Number.isFinite(expiresAtNum) ? expiresAtNum : null,
39754
- loginAt: new Date(now).toISOString()
39755
- });
39756
- return { ok: true, identity };
39757
- }
39758
- gcExpired(now) {
39759
- for (const [state, issuedAt] of this.pendingStates) {
39760
- if (now - issuedAt > STATE_TTL_MS) this.pendingStates.delete(state);
39761
- }
39762
- }
39763
- };
39764
-
39765
- // src/feishu-auth/ttc-client.ts
39766
- var TTC_HOSTS = {
39767
- auth: "https://app.ttcadvisory.com",
39768
- api: "https://api.ttcadvisory.com"
39769
- };
39770
- var TtcUserInfoError = class extends Error {
39771
- constructor(code, message) {
39772
- super(message);
39773
- this.code = code;
39774
- this.name = "TtcUserInfoError";
39775
- }
39776
- code;
39777
- };
39778
- async function fetchTtcUserInfo(opts) {
39779
- const doFetch = opts.fetchImpl ?? fetch;
39780
- let res;
39781
- try {
39782
- res = await doFetch(`${opts.apiBase}/api/user_service/v1/login/user`, {
39783
- headers: { Authorization: `Bearer ${opts.token}` }
39784
- });
39785
- } catch (err) {
39786
- throw new TtcUserInfoError("NETWORK", `TTC user_info request failed: ${err.message}`);
39787
- }
39788
- if (!res.ok) {
39789
- if (res.status === 401) {
39790
- throw new TtcUserInfoError("UNAUTHORIZED", "TTC token invalid or expired");
39791
- }
39792
- throw new TtcUserInfoError("API_ERROR", `TTC user_info HTTP ${res.status}`);
39793
- }
39794
- const body = await res.json();
39795
- if (body.code !== 0) {
39796
- throw new TtcUserInfoError("API_ERROR", `TTC user_info code=${body.code}: ${body.message ?? ""}`);
39797
- }
39798
- const d = body.data;
39799
- if (!d || typeof d.unique_id !== "string" || d.unique_id.length === 0 || typeof d.name !== "string" || d.name.length === 0) {
39800
- throw new TtcUserInfoError("BAD_RESPONSE", "TTC user_info missing unique_id or name");
39801
- }
39802
- return {
39803
- ownerId: d.unique_id,
39804
- provider: "ttc",
39805
- displayName: d.name,
39806
- ...typeof d.avatar_url === "string" && d.avatar_url.length > 0 ? { avatarUrl: d.avatar_url } : {}
39807
- };
39808
- }
39809
-
39810
- // src/feishu-auth/central-client.ts
39811
- var CentralClientError = class extends Error {
39812
- constructor(code, message, cause) {
39813
- super(message);
39814
- this.code = code;
39815
- this.cause = cause;
39816
- this.name = "CentralClientError";
39817
- }
39818
- code;
39819
- cause;
39820
- };
39821
- async function centralRequest(opts, path60, init) {
39822
- const f = opts.fetchImpl ?? globalThis.fetch;
39823
- const url = `${opts.api.replace(/\/+$/, "")}${path60}`;
39824
- const ctrl = new AbortController();
39825
- const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 15e3);
39826
- let res;
39827
- try {
39828
- res = await f(url, {
39829
- method: init.method,
39830
- headers: {
39831
- "content-type": "application/json",
39832
- ...init.bearer ? { authorization: `Bearer ${init.bearer}` } : {}
39833
- },
39834
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
39835
- signal: ctrl.signal
39836
- });
39837
- } catch (err) {
39838
- clearTimeout(timer);
39839
- throw new CentralClientError("NETWORK", `central server request failed: ${err.message}`, err);
39840
- }
39841
- clearTimeout(timer);
39842
- if (res.status === 401) {
39843
- throw new CentralClientError("UNAUTHORIZED", "TTC JWT invalid or expired");
39844
- }
39845
- if (!res.ok) {
39846
- throw new CentralClientError("API_ERROR", `central server HTTP ${res.status}`);
39847
- }
39848
- try {
39849
- return await res.json();
39850
- } catch (err) {
39851
- throw new CentralClientError("BAD_RESPONSE", "central server: invalid json response", err);
39852
- }
39853
- }
39854
- async function centralExchange(opts) {
39855
- const body = await centralRequest(opts, "/api/clawd-identity/exchange", {
39856
- method: "POST",
39857
- body: {
39858
- jwt: opts.jwt,
39859
- provider: opts.provider,
39860
- deviceId: opts.deviceId,
39861
- selfDeviceId: opts.selfDeviceId
39862
- }
39863
- });
39864
- if (typeof body.token !== "string" || body.token.length === 0 || typeof body.ownerId !== "string" || typeof body.provider !== "string" || typeof body.displayName !== "string") {
39865
- throw new CentralClientError("BAD_RESPONSE", "exchange: missing token/ownerId/provider/displayName");
39866
- }
39867
- const deviceUrl = typeof body.deviceUrl === "string" && body.deviceUrl.length > 0 ? body.deviceUrl : null;
39868
- return { token: body.token, ownerId: body.ownerId, provider: body.provider, displayName: body.displayName, deviceUrl };
39869
- }
39870
- async function centralGetPublicKey(opts) {
39871
- const body = await centralRequest(opts, "/api/clawd-identity/public-key", {
39872
- method: "GET"
39873
- });
39874
- if (typeof body.publicKeyPem !== "string" || !body.publicKeyPem.includes("PUBLIC KEY")) {
39875
- throw new CentralClientError("BAD_RESPONSE", "public-key: missing or malformed publicKeyPem");
39876
- }
39877
- return { publicKeyPem: body.publicKeyPem };
39878
- }
39879
- async function centralUpsertDevice(opts) {
39880
- const body = await centralRequest(opts, "/api/clawd-identity/devices/self", {
39881
- method: "PUT",
39882
- bearer: opts.ttcJwt,
39883
- body: {
39884
- deviceId: opts.deviceId,
39885
- provider: opts.provider,
39886
- ...opts.url ? { url: opts.url } : {},
39887
- ...opts.name ? { name: opts.name } : {}
39888
- }
39889
- });
39890
- if (body.ok !== true) {
39891
- throw new CentralClientError("BAD_RESPONSE", "upsertDevice: server did not ack ok");
39892
- }
39893
- return { ok: true };
39894
- }
39895
- async function centralListDevices(opts) {
39896
- const body = await centralRequest(
39897
- opts,
39898
- `/api/clawd-identity/devices?provider=${encodeURIComponent(opts.provider)}`,
39899
- { method: "GET", bearer: opts.ttcJwt }
39900
- );
39901
- if (!Array.isArray(body.devices)) {
39902
- throw new CentralClientError("BAD_RESPONSE", "devices: missing devices array");
39903
- }
39904
- const out = [];
39905
- for (const raw of body.devices) {
39906
- const d = raw;
39907
- if (typeof d.deviceId !== "string" || typeof d.name !== "string" || typeof d.url !== "string") {
39908
- throw new CentralClientError("BAD_RESPONSE", "devices: malformed device entry");
39909
- }
39910
- out.push({ deviceId: d.deviceId, name: d.name, url: d.url });
39911
- }
39912
- return out;
39913
- }
39914
-
39915
- // src/feishu-auth/verify-token.ts
39916
- var crypto13 = __toESM(require("crypto"), 1);
39917
- var CONNECT_TOKEN_PREFIX = "clawdtk1";
39918
- function verifyConnectToken(args) {
39919
- const now = args.nowSeconds ?? Math.floor(Date.now() / 1e3);
39920
- const parts = args.token.split(".");
39921
- if (parts.length !== 3 || parts[0] !== CONNECT_TOKEN_PREFIX) {
39922
- return { ok: false, reason: "BAD_FORMAT" };
39923
- }
39924
- const [prefix, payloadB64, sigB64] = parts;
39925
- const data = `${prefix}.${payloadB64}`;
39926
- let signatureValid = false;
39927
- try {
39928
- signatureValid = crypto13.verify(
39929
- null,
39930
- Buffer.from(data, "utf8"),
39931
- args.publicKeyPem,
39932
- Buffer.from(sigB64, "base64url")
39933
- );
39934
- } catch {
39935
- return { ok: false, reason: "BAD_SIGNATURE" };
39936
- }
39937
- if (!signatureValid) {
39938
- return { ok: false, reason: "BAD_SIGNATURE" };
39939
- }
39940
- let payload;
39941
- try {
39942
- payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
39943
- } catch {
39944
- return { ok: false, reason: "BAD_FORMAT" };
39945
- }
39946
- 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") {
39947
- return { ok: false, reason: "BAD_FORMAT" };
39948
- }
39949
- if (payload.aud !== args.expectedDeviceId) {
39950
- return { ok: false, reason: "WRONG_AUDIENCE" };
39951
- }
39952
- if (payload.exp <= now) {
39953
- return { ok: false, reason: "EXPIRED" };
39954
- }
39955
- return {
39956
- ok: true,
39957
- subDevice: payload.subDevice,
39958
- subOwner: payload.subOwner,
39959
- provider: payload.provider,
39960
- displayName: payload.name
39961
- };
39962
- }
39963
-
39964
- // src/feishu-auth/server-key.ts
39965
- var fs35 = __toESM(require("fs"), 1);
39966
- var path39 = __toESM(require("path"), 1);
39967
- var FILE_NAME2 = "server-signing-key.json";
39968
- var ServerKeyStore = class {
39969
- constructor(dataDir) {
39970
- this.dataDir = dataDir;
39971
- }
39972
- dataDir;
39973
- filePath() {
39974
- return path39.join(this.dataDir, FILE_NAME2);
39975
- }
39976
- /** 读缓存的公钥;无缓存 / 损坏 → null(调用方决定是否触发拉取) */
39977
- read() {
39978
- try {
39979
- const raw = fs35.readFileSync(this.filePath(), "utf8");
39980
- const parsed = JSON.parse(raw);
39981
- if (typeof parsed.publicKeyPem === "string" && parsed.publicKeyPem.includes("PUBLIC KEY")) {
39982
- return parsed.publicKeyPem;
39983
- }
39984
- return null;
39985
- } catch {
39986
- return null;
39987
- }
39988
- }
39989
- write(publicKeyPem) {
39990
- const content = {
39991
- publicKeyPem,
39992
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
39993
- };
39994
- fs35.mkdirSync(this.dataDir, { recursive: true });
39995
- fs35.writeFileSync(this.filePath(), JSON.stringify(content, null, 2), { mode: 384 });
39996
- }
39997
- clear() {
39998
- try {
39999
- fs35.unlinkSync(this.filePath());
40000
- } catch {
40001
- }
40002
- }
40003
- };
40004
-
40005
39574
  // src/index.ts
40006
39575
  init_protocol();
40007
39576
 
@@ -40132,15 +39701,23 @@ function buildSessionHandlers(deps) {
40132
39701
  }
40133
39702
  ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
40134
39703
  const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
40135
- const { response, broadcast } = manager.create(args, creatorPrincipalId);
39704
+ const creatorOwnerPrincipalId = ctx?.principal.kind === "guest" ? ctx.peerOwnerPrincipalId : ownerPrincipalId;
39705
+ const { response, broadcast } = manager.create(
39706
+ args,
39707
+ creatorPrincipalId,
39708
+ creatorOwnerPrincipalId
39709
+ );
40136
39710
  return { response: { type: "session:info", ...response }, broadcast };
40137
39711
  };
40138
39712
  const list = async (_frame, _client, ctx) => {
40139
39713
  const { response } = manager.list();
40140
39714
  if (ctx && ctx.principal.kind === "guest") {
40141
39715
  const sessions = response.sessions ?? [];
40142
- const guestDeviceId = ctx.principal.id;
40143
- const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
39716
+ const peerOwnerId = ctx.peerOwnerPrincipalId;
39717
+ const capId = ctx.capabilityId;
39718
+ const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter(
39719
+ (s) => peerOwnerId ? s.creatorOwnerPrincipalId === peerOwnerId : s.creatorPrincipalId === capId
39720
+ );
40144
39721
  return { response: { type: "session:list", sessions: filtered } };
40145
39722
  }
40146
39723
  return { response: { type: "session:list", ...response } };
@@ -40428,7 +40005,7 @@ function buildPermissionHandlers(deps) {
40428
40005
  }
40429
40006
 
40430
40007
  // src/handlers/history.ts
40431
- var path42 = __toESM(require("path"), 1);
40008
+ var path41 = __toESM(require("path"), 1);
40432
40009
  init_protocol();
40433
40010
 
40434
40011
  // src/session/recent-dirs.ts
@@ -40446,7 +40023,7 @@ function listRecentDirs(store, limit = 50) {
40446
40023
  }
40447
40024
 
40448
40025
  // src/permission/persona-paths.ts
40449
- var path41 = __toESM(require("path"), 1);
40026
+ var path40 = __toESM(require("path"), 1);
40450
40027
  function getAllowedPersonaIds(grants, action) {
40451
40028
  const ids = /* @__PURE__ */ new Set();
40452
40029
  for (const g2 of grants) {
@@ -40459,26 +40036,26 @@ function getAllowedPersonaIds(grants, action) {
40459
40036
  return ids;
40460
40037
  }
40461
40038
  function isGuestPathAllowed(grants, absPath, personaRoot, action = "read") {
40462
- const root = path41.resolve(personaRoot);
40463
- const target = path41.resolve(absPath);
40464
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40039
+ const root = path40.resolve(personaRoot);
40040
+ const target = path40.resolve(absPath);
40041
+ const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40465
40042
  if (!target.startsWith(root + sep3)) return false;
40466
- const rel = path41.relative(root, target);
40043
+ const rel = path40.relative(root, target);
40467
40044
  if (!rel || rel.startsWith("..")) return false;
40468
- const personaId = rel.split(path41.sep)[0];
40045
+ const personaId = rel.split(path40.sep)[0];
40469
40046
  if (!personaId) return false;
40470
40047
  const allowed = getAllowedPersonaIds(grants, action);
40471
40048
  if (allowed === "*") return true;
40472
40049
  return allowed.has(personaId);
40473
40050
  }
40474
40051
  function personaIdFromPath(absPath, personaRoot) {
40475
- const root = path41.resolve(personaRoot);
40476
- const target = path41.resolve(absPath);
40477
- const sep3 = root.endsWith(path41.sep) ? "" : path41.sep;
40052
+ const root = path40.resolve(personaRoot);
40053
+ const target = path40.resolve(absPath);
40054
+ const sep3 = root.endsWith(path40.sep) ? "" : path40.sep;
40478
40055
  if (!target.startsWith(root + sep3)) return null;
40479
- const rel = path41.relative(root, target);
40056
+ const rel = path40.relative(root, target);
40480
40057
  if (!rel || rel.startsWith("..")) return null;
40481
- const id = rel.split(path41.sep)[0];
40058
+ const id = rel.split(path40.sep)[0];
40482
40059
  return id || null;
40483
40060
  }
40484
40061
 
@@ -40503,7 +40080,7 @@ function buildHistoryHandlers(deps) {
40503
40080
  if (!pid) return false;
40504
40081
  return isGuestPathAllowed(
40505
40082
  ctx.grants,
40506
- path42.join(personaRoot, pid),
40083
+ path41.join(personaRoot, pid),
40507
40084
  personaRoot,
40508
40085
  "read"
40509
40086
  );
@@ -40514,7 +40091,7 @@ function buildHistoryHandlers(deps) {
40514
40091
  };
40515
40092
  const list = async (frame, _client, ctx) => {
40516
40093
  const args = HistoryListArgs.parse(frame);
40517
- assertGuestPath(ctx, path42.resolve(args.projectPath), personaRoot, "history:list");
40094
+ assertGuestPath(ctx, path41.resolve(args.projectPath), personaRoot, "history:list");
40518
40095
  const sessions = await history.listSessions(args);
40519
40096
  return { response: { type: "history:list", sessions } };
40520
40097
  };
@@ -40523,7 +40100,7 @@ function buildHistoryHandlers(deps) {
40523
40100
  const { response: file } = manager.get({ sessionId: args.sessionId });
40524
40101
  const f = file;
40525
40102
  if (ctx && ctx.principal.kind === "guest") {
40526
- const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.principal.id;
40103
+ const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
40527
40104
  if (!ok) {
40528
40105
  throw new ClawdError(
40529
40106
  ERROR_CODES.UNAUTHORIZED,
@@ -40546,13 +40123,13 @@ function buildHistoryHandlers(deps) {
40546
40123
  };
40547
40124
  const subagents = async (frame, _client, ctx) => {
40548
40125
  const args = HistorySubagentsArgs.parse(frame);
40549
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagents");
40126
+ assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagents");
40550
40127
  const subs = await history.listSubagents(args);
40551
40128
  return { response: { type: "history:subagents", subagents: subs } };
40552
40129
  };
40553
40130
  const subagentRead = async (frame, _client, ctx) => {
40554
40131
  const args = HistorySubagentReadArgs.parse(frame);
40555
- assertGuestPath(ctx, path42.resolve(args.cwd), personaRoot, "history:subagent-read");
40132
+ assertGuestPath(ctx, path41.resolve(args.cwd), personaRoot, "history:subagent-read");
40556
40133
  const res = await history.readSubagent(args);
40557
40134
  return { response: { type: "history:subagent-read", ...res } };
40558
40135
  };
@@ -40560,7 +40137,7 @@ function buildHistoryHandlers(deps) {
40560
40137
  const dirs = listRecentDirs(store);
40561
40138
  if (ctx?.principal.kind === "guest" && personaRoot) {
40562
40139
  const filtered = dirs.filter(
40563
- (d) => isGuestPathAllowed(ctx.grants, path42.resolve(d.cwd), personaRoot, "read")
40140
+ (d) => isGuestPathAllowed(ctx.grants, path41.resolve(d.cwd), personaRoot, "read")
40564
40141
  );
40565
40142
  return { response: { type: "history:recentDirs", dirs: filtered } };
40566
40143
  }
@@ -40577,7 +40154,7 @@ function buildHistoryHandlers(deps) {
40577
40154
  }
40578
40155
 
40579
40156
  // src/handlers/workspace.ts
40580
- var path43 = __toESM(require("path"), 1);
40157
+ var path42 = __toESM(require("path"), 1);
40581
40158
  var os15 = __toESM(require("os"), 1);
40582
40159
  init_protocol();
40583
40160
  init_protocol();
@@ -40618,22 +40195,22 @@ function buildWorkspaceHandlers(deps) {
40618
40195
  const args = WorkspaceListArgs.parse(frame);
40619
40196
  const isGuest = ctx?.principal.kind === "guest";
40620
40197
  const fallbackCwd = isGuest && personaRoot ? personaRoot : os15.homedir();
40621
- const resolvedCwd = path43.resolve(args.cwd ?? fallbackCwd);
40622
- const target = args.path ? path43.resolve(resolvedCwd, args.path) : resolvedCwd;
40198
+ const resolvedCwd = path42.resolve(args.cwd ?? fallbackCwd);
40199
+ const target = args.path ? path42.resolve(resolvedCwd, args.path) : resolvedCwd;
40623
40200
  assertGuestPath2(ctx, target, personaRoot, "workspace:list");
40624
40201
  const res = workspace.list({ ...args, cwd: resolvedCwd });
40625
40202
  return { response: { type: "workspace:list", ...res } };
40626
40203
  };
40627
40204
  const read = async (frame, _client, ctx) => {
40628
40205
  const args = WorkspaceReadArgs.parse(frame);
40629
- const target = path43.isAbsolute(args.path) ? path43.resolve(args.path) : path43.resolve(args.cwd, args.path);
40206
+ const target = path42.isAbsolute(args.path) ? path42.resolve(args.path) : path42.resolve(args.cwd, args.path);
40630
40207
  assertGuestPath2(ctx, target, personaRoot, "workspace:read");
40631
40208
  const res = workspace.read(args);
40632
40209
  return { response: { type: "workspace:read", ...res } };
40633
40210
  };
40634
40211
  const skillsList = async (frame, _client, ctx) => {
40635
40212
  const args = SkillsListArgs.parse(frame);
40636
- const cwdAbs = path43.resolve(args.cwd);
40213
+ const cwdAbs = path42.resolve(args.cwd);
40637
40214
  assertGuestPath2(ctx, cwdAbs, personaRoot, "skills:list");
40638
40215
  const list2 = skills.list(args);
40639
40216
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40645,7 +40222,7 @@ function buildWorkspaceHandlers(deps) {
40645
40222
  };
40646
40223
  const agentsList = async (frame, _client, ctx) => {
40647
40224
  const args = AgentsListArgs.parse(frame);
40648
- const cwdAbs = path43.resolve(args.cwd);
40225
+ const cwdAbs = path42.resolve(args.cwd);
40649
40226
  assertGuestPath2(ctx, cwdAbs, personaRoot, "agents:list");
40650
40227
  const list2 = agents.list(args);
40651
40228
  if (ctx?.principal.kind === "guest" && personaRoot) {
@@ -40664,7 +40241,7 @@ function buildWorkspaceHandlers(deps) {
40664
40241
  }
40665
40242
 
40666
40243
  // src/handlers/git.ts
40667
- var path45 = __toESM(require("path"), 1);
40244
+ var path44 = __toESM(require("path"), 1);
40668
40245
  init_protocol();
40669
40246
  init_protocol();
40670
40247
 
@@ -40750,7 +40327,7 @@ async function listGitBranches(cwd) {
40750
40327
  // src/handlers/git.ts
40751
40328
  function assertGuestCwd(ctx, cwd, personaRoot, method) {
40752
40329
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
40753
- if (!isGuestPathAllowed(ctx.grants, path45.resolve(cwd), personaRoot, "read")) {
40330
+ if (!isGuestPathAllowed(ctx.grants, path44.resolve(cwd), personaRoot, "read")) {
40754
40331
  throw new ClawdError(
40755
40332
  ERROR_CODES.UNAUTHORIZED,
40756
40333
  `guest ${ctx.principal.id} cannot ${method} cwd ${cwd}`
@@ -40805,7 +40382,7 @@ var DeleteArgsSchema = external_exports.object({
40805
40382
  capabilityId: external_exports.string().min(1)
40806
40383
  }).strict();
40807
40384
  function buildCapabilityHandlers(deps) {
40808
- const { manager } = deps;
40385
+ const { manager, getShareBaseUrl, getPersonalCapability } = deps;
40809
40386
  const list = async () => {
40810
40387
  return {
40811
40388
  response: {
@@ -40831,45 +40408,70 @@ function buildCapabilityHandlers(deps) {
40831
40408
  }
40832
40409
  };
40833
40410
  };
40411
+ const personalCapGet = async () => {
40412
+ const { token, capability } = getPersonalCapability();
40413
+ return {
40414
+ response: {
40415
+ type: "personal-cap:get:ok",
40416
+ token,
40417
+ capability: stripSecretHash(capability),
40418
+ shareBaseUrl: getShareBaseUrl()
40419
+ }
40420
+ };
40421
+ };
40834
40422
  return {
40835
40423
  "capability:list": list,
40836
- "capability:delete": del
40424
+ "capability:delete": del,
40425
+ "personal-cap:get": personalCapGet
40837
40426
  };
40838
40427
  }
40839
40428
 
40840
40429
  // src/handlers/inbox.ts
40841
40430
  init_protocol();
40842
- function resolvePeerDeviceId(ctx, argsPeerDeviceId) {
40431
+ function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
40843
40432
  if (ctx.principal.kind === "owner") {
40844
- if (!argsPeerDeviceId) {
40433
+ if (!argsPeerOwnerId) {
40845
40434
  throw new ClawdError(
40846
40435
  ERROR_CODES.VALIDATION_ERROR,
40847
- "inbox: owner ctx requires peerDeviceId"
40436
+ "inbox: owner ctx requires peerOwnerId"
40848
40437
  );
40849
40438
  }
40850
- return argsPeerDeviceId;
40439
+ return argsPeerOwnerId;
40440
+ }
40441
+ if (!ctx.capabilityId) {
40442
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
40851
40443
  }
40852
- const resolved = ctx.principal.id;
40853
- if (argsPeerDeviceId && argsPeerDeviceId !== resolved) {
40444
+ let resolved = ctx.peerOwnerPrincipalId;
40445
+ if (!resolved) {
40446
+ const cap = capManager.findById(ctx.capabilityId);
40447
+ resolved = cap?.peerOwnerId;
40448
+ }
40449
+ if (!resolved) {
40854
40450
  throw new ClawdError(
40855
40451
  ERROR_CODES.UNAUTHORIZED,
40856
- `inbox: guest args.peerDeviceId mismatch (resolved=${resolved}, args=${argsPeerDeviceId})`
40452
+ `inbox: guest cap ${ctx.capabilityId} has no peerOwnerPrincipalId (auth frame missing selfPrincipalId)`
40453
+ );
40454
+ }
40455
+ if (argsPeerOwnerId && argsPeerOwnerId !== resolved) {
40456
+ throw new ClawdError(
40457
+ ERROR_CODES.UNAUTHORIZED,
40458
+ `inbox: guest args.peerOwnerId mismatch (resolved=${resolved}, args=${argsPeerOwnerId})`
40857
40459
  );
40858
40460
  }
40859
40461
  return resolved;
40860
40462
  }
40861
40463
  function buildInboxHandlers(deps) {
40862
- const { manager } = deps;
40464
+ const { manager, capManager } = deps;
40863
40465
  const postMessage = async (frame, _client, ctx) => {
40864
40466
  const { type: _t, requestId: _r, ...rest } = frame;
40865
40467
  const args = InboxPostMessageArgsSchema.parse(rest);
40866
40468
  if (!ctx) {
40867
40469
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
40868
40470
  }
40869
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40471
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40870
40472
  const message = manager.postMessage({
40871
- peerDeviceId,
40872
- senderDeviceId: ctx.principal.id,
40473
+ peerOwnerId,
40474
+ senderPrincipalId: ctx.principal.id,
40873
40475
  text: args.text,
40874
40476
  id: args.id,
40875
40477
  createdAt: args.createdAt
@@ -40884,10 +40486,10 @@ function buildInboxHandlers(deps) {
40884
40486
  if (!ctx) {
40885
40487
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
40886
40488
  }
40887
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40888
- const messages = manager.list(peerDeviceId, args.sinceCreatedAt);
40489
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40490
+ const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
40889
40491
  return {
40890
- response: { type: "inbox:list:ok", peerDeviceId, messages }
40492
+ response: { type: "inbox:list:ok", peerOwnerId, messages }
40891
40493
  };
40892
40494
  };
40893
40495
  const markRead = async (frame, _client, ctx) => {
@@ -40896,16 +40498,16 @@ function buildInboxHandlers(deps) {
40896
40498
  if (!ctx) {
40897
40499
  throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
40898
40500
  }
40899
- const peerDeviceId = resolvePeerDeviceId(ctx, args.peerDeviceId);
40501
+ const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
40900
40502
  const updated = manager.markRead({
40901
- peerDeviceId,
40902
- readerDeviceId: ctx.principal.id,
40503
+ peerOwnerId,
40504
+ principalId: ctx.principal.id,
40903
40505
  upToCreatedAt: args.upToCreatedAt
40904
40506
  });
40905
40507
  return {
40906
40508
  response: {
40907
40509
  type: "inbox:markRead:ok",
40908
- peerDeviceId,
40510
+ peerOwnerId,
40909
40511
  upToCreatedAt: args.upToCreatedAt,
40910
40512
  updated
40911
40513
  }
@@ -40918,45 +40520,116 @@ function buildInboxHandlers(deps) {
40918
40520
  };
40919
40521
  }
40920
40522
 
40921
- // src/handlers/contact.ts
40523
+ // src/handlers/received-capability.ts
40922
40524
  init_protocol();
40923
40525
  function ensureOwner(ctx) {
40924
40526
  if (!ctx || ctx.principal.kind !== "owner") {
40925
40527
  throw new ClawdError(
40926
40528
  ERROR_CODES.UNAUTHORIZED,
40927
- "UNAUTHORIZED: contact:* requires owner ctx"
40529
+ "UNAUTHORIZED: received-capability:* requires owner ctx"
40530
+ );
40531
+ }
40532
+ }
40533
+ function ensureGuestCtx(ctx) {
40534
+ if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
40535
+ throw new ClawdError(
40536
+ ERROR_CODES.UNAUTHORIZED,
40537
+ "UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
40928
40538
  );
40929
40539
  }
40930
40540
  }
40931
- function buildContactHandlers(deps) {
40541
+ function buildReceivedCapabilityHandlers(deps) {
40542
+ const now = deps.now ?? Date.now;
40932
40543
  const list = async (_frame, _client, ctx) => {
40933
40544
  ensureOwner(ctx);
40934
40545
  return {
40935
40546
  response: {
40936
- type: "contact:list:ok",
40937
- contacts: deps.store.list()
40547
+ type: "received-capability:list:ok",
40548
+ receivedCaps: deps.store.list()
40938
40549
  }
40939
40550
  };
40940
40551
  };
40552
+ const add = async (frame, _client, ctx) => {
40553
+ ensureOwner(ctx);
40554
+ const { type: _t, requestId: _r, ...rest } = frame;
40555
+ const args = ReceivedCapabilityAddArgsSchema.parse(rest);
40556
+ let conn;
40557
+ try {
40558
+ conn = await deps.connectRemote({
40559
+ url: args.remoteUrl,
40560
+ token: args.token,
40561
+ selfPrincipalId: deps.selfPrincipalId(),
40562
+ selfDisplayName: deps.selfDisplayName()
40563
+ });
40564
+ } catch (e) {
40565
+ throw new ClawdError(
40566
+ ERROR_CODES.VALIDATION_ERROR,
40567
+ `INVITE_REMOTE_UNREACHABLE: ${e.message}`
40568
+ );
40569
+ }
40570
+ try {
40571
+ const wh = await conn.whoami();
40572
+ const rc = {
40573
+ ownerPrincipalId: wh.ownerId,
40574
+ ownerDisplayName: wh.displayName,
40575
+ remoteUrl: args.remoteUrl,
40576
+ capabilityId: wh.capabilityId,
40577
+ capabilityToken: args.token,
40578
+ grants: wh.grants,
40579
+ receivedAt: now()
40580
+ };
40581
+ deps.store.upsert(rc);
40582
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40583
+ return {
40584
+ response: { type: "received-capability:add:ok", receivedCap: rc }
40585
+ };
40586
+ } finally {
40587
+ try {
40588
+ conn.close();
40589
+ } catch {
40590
+ }
40591
+ }
40592
+ };
40941
40593
  const remove = async (frame, _client, ctx) => {
40942
40594
  ensureOwner(ctx);
40943
40595
  const { type: _t, requestId: _r, ...rest } = frame;
40944
- const args = ContactRemoveArgsSchema.parse(rest);
40945
- deps.store.removeByDeviceId(args.deviceId);
40596
+ const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
40597
+ deps.store.remove(args.ownerPrincipalId);
40946
40598
  deps.broadcast({
40947
- type: "contact:removed",
40948
- deviceId: args.deviceId
40599
+ type: "received-capability:removed",
40600
+ ownerPrincipalId: args.ownerPrincipalId
40949
40601
  });
40950
40602
  return {
40951
40603
  response: {
40952
- type: "contact:remove:ok",
40953
- deviceId: args.deviceId
40604
+ type: "received-capability:remove:ok",
40605
+ ownerPrincipalId: args.ownerPrincipalId
40954
40606
  }
40955
40607
  };
40956
40608
  };
40609
+ const autoAttach = async (frame, _client, ctx) => {
40610
+ ensureGuestCtx(ctx);
40611
+ const { type: _t, requestId: _r, ...rest } = frame;
40612
+ const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
40613
+ const rc = {
40614
+ ownerPrincipalId: args.ownerPrincipalId,
40615
+ ownerDisplayName: args.ownerDisplayName,
40616
+ remoteUrl: args.remoteUrl,
40617
+ capabilityId: args.capabilityId,
40618
+ capabilityToken: args.token,
40619
+ grants: args.grants,
40620
+ receivedAt: now()
40621
+ };
40622
+ deps.store.upsert(rc);
40623
+ deps.broadcast({ type: "received-capability:added", receivedCap: rc });
40624
+ return {
40625
+ response: { type: "received-capability:autoAttach:ok" }
40626
+ };
40627
+ };
40957
40628
  return {
40958
- "contact:list": list,
40959
- "contact:remove": remove
40629
+ "received-capability:list": list,
40630
+ "received-capability:add": add,
40631
+ "received-capability:remove": remove,
40632
+ "received-capability:autoAttach": autoAttach
40960
40633
  };
40961
40634
  }
40962
40635
 
@@ -40967,11 +40640,7 @@ function buildWhoamiHandler(deps) {
40967
40640
  if (!ctx) {
40968
40641
  throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
40969
40642
  }
40970
- const owner = {
40971
- ...makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName),
40972
- ownerId: deps.ownerId,
40973
- provider: deps.ownerProvider
40974
- };
40643
+ const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
40975
40644
  let capability;
40976
40645
  if (ctx.principal.kind === "owner") {
40977
40646
  capability = {
@@ -40982,13 +40651,14 @@ function buildWhoamiHandler(deps) {
40982
40651
  usedCount: 0
40983
40652
  };
40984
40653
  } else {
40985
- capability = {
40986
- id: ctx.principal.id,
40987
- displayName: ctx.principal.displayName ?? "guest",
40988
- grants: ctx.grants,
40989
- issuedAt: 0,
40990
- usedCount: 0
40991
- };
40654
+ if (!ctx.capabilityId) {
40655
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
40656
+ }
40657
+ const cap = deps.capabilityManager.findById(ctx.capabilityId);
40658
+ if (!cap) {
40659
+ throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
40660
+ }
40661
+ capability = stripSecretHash(cap);
40992
40662
  }
40993
40663
  const grantedPersonas = [];
40994
40664
  const isGuest = ctx.principal.kind === "guest";
@@ -41019,123 +40689,6 @@ function buildWhoamiHandler(deps) {
41019
40689
  };
41020
40690
  }
41021
40691
 
41022
- // src/handlers/feishu-auth.ts
41023
- function buildFeishuAuthHandlers(deps) {
41024
- const loginStart = async () => {
41025
- const { authUrl, state } = deps.loginFlow.start();
41026
- return {
41027
- response: { type: "auth:login:start:ok", authUrl, state }
41028
- };
41029
- };
41030
- const getIdentity = async () => {
41031
- const record = deps.ownerIdentityStore.read();
41032
- return {
41033
- response: {
41034
- type: "auth:getIdentity:ok",
41035
- identity: record ? record.identity : null,
41036
- // 决策 #15:设备属性,与登录态无关——UI"分享我的 Hub 标识"用
41037
- deviceId: deps.deviceId
41038
- }
41039
- };
41040
- };
41041
- const logout = async () => {
41042
- deps.ownerIdentityStore.clear();
41043
- return {
41044
- response: { type: "auth:logout:ok" }
41045
- };
41046
- };
41047
- return {
41048
- "auth:login:start": loginStart,
41049
- "auth:getIdentity": getIdentity,
41050
- "auth:logout": logout
41051
- };
41052
- }
41053
-
41054
- // src/handlers/device.ts
41055
- init_protocol();
41056
- function ensureOwner2(ctx) {
41057
- if (!ctx || ctx.principal.kind !== "owner") {
41058
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "UNAUTHORIZED: device:* requires owner ctx");
41059
- }
41060
- }
41061
- function buildDeviceHandlers(deps) {
41062
- const now = deps.now ?? Date.now;
41063
- const list = async (_frame, _client, ctx) => {
41064
- ensureOwner2(ctx);
41065
- const devices = await deps.listDevices();
41066
- return {
41067
- response: { type: "device:list:ok", devices }
41068
- };
41069
- };
41070
- const connect = async (frame, _client, ctx) => {
41071
- ensureOwner2(ctx);
41072
- const { type: _t, requestId: _r, ...rest } = frame;
41073
- const args = DeviceConnectArgsSchema.parse(rest);
41074
- const exchanged = await deps.exchange(args.deviceId);
41075
- const url = args.url ?? exchanged.deviceUrl;
41076
- if (!url) {
41077
- throw new ClawdError(
41078
- ERROR_CODES.VALIDATION_ERROR,
41079
- "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"
41080
- );
41081
- }
41082
- let conn;
41083
- try {
41084
- conn = await deps.connectRemote({
41085
- url,
41086
- token: exchanged.token,
41087
- // 自动反向(spec 决策 #11):自报本机可达地址,让对方反向加我为联系人;
41088
- // 本机无 tunnel → null → connectRemote 省略 selfUrl 字段(不造假数据)。
41089
- // 身份不自报:对方从 token 签名背书取我的 deviceId/ownerId/provider(决策 #16)。
41090
- selfUrl: deps.selfUrl() ?? void 0
41091
- });
41092
- } catch (e) {
41093
- throw new ClawdError(
41094
- ERROR_CODES.VALIDATION_ERROR,
41095
- `DEVICE_UNREACHABLE: ${e.message}`
41096
- );
41097
- }
41098
- try {
41099
- const wh = await conn.whoami();
41100
- const contact = {
41101
- deviceId: args.deviceId,
41102
- ownerId: wh.ownerId,
41103
- provider: wh.provider,
41104
- // 显示名优先用调用方传的 name:公共目录点击连接带 clawd_devices.name(对外展示名,
41105
- // 如"公共1");"输入标识"连接不带 name → 回落 whoami 报的对方 owner 真名。
41106
- // 归属人身份不丢(ownerId/provider 仍来自 whoami 背书)。
41107
- displayName: args.name || wh.displayName || args.deviceId,
41108
- remoteUrl: url,
41109
- // connectToken 存自包含签名 token(断线重连复用,30 天有效;
41110
- // 过期后对方拒 TOKEN_EXPIRED → 重新 device:connect 换新票)
41111
- connectToken: exchanged.token,
41112
- grants: wh.grants,
41113
- addedAt: now()
41114
- };
41115
- deps.store.upsert(contact);
41116
- deps.broadcast({ type: "contact:added", contact });
41117
- return {
41118
- response: {
41119
- type: "device:connect:ok",
41120
- // 返回连接的设备标识(= 请求的 args.deviceId)
41121
- deviceId: args.deviceId,
41122
- name: contact.displayName,
41123
- url
41124
- }
41125
- };
41126
- } finally {
41127
- try {
41128
- conn.close();
41129
- } catch {
41130
- }
41131
- }
41132
- };
41133
- return {
41134
- "device:list": list,
41135
- "device:connect": connect
41136
- };
41137
- }
41138
-
41139
40692
  // src/handlers/meta.ts
41140
40693
  var import_node_os15 = __toESM(require("os"), 1);
41141
40694
  init_protocol();
@@ -42548,25 +42101,27 @@ function buildMethodHandlers(deps) {
42548
42101
  personaRegistry: deps.personaRegistry
42549
42102
  }),
42550
42103
  ...buildCapabilityHandlers({
42551
- manager: deps.capabilityManager
42104
+ manager: deps.capabilityManager,
42105
+ getShareBaseUrl: deps.getShareBaseUrl,
42106
+ getPersonalCapability: deps.getPersonalCapability
42552
42107
  }),
42553
42108
  ...buildInboxHandlers({
42554
- manager: deps.inboxManager
42109
+ manager: deps.inboxManager,
42110
+ capManager: deps.capabilityManager
42555
42111
  }),
42556
- ...buildContactHandlers({
42557
- store: deps.contactStore,
42558
- broadcast: deps.broadcastToOwners
42112
+ ...buildReceivedCapabilityHandlers({
42113
+ store: deps.receivedCapabilityStore,
42114
+ connectRemote: deps.connectRemote,
42115
+ broadcast: deps.broadcastToOwners,
42116
+ selfPrincipalId: () => deps.ownerPrincipalId,
42117
+ selfDisplayName: () => deps.ownerDisplayName
42559
42118
  }),
42560
42119
  whoami: buildWhoamiHandler({
42561
42120
  ownerDisplayName: deps.ownerDisplayName,
42562
42121
  ownerPrincipalId: deps.ownerPrincipalId,
42563
- ownerId: deps.ownerId,
42564
- ownerProvider: deps.ownerProvider,
42565
42122
  personaStore: deps.personaStore,
42566
42123
  capabilityManager: deps.capabilityManager
42567
42124
  }),
42568
- ...buildFeishuAuthHandlers(deps.feishuAuth),
42569
- ...buildDeviceHandlers(deps.feishuDevice),
42570
42125
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {},
42571
42126
  ...buildExtensionHandlers({
42572
42127
  loadAll,
@@ -42760,7 +42315,7 @@ var ProjectStore = class {
42760
42315
  // src/app-builder/dev-server-supervisor.ts
42761
42316
  var import_node_child_process9 = require("child_process");
42762
42317
  var import_node_events2 = require("events");
42763
- var DEFAULT_READY_PATTERN = /Local:\s+https?:\/\//;
42318
+ var DEFAULT_READY_PATTERN = /Local:\s+https?:\/\/|Nest application successfully started|server listening on/i;
42764
42319
  var DevServerSupervisor = class extends import_node_events2.EventEmitter {
42765
42320
  running = /* @__PURE__ */ new Map();
42766
42321
  // key: sessionId
@@ -43013,22 +42568,23 @@ var METHOD_GRANT_MAP = {
43013
42568
  // "查 capabilityManager 拿 caller 的 guest capability"。
43014
42569
  "whoami": { kind: "public" },
43015
42570
  // ---- capability platform(admin-only) ----
43016
- // 飞书统一身份 Phase 2 (2026-06-01, spec 决策 #12): personal-cap:get / capability:issue /
43017
- // bindPeer 已下线(personal token / 邀请码全链路删除)。capability:list / delete 保留供调试 +
43018
- // 撤销级联 + 清旧 per-peer cap。
42571
+ // global personal token (2026-05-25): capability:issue / bindPeer 已下线;
42572
+ // personal capability daemon 启动时自动创建,personal-cap:get owner-only token + shareBaseUrl
43019
42573
  "capability:list": ADMIN_ANY,
43020
42574
  "capability:delete": ADMIN_ANY,
43021
- // ---- inbox: 双投 P2P IM ----
43022
- // 三条都 'public' 能连上 = 已通过本地验票. handler 内额外校验 guest 的
43023
- // args.peerDeviceId ctx.principal.id(=deviceId) 一致, guest 越权操作别的 DM 桶.
42575
+ "personal-cap:get": ADMIN_ANY,
42576
+ // ---- inbox: channel-based IM (capability platform v3) ----
42577
+ // 三条都 'public' capability 本身就是授权凭证. handler 内额外校验
42578
+ // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
43024
42579
  "inbox:list": { kind: "public" },
43025
42580
  "inbox:markRead": { kind: "public" },
43026
42581
  "inbox:postMessage": { kind: "public" },
43027
- // ---- contact:* (联系人列表) ----
43028
- // 飞书统一身份 Phase 3 (决策 #14): received-capability:* 正名 contact:*。
43029
- // 写入路径在 device:connect / 自动反向;list / remove 作联系人读 / 删,owner-only。
43030
- "contact:list": ADMIN_ANY,
43031
- "contact:remove": ADMIN_ANY,
42582
+ // ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
42583
+ // owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
42584
+ "received-capability:list": ADMIN_ANY,
42585
+ "received-capability:add": ADMIN_ANY,
42586
+ "received-capability:remove": ADMIN_ANY,
42587
+ "received-capability:autoAttach": { kind: "public" },
43032
42588
  // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
43033
42589
  // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
43034
42590
  // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
@@ -43069,7 +42625,7 @@ var METHOD_GRANT_MAP = {
43069
42625
  "history:list": CAPABILITY_SCOPED,
43070
42626
  // history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
43071
42627
  // 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
43072
- // handler 内按 ctx.principal.id === file.creatorPrincipalId 拦越权。
42628
+ // handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
43073
42629
  "history:read": CAPABILITY_SCOPED,
43074
42630
  "history:subagents": CAPABILITY_SCOPED,
43075
42631
  "history:subagent-read": CAPABILITY_SCOPED,
@@ -43126,15 +42682,6 @@ var METHOD_GRANT_MAP = {
43126
42682
  // guest-self:guest 在自己 daemon 上触发安装与更新(无持久化 subscription 概念)
43127
42683
  "extension.install-from-channel": ADMIN_ANY,
43128
42684
  "extension.update-from-channel": ADMIN_ANY,
43129
- // ---- auth:* 飞书统一身份 Phase 1(owner-only) ----
43130
- // 登录/登出/查身份都是 owner 本机操作,guest 不可调
43131
- "auth:login:start": ADMIN_ANY,
43132
- "auth:getIdentity": ADMIN_ANY,
43133
- "auth:logout": ADMIN_ANY,
43134
- // ---- device:* 设备目录 + 连接(决策 #15,owner-only) ----
43135
- // 设备目录 / 连接都是 owner 本机操作(用 owner 的 ttcJwt 找中心 server),guest 不可调
43136
- "device:list": ADMIN_ANY,
43137
- "device:connect": ADMIN_ANY,
43138
42685
  // ---- app-builder Project picker(spec 2026-06-01-app-builder-project-picker-design) ----
43139
42686
  // owner-only:picker UI 给 owner 管理本机 build 中的 project(fs + 端口分配)。
43140
42687
  // 即使 persona-app-builder 是 PreinstalledPersona,project 管理也属本机 owner 行为,
@@ -43541,23 +43088,19 @@ async function startDaemon(config) {
43541
43088
  } else if (config.tunnel) {
43542
43089
  resolvedAuthToken = authFile.token;
43543
43090
  }
43544
- const ownerIdentityStore = new OwnerIdentityStore(config.dataDir);
43545
- const feishuIdentity = ownerIdentityStore.read();
43546
- let feishuActive = feishuIdentity !== null;
43547
- const ownerPrincipalId = authFile.deviceId;
43548
- let ownerId = feishuIdentity?.identity.ownerId ?? "";
43549
- let ownerProvider = feishuIdentity?.identity.provider ?? "";
43550
- let ownerDisplayName = feishuIdentity?.identity.displayName ?? loadOwnerDisplayName(config.dataDir);
43091
+ const ownerPrincipalId = authFile.ownerPrincipalId;
43092
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
43551
43093
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
43552
43094
  const inboxStore = new InboxStore(config.dataDir);
43553
- const inboxManager = new InboxManager(inboxStore, (_peerDeviceId, frame) => {
43095
+ const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
43554
43096
  wsServer?.broadcastToOwners(frame);
43555
43097
  });
43556
- const contactStore = new ContactStore(config.dataDir);
43557
- contactStore.load();
43558
- const serverKeyStore = new ServerKeyStore(config.dataDir);
43098
+ const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
43099
+ receivedCapabilityStore.load();
43559
43100
  const capabilityStore = new CapabilityStore(config.dataDir);
43560
43101
  const capabilityRegistry = new CapabilityRegistry(capabilityStore);
43102
+ const personalCapability = loadOrCreatePersonalCapability({ dataDir: config.dataDir });
43103
+ capabilityRegistry.upsertCapability(personalCapability.capability);
43561
43104
  const capabilityManager = new CapabilityManager(capabilityRegistry, {
43562
43105
  onDeleted: (cap) => {
43563
43106
  const deletedAt = Date.now();
@@ -43566,7 +43109,7 @@ async function startDaemon(config) {
43566
43109
  capabilityId: cap.id,
43567
43110
  deletedAt
43568
43111
  });
43569
- wsServer?.closeConnectionsByGuestId(cap.id);
43112
+ wsServer?.closeConnectionsByCapability(cap.id);
43570
43113
  const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
43571
43114
  if (cleanup.removed.length > 0) {
43572
43115
  logger.info("capability delete cascade: guest sessions removed", {
@@ -43583,60 +43126,15 @@ async function startDaemon(config) {
43583
43126
  // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
43584
43127
  // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
43585
43128
  expectedToken: resolvedAuthToken,
43586
- authenticate: async (t, selfUrl) => {
43587
- const result = await authenticate(t, {
43129
+ authenticate: (t, selfPrincipalId, selfDisplayName) => {
43130
+ return authenticate(t, {
43588
43131
  isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
43589
43132
  ownerPrincipalId,
43590
43133
  ownerDisplayName,
43591
- // 飞书 gate(spec 决策 #9):未激活飞书身份时不接受 guest 入站(不注入验签函数
43592
- // → 一律 BAD_TOKEN)。已激活时用缓存的 server 公钥本地验签;
43593
- // 公钥缓存缺失(登录后拉取失败)→ fail-closed 拒绝 guest。
43594
- ...feishuActive ? {
43595
- verifyConnectToken: (token) => {
43596
- const publicKeyPem = serverKeyStore.read();
43597
- if (!publicKeyPem) return { ok: false, reason: "BAD_SIGNATURE" };
43598
- return verifyConnectToken({
43599
- token,
43600
- publicKeyPem,
43601
- expectedDeviceId: authFile.deviceId
43602
- });
43603
- }
43604
- } : {}
43134
+ capabilityRegistry,
43135
+ selfPrincipalId,
43136
+ selfDisplayName
43605
43137
  });
43606
- if (result.ok && result.context.principal.kind === "guest") {
43607
- void autoReverseContact({
43608
- store: contactStore,
43609
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
43610
- deviceId: result.context.principal.id,
43611
- ownerId: result.context.ownerId ?? "",
43612
- provider: result.context.provider ?? "",
43613
- displayName: result.context.principal.displayName ?? result.context.principal.id,
43614
- selfUrl,
43615
- // 决策 #16 换票补全:用 owner 当前 jwt 换 aud=对方设备 的票(本机主动连对方用)。
43616
- // 现读 ownerIdentityStore(热切换后用新身份);失败 → null → 保持空票。
43617
- exchangeToken: async (targetDeviceId) => {
43618
- const record = ownerIdentityStore.read();
43619
- if (!record) return null;
43620
- try {
43621
- const r = await centralExchange({
43622
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43623
- jwt: record.ttcToken,
43624
- provider: record.identity.provider,
43625
- deviceId: targetDeviceId,
43626
- selfDeviceId: authFile.deviceId
43627
- });
43628
- return r.token;
43629
- } catch (e) {
43630
- logger.warn("auto-reverse exchange token failed", {
43631
- targetDeviceId,
43632
- err: e.message
43633
- });
43634
- return null;
43635
- }
43636
- }
43637
- });
43638
- }
43639
- return result;
43640
43138
  },
43641
43139
  onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
43642
43140
  buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
@@ -43791,52 +43289,10 @@ async function startDaemon(config) {
43791
43289
  }
43792
43290
  return `http://${config.host}:${config.port}`;
43793
43291
  };
43794
- const reportDevice = async () => {
43795
- if (!feishuActive) return;
43796
- const record = ownerIdentityStore.read();
43797
- if (!record) return;
43798
- try {
43799
- await centralUpsertDevice({
43800
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43801
- ttcJwt: record.ttcToken,
43802
- provider: record.identity.provider,
43803
- deviceId: authFile.deviceId,
43804
- url: currentTunnelUrl ?? void 0,
43805
- name: ownerDisplayName
43806
- });
43807
- logger.info("device registered to central server", {
43808
- deviceId: authFile.deviceId,
43809
- url: currentTunnelUrl ?? "(none)"
43810
- });
43811
- } catch (err) {
43812
- logger.warn("device register failed (best-effort)", { err: err.message });
43813
- }
43814
- };
43815
- const fetchServerKey = async () => {
43816
- if (!feishuActive) return;
43817
- if (serverKeyStore.read()) return;
43818
- try {
43819
- const { publicKeyPem } = await centralGetPublicKey({
43820
- api: config.clawosApi ?? DEFAULT_CLAWOS_API
43821
- });
43822
- serverKeyStore.write(publicKeyPem);
43823
- logger.info("server signing key cached");
43824
- } catch (err) {
43825
- logger.warn("server signing key fetch failed (guest \u5165\u7AD9\u5C06\u88AB\u62D2\u7EDD)", {
43826
- err: err.message
43827
- });
43828
- }
43829
- };
43830
43292
  const extensionRuntime = new Runtime({ root: extensionsRoot() });
43831
43293
  const publishedChannelStore = new PublishedChannelStore(publishedChannelsFile());
43832
43294
  await publishedChannelStore.load();
43833
43295
  const bundleCache = new BundleCache(bundleCacheRoot());
43834
- const loginFlow = new LoginFlow({
43835
- store: ownerIdentityStore,
43836
- fetchUserInfo: (ttcToken) => fetchTtcUserInfo({ apiBase: TTC_HOSTS.api, token: ttcToken }),
43837
- ttcAuthBase: TTC_HOSTS.auth,
43838
- getCallbackUrl: () => `http://127.0.0.1:${config.port}/auth/callback`
43839
- });
43840
43296
  const appBuilderStore = new ProjectStore(
43841
43297
  import_node_path45.default.join(config.dataDir, "personas/persona-app-builder")
43842
43298
  );
@@ -43847,7 +43303,7 @@ async function startDaemon(config) {
43847
43303
  const effectivePreviewPorts = Array.from(
43848
43304
  /* @__PURE__ */ new Set([...config.previewPorts ?? [], ...appBuilderPortRange])
43849
43305
  );
43850
- const makeHandlers = () => buildMethodHandlers({
43306
+ const handlers = buildMethodHandlers({
43851
43307
  manager,
43852
43308
  workspace,
43853
43309
  skills,
@@ -43892,12 +43348,15 @@ async function startDaemon(config) {
43892
43348
  personaRoot: import_node_path45.default.join(config.dataDir, "personas"),
43893
43349
  // capability:list / delete handler 依赖
43894
43350
  capabilityManager,
43351
+ // personal-cap:get 返回的 shareBaseUrl 用此 base URL:
43352
+ // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
43353
+ getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
43354
+ // global personal token (2026-05-25): personal-cap:get handler 直接返回此 token + cap
43355
+ getPersonalCapability: () => personalCapability,
43895
43356
  // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
43896
43357
  ownerDisplayName,
43897
- // 决策 #16: whoami owner.id = 本机 deviceId(路由主键)+ ownerId/provider 归属人
43358
+ // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
43898
43359
  ownerPrincipalId,
43899
- ownerId,
43900
- ownerProvider,
43901
43360
  personaStore,
43902
43361
  // capability handler 也用 (capability:issue 走 registry / capability:list)
43903
43362
  capabilityRegistry,
@@ -43905,56 +43364,18 @@ async function startDaemon(config) {
43905
43364
  // cascade 接 store (list 统计 deletedInboxEvents).
43906
43365
  inboxManager,
43907
43366
  inboxStore,
43908
- // 联系人列表 store(device:connect / 自动反向落同一 store
43909
- contactStore,
43910
- // contact:removed broadcast;复用 capability:tokenIssued 同款通路
43367
+ // bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
43368
+ receivedCapabilityStore,
43369
+ // received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
43911
43370
  broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
43371
+ // received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
43372
+ connectRemote,
43912
43373
  // extension runtime (v1: local-mode only). Instance owned by daemon top
43913
43374
  // level so stopAll() runs in the shutdown hook below.
43914
43375
  extensionRuntime,
43915
43376
  // extension sharing v1 — owner-side published-channels store.
43916
43377
  publishedChannelStore,
43917
43378
  bundleCache,
43918
- // 飞书统一身份 Phase 1:登录 RPC handlers(auth:login:start / getIdentity / logout)
43919
- // deviceId(决策 #15):getIdentity 返回给 UI 拼"我的设备标识"
43920
- feishuAuth: { loginFlow, ownerIdentityStore, deviceId: authFile.deviceId },
43921
- // 设备目录 + 连接(决策 #15:daemon 代理中心 server)。
43922
- // exchange/listDevices 包掉 owner ttcJwt(每次现读 store——热切换后立即用新身份);
43923
- // dispatcher 的 FEISHU_GATED_METHODS gate 已保证未登录时这些 RPC 到不了 handler,
43924
- // 这里的 FEISHU_LOGIN_REQUIRED 抛错只是防御纵深。
43925
- feishuDevice: {
43926
- store: contactStore,
43927
- exchange: async (deviceId) => {
43928
- const record = ownerIdentityStore.read();
43929
- if (!record) {
43930
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:connect requires feishu login");
43931
- }
43932
- return centralExchange({
43933
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43934
- jwt: record.ttcToken,
43935
- provider: record.identity.provider,
43936
- deviceId,
43937
- selfDeviceId: authFile.deviceId
43938
- });
43939
- },
43940
- listDevices: async () => {
43941
- const record = ownerIdentityStore.read();
43942
- if (!record) {
43943
- throw new ClawdError(ERROR_CODES.FEISHU_LOGIN_REQUIRED, "device:list requires feishu login");
43944
- }
43945
- return centralListDevices({
43946
- api: config.clawosApi ?? DEFAULT_CLAWOS_API,
43947
- ttcJwt: record.ttcToken,
43948
- provider: record.identity.provider
43949
- });
43950
- },
43951
- connectRemote,
43952
- broadcast: (frame) => wsServer?.broadcastToOwners(frame),
43953
- // 自动反向(spec 决策 #11):本机可达地址 = tunnel URL(公网),出站连接时
43954
- // 透传让对方反向加我为联系人。无 tunnel(仅本机 ws)→ null,对方不自动反向(不造假数据)。
43955
- // 决策 #16:身份不自报——本机 deviceId/ownerId/provider 由 exchange 签进 token 背书。
43956
- selfUrl: () => currentTunnelUrl
43957
- },
43958
43379
  // app-builder
43959
43380
  appBuilderStore,
43960
43381
  devServerLifecycle: devServerSupervisor,
@@ -43963,7 +43384,6 @@ async function startDaemon(config) {
43963
43384
  broadcastProjectUpdated: (event) => wsServer?.broadcastToOwners(event),
43964
43385
  logger
43965
43386
  });
43966
- let handlers = makeHandlers();
43967
43387
  const authResolver = new AuthContextResolver({
43968
43388
  ownerToken: resolvedAuthToken
43969
43389
  });
@@ -44001,31 +43421,6 @@ async function startDaemon(config) {
44001
43421
  },
44002
43422
  // POST /extensions/import lands in ~/.clawd/extensions/<id>/
44003
43423
  extensionsRoot: () => extensionsRoot(),
44004
- // 飞书登录 loopback 回调(热切换,2026-06-01 拍板):成功落盘后——
44005
- // 1. 进程内实时切换身份(let binding + 值快照持有者重建)
44006
- // 2. 广播 auth:login:done 给在线 owner 连接(UI 即时反馈)
44007
- // 3. 踢掉所有 ws 连接(在线连接的 ctx 是旧身份)→ UI 自动重连 → 新身份 ctx →
44008
- // People 立即解锁,不需要重启 daemon
44009
- // 失败则广播 auth:login:failed。
44010
- feishuLogin: {
44011
- handleLoginCallback: async (params) => {
44012
- const result = await loginFlow.handleCallback(params);
44013
- if (result.ok) {
44014
- feishuActive = true;
44015
- ownerId = result.identity.ownerId;
44016
- ownerProvider = result.identity.provider;
44017
- ownerDisplayName = result.identity.displayName;
44018
- handlers = makeHandlers();
44019
- wsServer?.broadcastToOwners({ type: "auth:login:done", identity: result.identity });
44020
- setImmediate(() => wsServer?.closeAllClients());
44021
- void reportDevice();
44022
- void fetchServerKey();
44023
- } else {
44024
- wsServer?.broadcastToOwners({ type: "auth:login:failed", reason: result.reason });
44025
- }
44026
- return result;
44027
- }
44028
- },
44029
43424
  // app-builder build 模式:/preview/<port>/ 反代 dev server 的端口白名单(见 spec §三.2)
44030
43425
  previewPorts: effectivePreviewPorts
44031
43426
  });
@@ -44033,6 +43428,8 @@ async function startDaemon(config) {
44033
43428
  host: config.host,
44034
43429
  port: config.port,
44035
43430
  logger,
43431
+ // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
43432
+ ownerPrincipalId,
44036
43433
  readyFrameBuilder: (ctx) => buildReadyFrame(
44037
43434
  {
44038
43435
  manager,
@@ -44052,12 +43449,14 @@ async function startDaemon(config) {
44052
43449
  // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
44053
43450
  // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
44054
43451
  // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
44055
- tryVerifyCapabilityToken: (token) => {
43452
+ tryVerifyCapabilityToken: (token, selfPrincipalId, _selfDisplayName) => {
44056
43453
  const v2 = capabilityRegistry.verifyToken(token);
44057
43454
  if (!v2.ok) return null;
44058
43455
  return {
44059
43456
  principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
44060
- grants: v2.capability.grants
43457
+ grants: v2.capability.grants,
43458
+ capabilityId: v2.capability.id,
43459
+ ...selfPrincipalId ? { peerOwnerPrincipalId: selfPrincipalId } : {}
44061
43460
  };
44062
43461
  },
44063
43462
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
@@ -44108,12 +43507,6 @@ async function startDaemon(config) {
44108
43507
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
44109
43508
  const handler = handlers[type];
44110
43509
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
44111
- if (!feishuActive && FEISHU_GATED_METHODS.includes(type)) {
44112
- throw new ClawdError(
44113
- ERROR_CODES.FEISHU_LOGIN_REQUIRED,
44114
- `${type} requires feishu login (People/remote identity)`
44115
- );
44116
- }
44117
43510
  const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
44118
43511
  const verdict = computeGrantForFrame(type, frame);
44119
43512
  if (verdict.kind === "check") {
@@ -44231,8 +43624,6 @@ ${bar}
44231
43624
  logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
44232
43625
  }
44233
43626
  }
44234
- void reportDevice();
44235
- void fetchServerKey();
44236
43627
  const tickAttachmentGc = () => {
44237
43628
  try {
44238
43629
  runAttachmentGc({