@clawos-dev/clawd 0.2.71-beta.137.a346461 → 0.2.71-beta.138.2a35bba

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 +718 -2359
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -92,6 +92,10 @@ var init_methods = __esm({
92
92
  "persona:get",
93
93
  "persona:update",
94
94
  "persona:delete",
95
+ // token 颁发 / 吊销:file-sharing HTTP Bearer 鉴权用(PersonaRegistry.findByToken
96
+ // 反查 personaId / label)。owner UI 在 PersonaSettingsDrawer "分享 Token" section 管理。
97
+ "persona:issueToken",
98
+ "persona:revokeToken",
95
99
  // ---- session:pty 双向透传(仅 CLAWD_CC_MODE=tui 才生效,对应 session:pty push 帧的上行) ----
96
100
  // session:pty:input — UI 把用户键盘字节(base64 UTF-8)发给 daemon,daemon 直写 pty stdin。
97
101
  // 高频帧(每次按键 1 帧),不入 reducer / 不广播。response 是 fire-and-forget 风格 ack。
@@ -102,50 +106,16 @@ var init_methods = __esm({
102
106
  // ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
103
107
  // 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
104
108
  // (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
105
- // 全部管理类 RPC dispatcher grant 限 ownercapability admin-only);
106
- // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,HMAC 签名自包含,不在白名单内)。
109
+ // 全部管理类 RPC handler 入口 requireOwnerpersonal token 调任意一个都 403);
110
+ // 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,签名验证),不在白名单内。
107
111
  "attachment.signUrl",
108
112
  "attachment.groupAdd",
109
113
  "attachment.groupRemove",
110
114
  "attachment.groupList",
111
- // ---- capability:* (capability platform 鉴权底座) ----
112
- // owner 颁发 / 列出 / 删除给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
113
- // 在 daemon 端固定为 `{ resource: '*', action: 'admin' }`,owner 自动满足)。
114
- // 颁发后 daemon 推 'capability:tokenIssued' 帧;删除推 'capability:tokenDeleted' 帧。
115
- // 删除是 hard delete:CapabilityStore 物理移除 + 关该 cap 的所有活跃 ws + rm
116
- // personas/<pid>/.clawd/sessions/guests/<capId>/ guest sessions 目录。
117
- "capability:issue",
118
- "capability:list",
119
- "capability:delete",
120
- // ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
121
- // owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
122
- // inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
123
- // 任何 principal (owner/guest) 可调 (Phase 4).
124
- "inbox:list",
125
- "inbox:markRead",
126
- "inbox:postMessage",
127
- // ---- remote-persona:* (Phase 4 远程 persona, v1 仅本地存储, 不接 outgoing WS) ----
128
- // owner 添加 / 列出 / 删除远程 persona 入口. admin-only.
129
- "remote-persona:add",
130
- "remote-persona:list",
131
- "remote-persona:remove",
132
- // ---- whoami (v2 capability platform) ----
133
- // 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
134
- // grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
135
- // 的 preview 一次性临时 client 用它判定身份和可用 persona。
136
- "whoami",
115
+ // v2:跨 session 聚合(本期 UI 不调,保留槽位用于 HTTP ACL 内部判定 / 未来 "All files" tab)
116
+ "attachment.groupListPersona",
137
117
  "info",
138
- "ping",
139
- // ---- person:* (Person identity Phase 1, spec v5) ----
140
- // Person 抽象 = People panel 顶层 entity(一个 Person 聚合 N 个 cap + M 个 remote
141
- // alias)。owner admin-only。daemon 维护 PersonStore + PersonAliasStore。
142
- // - list 返回 PersonWithLinks[](含派生 linkedCapabilityIds / linkedRemoteAliases)
143
- // - create / update / delete 操作 Person 实体(delete 触发 cascade:revoke 全部
144
- // linked cap + remove subscribed alias + clear linked inbox events)
145
- "person:list",
146
- "person:create",
147
- "person:update",
148
- "person:delete"
118
+ "ping"
149
119
  ];
150
120
  }
151
121
  });
@@ -635,8 +605,8 @@ var init_parseUtil = __esm({
635
605
  init_errors2();
636
606
  init_en();
637
607
  makeIssue = (params) => {
638
- const { data, path: path38, errorMaps, issueData } = params;
639
- const fullPath = [...path38, ...issueData.path || []];
608
+ const { data, path: path32, errorMaps, issueData } = params;
609
+ const fullPath = [...path32, ...issueData.path || []];
640
610
  const fullIssue = {
641
611
  ...issueData,
642
612
  path: fullPath
@@ -947,11 +917,11 @@ var init_types = __esm({
947
917
  init_parseUtil();
948
918
  init_util();
949
919
  ParseInputLazyPath = class {
950
- constructor(parent, value, path38, key) {
920
+ constructor(parent, value, path32, key) {
951
921
  this._cachedPath = [];
952
922
  this.parent = parent;
953
923
  this.data = value;
954
- this._path = path38;
924
+ this._path = path32;
955
925
  this._key = key;
956
926
  }
957
927
  get path() {
@@ -4335,12 +4305,12 @@ var init_zod = __esm({
4335
4305
  });
4336
4306
 
4337
4307
  // ../protocol/src/attachment-schemas.ts
4338
- var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
4308
+ var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema, AttachmentGroupListPersonaArgs, AttachmentGroupListPersonaResponseSchema;
4339
4309
  var init_attachment_schemas = __esm({
4340
4310
  "../protocol/src/attachment-schemas.ts"() {
4341
4311
  "use strict";
4342
4312
  init_zod();
4343
- TOKEN_ROLES = ["owner"];
4313
+ TOKEN_ROLES = ["owner", "personal"];
4344
4314
  GROUP_FILE_SOURCES = ["agent", "owner"];
4345
4315
  GroupFileEntrySchema = external_exports.object({
4346
4316
  /** daemon 派发的稳定 id(用于 RPC remove / UI key) */
@@ -4361,9 +4331,11 @@ var init_attachment_schemas = __esm({
4361
4331
  stale: external_exports.boolean().optional()
4362
4332
  });
4363
4333
  AttachmentSignUrlArgs = external_exports.object({
4364
- /** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
4365
- absPath: external_exports.string().min(1),
4366
- /** TTL 秒数;缺省 24h;'never' null 不带 exp 字段(永久有效) */
4334
+ /** 群文件所属的 session */
4335
+ sessionId: external_exports.string().min(1),
4336
+ /** 相对 session.cwd 的路径;允许传绝对路径,daemon 端会归一化(必须在 cwd 内) */
4337
+ relPath: external_exports.string().min(1),
4338
+ /** TTL 秒数;缺省 24h;null 走永久 URL(不带 exp 字段) */
4367
4339
  ttlSeconds: external_exports.number().int().positive().nullable().optional()
4368
4340
  });
4369
4341
  AttachmentSignUrlResponseSchema = external_exports.object({
@@ -4394,15 +4366,31 @@ var init_attachment_schemas = __esm({
4394
4366
  AttachmentGroupListResponseSchema = external_exports.object({
4395
4367
  entries: external_exports.array(GroupFileEntrySchema)
4396
4368
  });
4369
+ AttachmentGroupListPersonaArgs = external_exports.object({
4370
+ personaId: external_exports.string().min(1)
4371
+ });
4372
+ AttachmentGroupListPersonaResponseSchema = external_exports.object({
4373
+ perSession: external_exports.array(
4374
+ external_exports.object({
4375
+ sessionId: external_exports.string().min(1),
4376
+ entries: external_exports.array(GroupFileEntrySchema)
4377
+ })
4378
+ )
4379
+ });
4397
4380
  }
4398
4381
  });
4399
4382
 
4400
4383
  // ../protocol/src/persona-schemas.ts
4401
- var PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
4384
+ var PersonaTokenEntrySchema, PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema;
4402
4385
  var init_persona_schemas = __esm({
4403
4386
  "../protocol/src/persona-schemas.ts"() {
4404
4387
  "use strict";
4405
4388
  init_zod();
4389
+ PersonaTokenEntrySchema = external_exports.object({
4390
+ label: external_exports.string(),
4391
+ issuedAt: external_exports.number(),
4392
+ revoked: external_exports.boolean()
4393
+ });
4406
4394
  PersonaFileSchema = external_exports.object({
4407
4395
  personaId: external_exports.string(),
4408
4396
  label: external_exports.string(),
@@ -4411,6 +4399,13 @@ var init_persona_schemas = __esm({
4411
4399
  // 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
4412
4400
  // for forward compatibility, consumer fallbacks to default when key not found.
4413
4401
  iconKey: external_exports.string().optional(),
4402
+ /**
4403
+ * Persona token 列表:file-sharing HTTP Bearer 鉴权用。
4404
+ * - 写:`PersonaManager.issueToken` / `revokeToken`
4405
+ * - 读:`PersonaRegistry.findByToken` 反向查表(需 persona.public === true)
4406
+ * - optional:兼容历史 persona.json 不含此字段的情形(旧 daemon 写文件时省略)
4407
+ */
4408
+ tokenMap: external_exports.record(external_exports.string(), PersonaTokenEntrySchema).optional(),
4414
4409
  createdAt: external_exports.number(),
4415
4410
  updatedAt: external_exports.number()
4416
4411
  }).strict();
@@ -4420,6 +4415,10 @@ var init_persona_schemas = __esm({
4420
4415
  // SKILL.md frontmatter `description:`;缺省时省略字段
4421
4416
  description: external_exports.string().optional()
4422
4417
  });
4418
+ PersonaPluginSummarySchema = external_exports.object({
4419
+ // settings.json 中 enabledPlugins 的 key,例如 'superpowers@claude-plugins-official'
4420
+ id: external_exports.string().min(1)
4421
+ });
4423
4422
  PersonaSandboxSettingsSchema = external_exports.object({
4424
4423
  permissions: external_exports.object({
4425
4424
  defaultMode: external_exports.string().optional()
@@ -4440,6 +4439,8 @@ var init_persona_schemas = __esm({
4440
4439
  personality: external_exports.string().optional(),
4441
4440
  // 仅 'persona:get' 携带;null = 文件不存在 / parse 失败;undefined = 不在该响应类型上
4442
4441
  skills: external_exports.array(PersonaSkillSummarySchema).optional(),
4442
+ // 仅 'persona:get' 携带;空数组 = 没启用任何插件 / settings.json 不存在;undefined = 不在该响应类型上
4443
+ plugins: external_exports.array(PersonaPluginSummarySchema).optional(),
4443
4444
  sandboxSettings: PersonaSandboxSettingsSchema.nullable().optional()
4444
4445
  });
4445
4446
  PersonaCreateArgsSchema = external_exports.object({
@@ -4463,11 +4464,19 @@ var init_persona_schemas = __esm({
4463
4464
  iconKey: external_exports.string().nullable().optional()
4464
4465
  }).strict()
4465
4466
  }).strict();
4467
+ PersonaIssueTokenArgsSchema = external_exports.object({
4468
+ personaId: external_exports.string().min(1),
4469
+ label: external_exports.string().min(1)
4470
+ });
4471
+ PersonaRevokeTokenArgsSchema = external_exports.object({
4472
+ personaId: external_exports.string().min(1),
4473
+ token: external_exports.string().min(1)
4474
+ });
4466
4475
  }
4467
4476
  });
4468
4477
 
4469
4478
  // ../protocol/src/schemas.ts
4470
- var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4479
+ var SessionStatusSchema, UsageSchema, ContextUsageSchema, sessionMetaShape, SessionMetaSchema, ModelInfoSchema, ModeInfoSchema, ConfigFieldSchemaSchema, CapabilitiesGetArgs, CapabilitiesResponseSchema, AllowRuleSchema, SessionFileSchema, ParsedEventBase, HistoryUserMetaSchema, SubagentToolStatsSchema, StructuredPatchHunkSchema, ToolResultExtraSchema, MemoryEntrySchema, AskQuestionOptionSchema, AskQuestionItemSchema, ParsedEventSchema, SessionCreateArgs, SessionIdArgs, SessionUpdateArgs, SessionSendArgs, SessionRewindArgs, SessionRewindResponseSchema, SessionRewindDiffArgs, RewindDiffHunkSchema, RewindDiffFileSchema, SessionRewindDiffResponseSchema, SessionRewindableMessageIdsArgs, SessionRewindableMessageIdsResponseSchema, SessionResumeArgs, SessionForkArgs, SessionForkResponseSchema, SessionObserveArgs, SessionEventsArgs, PermissionRespondArgs, HistoryListArgs, HistoryReadArgs, HistorySubagentsArgs, HistorySubagentReadArgs, WorkspaceListArgs, WorkspaceReadArgs, SkillsListArgs, SkillEntrySchema, AgentEntrySchema, AgentsListArgs, AgentsListResponseSchema, SessionSubscribeArgs, SessionPinArgs, SessionReorderPinsArgs, GitRootArgs, GitRootResponseSchema, GitBranchArgs, GitBranchResponseSchema, GitBranchesArgs, GitBranchesResponseSchema, HistoryRecentDirsArgs, RecentDirEntrySchema, HistoryRecentDirsResponseSchema, SessionQuestionFrameSchema, SessionQuestionClearedFrameSchema, AnswerQuestionArgs, AnswerQuestionResponseSchema, CancelQuestionArgs, CancelQuestionResponseSchema, AuthRequestFrameSchema, AuthOkFrameSchema, TunnelExitedEventSchema, TunnelUnavailableEventSchema, InfoRunningSessionSchema, InfoResponseSchema;
4471
4480
  var init_schemas = __esm({
4472
4481
  "../protocol/src/schemas.ts"() {
4473
4482
  "use strict";
@@ -4569,15 +4578,6 @@ var init_schemas = __esm({
4569
4578
  // 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
4570
4579
  // optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
4571
4580
  chatId: external_exports.string().min(1).optional(),
4572
- /**
4573
- * 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
4574
- * 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
4575
- * UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
4576
- * 后者再通过 PersonAlias 表反查 personId 二级分组。
4577
- *
4578
- * optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
4579
- */
4580
- creatorPrincipalId: external_exports.string().min(1).optional(),
4581
4581
  createdAt: external_exports.string().min(1),
4582
4582
  updatedAt: external_exports.string().min(1)
4583
4583
  });
@@ -4987,18 +4987,7 @@ var init_schemas = __esm({
4987
4987
  AuthRequestFrameSchema = external_exports.object({
4988
4988
  type: external_exports.literal("auth"),
4989
4989
  token: external_exports.string().min(1),
4990
- scheme: external_exports.literal("bearer").optional(),
4991
- /**
4992
- * Person identity Phase 1: 如果 guest 自己也是 daemon owner,把自己的稳定
4993
- * ownerPrincipalId 主动上报,让对端 daemon 反查 PersonAliasStore 自动 link 到
4994
- * 已有 Person(避免出现"两个 Bob")。纯访客 / 老 daemon 客户端可省略。
4995
- *
4996
- * daemon transport/auth.ts 在 verify token 成功后:
4997
- * - 若 cap.linkedPrincipalId 未设 → 更新 cap.linkedPrincipalId = selfPrincipalId
4998
- * + PersonAliasStore.rekey(cap.id, selfPrincipalId)
4999
- * - 若已设但与本次不一致 → 仅 warn log(潜在协议异常但不阻断)
5000
- */
5001
- selfPrincipalId: external_exports.string().min(1).optional()
4990
+ scheme: external_exports.literal("bearer").optional()
5002
4991
  });
5003
4992
  AuthOkFrameSchema = external_exports.object({
5004
4993
  type: external_exports.literal("auth:ok"),
@@ -5011,6 +5000,11 @@ var init_schemas = __esm({
5011
5000
  subdomain: external_exports.string().nullable(),
5012
5001
  url: external_exports.string().nullable()
5013
5002
  });
5003
+ TunnelUnavailableEventSchema = external_exports.object({
5004
+ type: external_exports.literal("tunnel:unavailable"),
5005
+ reason: external_exports.string().min(1),
5006
+ failedAt: external_exports.string().min(1)
5007
+ });
5014
5008
  InfoRunningSessionSchema = external_exports.object({
5015
5009
  sessionId: external_exports.string().min(1),
5016
5010
  status: SessionStatusSchema,
@@ -5031,9 +5025,10 @@ var init_schemas = __esm({
5031
5025
  // PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
5032
5026
  // 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
5033
5027
  // 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
5034
- /** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
5035
- * 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
5028
+ /** 'owner' = 拿到 owner-token 的连接(不限来源 IP);'personal' = persona 派发的访客 token。来源 TOKEN_ROLES(spec §11 #1 中央真理源) */
5036
5029
  tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
5030
+ /** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
5031
+ tokenPersonaId: external_exports.string().min(1).optional(),
5037
5032
  /** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
5038
5033
  isLoopback: external_exports.boolean().optional(),
5039
5034
  /** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
@@ -5060,239 +5055,6 @@ var init_persona_mode = __esm({
5060
5055
  }
5061
5056
  });
5062
5057
 
5063
- // ../protocol/src/principal.ts
5064
- function makeOwnerPrincipal(id, displayName) {
5065
- return PrincipalSchema.parse({ id, kind: "owner", displayName });
5066
- }
5067
- var PrincipalKindSchema, PrincipalSchema;
5068
- var init_principal = __esm({
5069
- "../protocol/src/principal.ts"() {
5070
- "use strict";
5071
- init_zod();
5072
- PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
5073
- PrincipalSchema = external_exports.object({
5074
- id: external_exports.string().min(1),
5075
- kind: PrincipalKindSchema,
5076
- displayName: external_exports.string()
5077
- }).strict();
5078
- }
5079
- });
5080
-
5081
- // ../protocol/src/capability.ts
5082
- function stripSecretHash(cap) {
5083
- const { secretHash: _hash, ...wire } = cap;
5084
- return wire;
5085
- }
5086
- var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema;
5087
- var init_capability = __esm({
5088
- "../protocol/src/capability.ts"() {
5089
- "use strict";
5090
- init_zod();
5091
- ResourceSchema = external_exports.discriminatedUnion("type", [
5092
- external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
5093
- external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
5094
- external_exports.object({ type: external_exports.literal("*") }).strict()
5095
- ]);
5096
- ActionSchema = external_exports.enum(["read", "send", "admin"]);
5097
- GrantSchema = external_exports.object({
5098
- resource: ResourceSchema,
5099
- actions: external_exports.array(ActionSchema).min(1)
5100
- }).strict();
5101
- CapabilitySchema = external_exports.object({
5102
- id: external_exports.string().min(1),
5103
- // sha256(token) hex 64
5104
- secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
5105
- displayName: external_exports.string(),
5106
- // 空数组合法 = 纯访客(只能 DM)
5107
- grants: external_exports.array(GrantSchema),
5108
- issuedAt: external_exports.number().int().nonnegative(),
5109
- expiresAt: external_exports.number().int().positive().optional(),
5110
- maxUses: external_exports.number().int().positive().optional(),
5111
- usedCount: external_exports.number().int().nonnegative(),
5112
- /**
5113
- * Person identity per-People token (2026-05-21): cap 直接绑死 Person 主人。
5114
- * 一 Person 一 cap:capability:issue 同 person 第二次调用 = 合并 grants,
5115
- * 不新建 cap。删 PersonAlias 表整套(cap.personId 直接含信息,无需反查)。
5116
- */
5117
- personId: external_exports.string().min(1)
5118
- }).strict();
5119
- CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
5120
- CapabilityErrorCodeSchema = external_exports.enum([
5121
- "TOKEN_INVALID",
5122
- "TOKEN_REVOKED",
5123
- "TOKEN_EXPIRED",
5124
- "TOKEN_EXHAUSTED"
5125
- ]);
5126
- WhoamiResponseSchema = external_exports.object({
5127
- type: external_exports.literal("whoami:ok"),
5128
- owner: external_exports.object({
5129
- id: external_exports.string(),
5130
- kind: external_exports.enum(["owner", "guest"]),
5131
- displayName: external_exports.string()
5132
- }).strict(),
5133
- capability: CapabilityWireSchema,
5134
- grantedPersonas: external_exports.array(
5135
- external_exports.object({
5136
- id: external_exports.string().min(1),
5137
- displayName: external_exports.string()
5138
- }).strict()
5139
- )
5140
- }).strict();
5141
- CapabilityIssueArgsSchema = external_exports.object({
5142
- displayName: external_exports.string().min(1),
5143
- grants: external_exports.array(GrantSchema),
5144
- personId: external_exports.string().min(1),
5145
- expiresAt: external_exports.number().int().positive().optional(),
5146
- maxUses: external_exports.number().int().positive().optional()
5147
- }).strict();
5148
- }
5149
- });
5150
-
5151
- // ../protocol/src/inbox.ts
5152
- var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
5153
- var init_inbox = __esm({
5154
- "../protocol/src/inbox.ts"() {
5155
- "use strict";
5156
- init_zod();
5157
- InboxMessageSchema = external_exports.object({
5158
- id: external_exports.string().min(1),
5159
- capabilityId: external_exports.string().min(1),
5160
- senderPrincipalId: external_exports.string().min(1),
5161
- text: external_exports.string().min(1),
5162
- createdAt: external_exports.number().int().nonnegative(),
5163
- readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
5164
- }).strict();
5165
- InboxPostMessageArgsSchema = external_exports.object({
5166
- capabilityId: external_exports.string().min(1),
5167
- text: external_exports.string().min(1)
5168
- }).strict();
5169
- InboxListArgsSchema = external_exports.object({
5170
- capabilityId: external_exports.string().min(1),
5171
- sinceCreatedAt: external_exports.number().int().nonnegative().optional()
5172
- }).strict();
5173
- InboxMarkReadArgsSchema = external_exports.object({
5174
- capabilityId: external_exports.string().min(1),
5175
- upToCreatedAt: external_exports.number().int().nonnegative()
5176
- }).strict();
5177
- }
5178
- });
5179
-
5180
- // ../protocol/src/remote-persona.ts
5181
- function stripRemotePersonaSecret(rp) {
5182
- const { capabilityToken: _t, ...wire } = rp;
5183
- return wire;
5184
- }
5185
- var RemotePersonaSchema, RemotePersonaWireSchema, RemotePersonaAddArgsSchema, RemotePersonaRemoveArgsSchema;
5186
- var init_remote_persona = __esm({
5187
- "../protocol/src/remote-persona.ts"() {
5188
- "use strict";
5189
- init_zod();
5190
- RemotePersonaSchema = external_exports.object({
5191
- alias: external_exports.string().min(1),
5192
- remoteUrl: external_exports.string().min(1),
5193
- capabilityToken: external_exports.string().min(1),
5194
- // v2 Phase 7: 远端 daemon 给我颁发的 capability id。preview 时 whoami 返回的
5195
- // capability.id 直接存进来。旧 v1 持久化文件没此字段 → schema parse fail →
5196
- // RemotePersonaStore.list 静默跳过 (alpha 阶段可接受, 老板重新 add 即可)。
5197
- myCapabilityId: external_exports.string().min(1),
5198
- // owner-id stabilization: 远端 daemon 的稳定 ownerPrincipalId(whoami.owner.id)。
5199
- // UI DM 时作为 peerPrincipalId 传给远端 daemon(取代字面量 'owner' sentinel),
5200
- // 让 inbox event from/to 路由 + UI useDm fromOwner 判定都用真实 id。
5201
- // 旧持久化文件缺该字段 → schema parse fail → store.list 静默跳过(同 myCapabilityId)。
5202
- ownerPrincipalId: external_exports.string().min(1),
5203
- remotePersonaId: external_exports.string().min(1),
5204
- remoteDisplayName: external_exports.string(),
5205
- ownerDisplayName: external_exports.string().optional(),
5206
- addedAt: external_exports.number().int().nonnegative(),
5207
- lastConnectedAt: external_exports.number().int().positive().optional(),
5208
- /**
5209
- * Person identity per-People token (2026-05-21): RemotePersona 直接绑死本地 Person。
5210
- * 删 PersonAlias 表后由该字段提供"远端 owner principal 对应本地哪个 person"。
5211
- */
5212
- personId: external_exports.string().min(1)
5213
- }).strict();
5214
- RemotePersonaWireSchema = RemotePersonaSchema.omit({ capabilityToken: true });
5215
- RemotePersonaAddArgsSchema = external_exports.object({
5216
- alias: external_exports.string().min(1),
5217
- remoteUrl: external_exports.string().min(1),
5218
- capabilityToken: external_exports.string().min(1),
5219
- // v2 Phase 7: UI 调 preview 后从 whoami response 取 capability.id 传入
5220
- myCapabilityId: external_exports.string().min(1),
5221
- // owner-id stabilization: UI 从 whoami.owner.id 取,远端 daemon 稳定身份
5222
- ownerPrincipalId: external_exports.string().min(1),
5223
- remotePersonaId: external_exports.string().min(1),
5224
- remoteDisplayName: external_exports.string(),
5225
- ownerDisplayName: external_exports.string().optional(),
5226
- /**
5227
- * Person identity per-People token: add 时必须关联到一个 Person(owner 视角的"对方是谁")。
5228
- * daemon handler 内 atomic 写 RemotePersona.personId(PersonAlias 表 2026-05-21 删除)。
5229
- */
5230
- personId: external_exports.string().min(1)
5231
- }).strict();
5232
- RemotePersonaRemoveArgsSchema = external_exports.object({
5233
- alias: external_exports.string().min(1)
5234
- }).strict();
5235
- }
5236
- });
5237
-
5238
- // ../protocol/src/person.ts
5239
- var PersonSchema, PersonWithLinksSchema, PersonCreateArgsSchema, PersonUpdateArgsSchema, PersonDeleteArgsSchema, PersonListResponseSchema, PersonCreateResponseSchema, PersonUpdateResponseSchema, PersonDeleteResponseSchema;
5240
- var init_person = __esm({
5241
- "../protocol/src/person.ts"() {
5242
- "use strict";
5243
- init_zod();
5244
- PersonSchema = external_exports.object({
5245
- id: external_exports.string().min(1),
5246
- displayName: external_exports.string().min(1),
5247
- notes: external_exports.string().optional(),
5248
- /** 默认 true(owner 可关闭以阻断该 Person 的入站 DM) */
5249
- dmEnabled: external_exports.boolean(),
5250
- createdAt: external_exports.number().int().nonnegative(),
5251
- updatedAt: external_exports.number().int().nonnegative()
5252
- }).strict();
5253
- PersonWithLinksSchema = PersonSchema.extend({
5254
- linkedCapabilityIds: external_exports.array(external_exports.string()),
5255
- linkedRemoteAliases: external_exports.array(external_exports.string())
5256
- }).strict();
5257
- PersonCreateArgsSchema = external_exports.object({
5258
- displayName: external_exports.string().min(1),
5259
- notes: external_exports.string().optional(),
5260
- /** 缺省 true(在 daemon handler 内 default) */
5261
- dmEnabled: external_exports.boolean().optional()
5262
- }).strict();
5263
- PersonUpdateArgsSchema = external_exports.object({
5264
- id: external_exports.string().min(1),
5265
- displayName: external_exports.string().min(1).optional(),
5266
- notes: external_exports.string().optional(),
5267
- dmEnabled: external_exports.boolean().optional()
5268
- }).strict();
5269
- PersonDeleteArgsSchema = external_exports.object({
5270
- id: external_exports.string().min(1)
5271
- }).strict();
5272
- PersonListResponseSchema = external_exports.object({
5273
- type: external_exports.literal("person:list:ok"),
5274
- persons: external_exports.array(PersonWithLinksSchema)
5275
- }).strict();
5276
- PersonCreateResponseSchema = external_exports.object({
5277
- type: external_exports.literal("person:create:ok"),
5278
- person: PersonSchema
5279
- }).strict();
5280
- PersonUpdateResponseSchema = external_exports.object({
5281
- type: external_exports.literal("person:update:ok"),
5282
- person: PersonSchema
5283
- }).strict();
5284
- PersonDeleteResponseSchema = external_exports.object({
5285
- type: external_exports.literal("person:delete:ok"),
5286
- /** cascade 删了哪些 capability id */
5287
- deletedCapabilityIds: external_exports.array(external_exports.string()),
5288
- /** cascade 移除了哪些 remote alias */
5289
- removedRemoteAliases: external_exports.array(external_exports.string()),
5290
- /** cascade 清掉了多少条 inbox 事件 */
5291
- deletedInboxEvents: external_exports.number().int().nonnegative()
5292
- }).strict();
5293
- }
5294
- });
5295
-
5296
5058
  // ../protocol/src/runtime.ts
5297
5059
  var init_runtime = __esm({
5298
5060
  "../protocol/src/runtime.ts"() {
@@ -5305,11 +5067,6 @@ var init_runtime = __esm({
5305
5067
  init_persona_schemas();
5306
5068
  init_persona_mode();
5307
5069
  init_attachment_schemas();
5308
- init_principal();
5309
- init_capability();
5310
- init_inbox();
5311
- init_remote_persona();
5312
- init_person();
5313
5070
  }
5314
5071
  });
5315
5072
 
@@ -5584,8 +5341,8 @@ var require_req = __commonJS({
5584
5341
  if (req.originalUrl) {
5585
5342
  _req.url = req.originalUrl;
5586
5343
  } else {
5587
- const path38 = req.path;
5588
- _req.url = typeof path38 === "string" ? path38 : req.url ? req.url.path || req.url : void 0;
5344
+ const path32 = req.path;
5345
+ _req.url = typeof path32 === "string" ? path32 : req.url ? req.url.path || req.url : void 0;
5589
5346
  }
5590
5347
  if (req.query) {
5591
5348
  _req.query = req.query;
@@ -5750,14 +5507,14 @@ var require_redact = __commonJS({
5750
5507
  }
5751
5508
  return obj;
5752
5509
  }
5753
- function parsePath(path38) {
5510
+ function parsePath(path32) {
5754
5511
  const parts = [];
5755
5512
  let current = "";
5756
5513
  let inBrackets = false;
5757
5514
  let inQuotes = false;
5758
5515
  let quoteChar = "";
5759
- for (let i = 0; i < path38.length; i++) {
5760
- const char = path38[i];
5516
+ for (let i = 0; i < path32.length; i++) {
5517
+ const char = path32[i];
5761
5518
  if (!inBrackets && char === ".") {
5762
5519
  if (current) {
5763
5520
  parts.push(current);
@@ -5888,10 +5645,10 @@ var require_redact = __commonJS({
5888
5645
  return current;
5889
5646
  }
5890
5647
  function redactPaths(obj, paths, censor, remove = false) {
5891
- for (const path38 of paths) {
5892
- const parts = parsePath(path38);
5648
+ for (const path32 of paths) {
5649
+ const parts = parsePath(path32);
5893
5650
  if (parts.includes("*")) {
5894
- redactWildcardPath(obj, parts, censor, path38, remove);
5651
+ redactWildcardPath(obj, parts, censor, path32, remove);
5895
5652
  } else {
5896
5653
  if (remove) {
5897
5654
  removeKey(obj, parts);
@@ -5976,8 +5733,8 @@ var require_redact = __commonJS({
5976
5733
  }
5977
5734
  } else {
5978
5735
  if (afterWildcard.includes("*")) {
5979
- const wrappedCensor = typeof censor === "function" ? (value, path38) => {
5980
- const fullPath = [...pathArray.slice(0, pathLength), ...path38];
5736
+ const wrappedCensor = typeof censor === "function" ? (value, path32) => {
5737
+ const fullPath = [...pathArray.slice(0, pathLength), ...path32];
5981
5738
  return censor(value, fullPath);
5982
5739
  } : censor;
5983
5740
  redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
@@ -6012,8 +5769,8 @@ var require_redact = __commonJS({
6012
5769
  return null;
6013
5770
  }
6014
5771
  const pathStructure = /* @__PURE__ */ new Map();
6015
- for (const path38 of pathsToClone) {
6016
- const parts = parsePath(path38);
5772
+ for (const path32 of pathsToClone) {
5773
+ const parts = parsePath(path32);
6017
5774
  let current = pathStructure;
6018
5775
  for (let i = 0; i < parts.length; i++) {
6019
5776
  const part = parts[i];
@@ -6065,24 +5822,24 @@ var require_redact = __commonJS({
6065
5822
  }
6066
5823
  return cloneSelectively(obj, pathStructure);
6067
5824
  }
6068
- function validatePath(path38) {
6069
- if (typeof path38 !== "string") {
5825
+ function validatePath(path32) {
5826
+ if (typeof path32 !== "string") {
6070
5827
  throw new Error("Paths must be (non-empty) strings");
6071
5828
  }
6072
- if (path38 === "") {
5829
+ if (path32 === "") {
6073
5830
  throw new Error("Invalid redaction path ()");
6074
5831
  }
6075
- if (path38.includes("..")) {
6076
- throw new Error(`Invalid redaction path (${path38})`);
5832
+ if (path32.includes("..")) {
5833
+ throw new Error(`Invalid redaction path (${path32})`);
6077
5834
  }
6078
- if (path38.includes(",")) {
6079
- throw new Error(`Invalid redaction path (${path38})`);
5835
+ if (path32.includes(",")) {
5836
+ throw new Error(`Invalid redaction path (${path32})`);
6080
5837
  }
6081
5838
  let bracketCount = 0;
6082
5839
  let inQuotes = false;
6083
5840
  let quoteChar = "";
6084
- for (let i = 0; i < path38.length; i++) {
6085
- const char = path38[i];
5841
+ for (let i = 0; i < path32.length; i++) {
5842
+ const char = path32[i];
6086
5843
  if ((char === '"' || char === "'") && bracketCount > 0) {
6087
5844
  if (!inQuotes) {
6088
5845
  inQuotes = true;
@@ -6096,20 +5853,20 @@ var require_redact = __commonJS({
6096
5853
  } else if (char === "]" && !inQuotes) {
6097
5854
  bracketCount--;
6098
5855
  if (bracketCount < 0) {
6099
- throw new Error(`Invalid redaction path (${path38})`);
5856
+ throw new Error(`Invalid redaction path (${path32})`);
6100
5857
  }
6101
5858
  }
6102
5859
  }
6103
5860
  if (bracketCount !== 0) {
6104
- throw new Error(`Invalid redaction path (${path38})`);
5861
+ throw new Error(`Invalid redaction path (${path32})`);
6105
5862
  }
6106
5863
  }
6107
5864
  function validatePaths(paths) {
6108
5865
  if (!Array.isArray(paths)) {
6109
5866
  throw new TypeError("paths must be an array");
6110
5867
  }
6111
- for (const path38 of paths) {
6112
- validatePath(path38);
5868
+ for (const path32 of paths) {
5869
+ validatePath(path32);
6113
5870
  }
6114
5871
  }
6115
5872
  function slowRedact(options = {}) {
@@ -6277,8 +6034,8 @@ var require_redaction = __commonJS({
6277
6034
  if (shape[k2] === null) {
6278
6035
  o[k2] = (value) => topCensor(value, [k2]);
6279
6036
  } else {
6280
- const wrappedCensor = typeof censor === "function" ? (value, path38) => {
6281
- return censor(value, [k2, ...path38]);
6037
+ const wrappedCensor = typeof censor === "function" ? (value, path32) => {
6038
+ return censor(value, [k2, ...path32]);
6282
6039
  } : censor;
6283
6040
  o[k2] = Redact({
6284
6041
  paths: shape[k2],
@@ -6496,10 +6253,10 @@ var require_atomic_sleep = __commonJS({
6496
6253
  var require_sonic_boom = __commonJS({
6497
6254
  "../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
6498
6255
  "use strict";
6499
- var fs34 = require("fs");
6256
+ var fs28 = require("fs");
6500
6257
  var EventEmitter2 = require("events");
6501
6258
  var inherits = require("util").inherits;
6502
- var path38 = require("path");
6259
+ var path32 = require("path");
6503
6260
  var sleep = require_atomic_sleep();
6504
6261
  var assert = require("assert");
6505
6262
  var BUSY_WRITE_TIMEOUT = 100;
@@ -6553,20 +6310,20 @@ var require_sonic_boom = __commonJS({
6553
6310
  const mode = sonic.mode;
6554
6311
  if (sonic.sync) {
6555
6312
  try {
6556
- if (sonic.mkdir) fs34.mkdirSync(path38.dirname(file), { recursive: true });
6557
- const fd = fs34.openSync(file, flags, mode);
6313
+ if (sonic.mkdir) fs28.mkdirSync(path32.dirname(file), { recursive: true });
6314
+ const fd = fs28.openSync(file, flags, mode);
6558
6315
  fileOpened(null, fd);
6559
6316
  } catch (err) {
6560
6317
  fileOpened(err);
6561
6318
  throw err;
6562
6319
  }
6563
6320
  } else if (sonic.mkdir) {
6564
- fs34.mkdir(path38.dirname(file), { recursive: true }, (err) => {
6321
+ fs28.mkdir(path32.dirname(file), { recursive: true }, (err) => {
6565
6322
  if (err) return fileOpened(err);
6566
- fs34.open(file, flags, mode, fileOpened);
6323
+ fs28.open(file, flags, mode, fileOpened);
6567
6324
  });
6568
6325
  } else {
6569
- fs34.open(file, flags, mode, fileOpened);
6326
+ fs28.open(file, flags, mode, fileOpened);
6570
6327
  }
6571
6328
  }
6572
6329
  function SonicBoom(opts) {
@@ -6607,8 +6364,8 @@ var require_sonic_boom = __commonJS({
6607
6364
  this.flush = flushBuffer;
6608
6365
  this.flushSync = flushBufferSync;
6609
6366
  this._actualWrite = actualWriteBuffer;
6610
- fsWriteSync = () => fs34.writeSync(this.fd, this._writingBuf);
6611
- fsWrite = () => fs34.write(this.fd, this._writingBuf, this.release);
6367
+ fsWriteSync = () => fs28.writeSync(this.fd, this._writingBuf);
6368
+ fsWrite = () => fs28.write(this.fd, this._writingBuf, this.release);
6612
6369
  } else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
6613
6370
  this._writingBuf = "";
6614
6371
  this.write = write;
@@ -6617,15 +6374,15 @@ var require_sonic_boom = __commonJS({
6617
6374
  this._actualWrite = actualWrite;
6618
6375
  fsWriteSync = () => {
6619
6376
  if (Buffer.isBuffer(this._writingBuf)) {
6620
- return fs34.writeSync(this.fd, this._writingBuf);
6377
+ return fs28.writeSync(this.fd, this._writingBuf);
6621
6378
  }
6622
- return fs34.writeSync(this.fd, this._writingBuf, "utf8");
6379
+ return fs28.writeSync(this.fd, this._writingBuf, "utf8");
6623
6380
  };
6624
6381
  fsWrite = () => {
6625
6382
  if (Buffer.isBuffer(this._writingBuf)) {
6626
- return fs34.write(this.fd, this._writingBuf, this.release);
6383
+ return fs28.write(this.fd, this._writingBuf, this.release);
6627
6384
  }
6628
- return fs34.write(this.fd, this._writingBuf, "utf8", this.release);
6385
+ return fs28.write(this.fd, this._writingBuf, "utf8", this.release);
6629
6386
  };
6630
6387
  } else {
6631
6388
  throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
@@ -6682,7 +6439,7 @@ var require_sonic_boom = __commonJS({
6682
6439
  }
6683
6440
  }
6684
6441
  if (this._fsync) {
6685
- fs34.fsyncSync(this.fd);
6442
+ fs28.fsyncSync(this.fd);
6686
6443
  }
6687
6444
  const len = this._len;
6688
6445
  if (this._reopening) {
@@ -6796,7 +6553,7 @@ var require_sonic_boom = __commonJS({
6796
6553
  const onDrain = () => {
6797
6554
  if (!this._fsync) {
6798
6555
  try {
6799
- fs34.fsync(this.fd, (err) => {
6556
+ fs28.fsync(this.fd, (err) => {
6800
6557
  this._flushPending = false;
6801
6558
  cb(err);
6802
6559
  });
@@ -6898,7 +6655,7 @@ var require_sonic_boom = __commonJS({
6898
6655
  const fd = this.fd;
6899
6656
  this.once("ready", () => {
6900
6657
  if (fd !== this.fd) {
6901
- fs34.close(fd, (err) => {
6658
+ fs28.close(fd, (err) => {
6902
6659
  if (err) {
6903
6660
  return this.emit("error", err);
6904
6661
  }
@@ -6947,7 +6704,7 @@ var require_sonic_boom = __commonJS({
6947
6704
  buf = this._bufs[0];
6948
6705
  }
6949
6706
  try {
6950
- const n = Buffer.isBuffer(buf) ? fs34.writeSync(this.fd, buf) : fs34.writeSync(this.fd, buf, "utf8");
6707
+ const n = Buffer.isBuffer(buf) ? fs28.writeSync(this.fd, buf) : fs28.writeSync(this.fd, buf, "utf8");
6951
6708
  const releasedBufObj = releaseWritingBuf(buf, this._len, n);
6952
6709
  buf = releasedBufObj.writingBuf;
6953
6710
  this._len = releasedBufObj.len;
@@ -6963,7 +6720,7 @@ var require_sonic_boom = __commonJS({
6963
6720
  }
6964
6721
  }
6965
6722
  try {
6966
- fs34.fsyncSync(this.fd);
6723
+ fs28.fsyncSync(this.fd);
6967
6724
  } catch {
6968
6725
  }
6969
6726
  }
@@ -6984,7 +6741,7 @@ var require_sonic_boom = __commonJS({
6984
6741
  buf = mergeBuf(this._bufs[0], this._lens[0]);
6985
6742
  }
6986
6743
  try {
6987
- const n = fs34.writeSync(this.fd, buf);
6744
+ const n = fs28.writeSync(this.fd, buf);
6988
6745
  buf = buf.subarray(n);
6989
6746
  this._len = Math.max(this._len - n, 0);
6990
6747
  if (buf.length <= 0) {
@@ -7012,13 +6769,13 @@ var require_sonic_boom = __commonJS({
7012
6769
  this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
7013
6770
  if (this.sync) {
7014
6771
  try {
7015
- const written = Buffer.isBuffer(this._writingBuf) ? fs34.writeSync(this.fd, this._writingBuf) : fs34.writeSync(this.fd, this._writingBuf, "utf8");
6772
+ const written = Buffer.isBuffer(this._writingBuf) ? fs28.writeSync(this.fd, this._writingBuf) : fs28.writeSync(this.fd, this._writingBuf, "utf8");
7016
6773
  release(null, written);
7017
6774
  } catch (err) {
7018
6775
  release(err);
7019
6776
  }
7020
6777
  } else {
7021
- fs34.write(this.fd, this._writingBuf, release);
6778
+ fs28.write(this.fd, this._writingBuf, release);
7022
6779
  }
7023
6780
  }
7024
6781
  function actualWriteBuffer() {
@@ -7027,7 +6784,7 @@ var require_sonic_boom = __commonJS({
7027
6784
  this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
7028
6785
  if (this.sync) {
7029
6786
  try {
7030
- const written = fs34.writeSync(this.fd, this._writingBuf);
6787
+ const written = fs28.writeSync(this.fd, this._writingBuf);
7031
6788
  release(null, written);
7032
6789
  } catch (err) {
7033
6790
  release(err);
@@ -7036,7 +6793,7 @@ var require_sonic_boom = __commonJS({
7036
6793
  if (kCopyBuffer) {
7037
6794
  this._writingBuf = Buffer.from(this._writingBuf);
7038
6795
  }
7039
- fs34.write(this.fd, this._writingBuf, release);
6796
+ fs28.write(this.fd, this._writingBuf, release);
7040
6797
  }
7041
6798
  }
7042
6799
  function actualClose(sonic) {
@@ -7052,12 +6809,12 @@ var require_sonic_boom = __commonJS({
7052
6809
  sonic._lens = [];
7053
6810
  assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
7054
6811
  try {
7055
- fs34.fsync(sonic.fd, closeWrapped);
6812
+ fs28.fsync(sonic.fd, closeWrapped);
7056
6813
  } catch {
7057
6814
  }
7058
6815
  function closeWrapped() {
7059
6816
  if (sonic.fd !== 1 && sonic.fd !== 2) {
7060
- fs34.close(sonic.fd, done);
6817
+ fs28.close(sonic.fd, done);
7061
6818
  } else {
7062
6819
  done();
7063
6820
  }
@@ -7314,7 +7071,7 @@ var require_thread_stream = __commonJS({
7314
7071
  var { version: version2 } = require_package();
7315
7072
  var { EventEmitter: EventEmitter2 } = require("events");
7316
7073
  var { Worker } = require("worker_threads");
7317
- var { join: join11 } = require("path");
7074
+ var { join: join4 } = require("path");
7318
7075
  var { pathToFileURL } = require("url");
7319
7076
  var { wait } = require_wait();
7320
7077
  var {
@@ -7350,7 +7107,7 @@ var require_thread_stream = __commonJS({
7350
7107
  function createWorker(stream, opts) {
7351
7108
  const { filename, workerData } = opts;
7352
7109
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
7353
- const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
7110
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join4(__dirname, "lib", "worker.js");
7354
7111
  const worker = new Worker(toExecute, {
7355
7112
  ...opts.workerOpts,
7356
7113
  trackUnmanagedFds: false,
@@ -7736,7 +7493,7 @@ var require_transport = __commonJS({
7736
7493
  "use strict";
7737
7494
  var { createRequire } = require("module");
7738
7495
  var getCallers = require_caller();
7739
- var { join: join11, isAbsolute, sep: sep2 } = require("path");
7496
+ var { join: join4, isAbsolute, sep } = require("path");
7740
7497
  var sleep = require_atomic_sleep();
7741
7498
  var onExit = require_on_exit_leak_free();
7742
7499
  var ThreadStream = require_thread_stream();
@@ -7799,7 +7556,7 @@ var require_transport = __commonJS({
7799
7556
  throw new Error("only one of target or targets can be specified");
7800
7557
  }
7801
7558
  if (targets) {
7802
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7559
+ target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7803
7560
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
7804
7561
  return {
7805
7562
  ...dest,
@@ -7817,7 +7574,7 @@ var require_transport = __commonJS({
7817
7574
  });
7818
7575
  });
7819
7576
  } else if (pipeline2) {
7820
- target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
7577
+ target = bundlerOverrides["pino-worker"] || join4(__dirname, "worker.js");
7821
7578
  options.pipelines = [pipeline2.map((dest) => {
7822
7579
  return {
7823
7580
  ...dest,
@@ -7839,12 +7596,12 @@ var require_transport = __commonJS({
7839
7596
  return origin;
7840
7597
  }
7841
7598
  if (origin === "pino/file") {
7842
- return join11(__dirname, "..", "file.js");
7599
+ return join4(__dirname, "..", "file.js");
7843
7600
  }
7844
7601
  let fixTarget2;
7845
7602
  for (const filePath of callers) {
7846
7603
  try {
7847
- const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
7604
+ const context = filePath === "node:repl" ? process.cwd() + sep : filePath;
7848
7605
  fixTarget2 = createRequire(context).resolve(origin);
7849
7606
  break;
7850
7607
  } catch (err) {
@@ -8829,7 +8586,7 @@ var require_safe_stable_stringify = __commonJS({
8829
8586
  return circularValue;
8830
8587
  }
8831
8588
  let res = "";
8832
- let join11 = ",";
8589
+ let join4 = ",";
8833
8590
  const originalIndentation = indentation;
8834
8591
  if (Array.isArray(value)) {
8835
8592
  if (value.length === 0) {
@@ -8843,7 +8600,7 @@ var require_safe_stable_stringify = __commonJS({
8843
8600
  indentation += spacer;
8844
8601
  res += `
8845
8602
  ${indentation}`;
8846
- join11 = `,
8603
+ join4 = `,
8847
8604
  ${indentation}`;
8848
8605
  }
8849
8606
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8851,13 +8608,13 @@ ${indentation}`;
8851
8608
  for (; i < maximumValuesToStringify - 1; i++) {
8852
8609
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8853
8610
  res += tmp2 !== void 0 ? tmp2 : "null";
8854
- res += join11;
8611
+ res += join4;
8855
8612
  }
8856
8613
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
8857
8614
  res += tmp !== void 0 ? tmp : "null";
8858
8615
  if (value.length - 1 > maximumBreadth) {
8859
8616
  const removedKeys = value.length - maximumBreadth - 1;
8860
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8617
+ res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8861
8618
  }
8862
8619
  if (spacer !== "") {
8863
8620
  res += `
@@ -8878,7 +8635,7 @@ ${originalIndentation}`;
8878
8635
  let separator = "";
8879
8636
  if (spacer !== "") {
8880
8637
  indentation += spacer;
8881
- join11 = `,
8638
+ join4 = `,
8882
8639
  ${indentation}`;
8883
8640
  whitespace = " ";
8884
8641
  }
@@ -8892,13 +8649,13 @@ ${indentation}`;
8892
8649
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
8893
8650
  if (tmp !== void 0) {
8894
8651
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8895
- separator = join11;
8652
+ separator = join4;
8896
8653
  }
8897
8654
  }
8898
8655
  if (keyLength > maximumBreadth) {
8899
8656
  const removedKeys = keyLength - maximumBreadth;
8900
8657
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
8901
- separator = join11;
8658
+ separator = join4;
8902
8659
  }
8903
8660
  if (spacer !== "" && separator.length > 1) {
8904
8661
  res = `
@@ -8939,7 +8696,7 @@ ${originalIndentation}`;
8939
8696
  }
8940
8697
  const originalIndentation = indentation;
8941
8698
  let res = "";
8942
- let join11 = ",";
8699
+ let join4 = ",";
8943
8700
  if (Array.isArray(value)) {
8944
8701
  if (value.length === 0) {
8945
8702
  return "[]";
@@ -8952,7 +8709,7 @@ ${originalIndentation}`;
8952
8709
  indentation += spacer;
8953
8710
  res += `
8954
8711
  ${indentation}`;
8955
- join11 = `,
8712
+ join4 = `,
8956
8713
  ${indentation}`;
8957
8714
  }
8958
8715
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -8960,13 +8717,13 @@ ${indentation}`;
8960
8717
  for (; i < maximumValuesToStringify - 1; i++) {
8961
8718
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8962
8719
  res += tmp2 !== void 0 ? tmp2 : "null";
8963
- res += join11;
8720
+ res += join4;
8964
8721
  }
8965
8722
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
8966
8723
  res += tmp !== void 0 ? tmp : "null";
8967
8724
  if (value.length - 1 > maximumBreadth) {
8968
8725
  const removedKeys = value.length - maximumBreadth - 1;
8969
- res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
8726
+ res += `${join4}"... ${getItemCount(removedKeys)} not stringified"`;
8970
8727
  }
8971
8728
  if (spacer !== "") {
8972
8729
  res += `
@@ -8979,7 +8736,7 @@ ${originalIndentation}`;
8979
8736
  let whitespace = "";
8980
8737
  if (spacer !== "") {
8981
8738
  indentation += spacer;
8982
- join11 = `,
8739
+ join4 = `,
8983
8740
  ${indentation}`;
8984
8741
  whitespace = " ";
8985
8742
  }
@@ -8988,7 +8745,7 @@ ${indentation}`;
8988
8745
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
8989
8746
  if (tmp !== void 0) {
8990
8747
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
8991
- separator = join11;
8748
+ separator = join4;
8992
8749
  }
8993
8750
  }
8994
8751
  if (spacer !== "" && separator.length > 1) {
@@ -9046,20 +8803,20 @@ ${originalIndentation}`;
9046
8803
  indentation += spacer;
9047
8804
  let res2 = `
9048
8805
  ${indentation}`;
9049
- const join12 = `,
8806
+ const join5 = `,
9050
8807
  ${indentation}`;
9051
8808
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
9052
8809
  let i = 0;
9053
8810
  for (; i < maximumValuesToStringify - 1; i++) {
9054
8811
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9055
8812
  res2 += tmp2 !== void 0 ? tmp2 : "null";
9056
- res2 += join12;
8813
+ res2 += join5;
9057
8814
  }
9058
8815
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
9059
8816
  res2 += tmp !== void 0 ? tmp : "null";
9060
8817
  if (value.length - 1 > maximumBreadth) {
9061
8818
  const removedKeys = value.length - maximumBreadth - 1;
9062
- res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
8819
+ res2 += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
9063
8820
  }
9064
8821
  res2 += `
9065
8822
  ${originalIndentation}`;
@@ -9075,16 +8832,16 @@ ${originalIndentation}`;
9075
8832
  return '"[Object]"';
9076
8833
  }
9077
8834
  indentation += spacer;
9078
- const join11 = `,
8835
+ const join4 = `,
9079
8836
  ${indentation}`;
9080
8837
  let res = "";
9081
8838
  let separator = "";
9082
8839
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
9083
8840
  if (isTypedArrayWithEntries(value)) {
9084
- res += stringifyTypedArray(value, join11, maximumBreadth);
8841
+ res += stringifyTypedArray(value, join4, maximumBreadth);
9085
8842
  keys = keys.slice(value.length);
9086
8843
  maximumPropertiesToStringify -= value.length;
9087
- separator = join11;
8844
+ separator = join4;
9088
8845
  }
9089
8846
  if (deterministic) {
9090
8847
  keys = sort(keys, comparator);
@@ -9095,13 +8852,13 @@ ${indentation}`;
9095
8852
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
9096
8853
  if (tmp !== void 0) {
9097
8854
  res += `${separator}${strEscape(key2)}: ${tmp}`;
9098
- separator = join11;
8855
+ separator = join4;
9099
8856
  }
9100
8857
  }
9101
8858
  if (keyLength > maximumBreadth) {
9102
8859
  const removedKeys = keyLength - maximumBreadth;
9103
8860
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
9104
- separator = join11;
8861
+ separator = join4;
9105
8862
  }
9106
8863
  if (separator !== "") {
9107
8864
  res = `
@@ -10192,11 +9949,11 @@ var init_lib = __esm({
10192
9949
  }
10193
9950
  }
10194
9951
  },
10195
- addToPath: function addToPath(path38, added, removed, oldPosInc, options) {
10196
- var last = path38.lastComponent;
9952
+ addToPath: function addToPath(path32, added, removed, oldPosInc, options) {
9953
+ var last = path32.lastComponent;
10197
9954
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
10198
9955
  return {
10199
- oldPos: path38.oldPos + oldPosInc,
9956
+ oldPos: path32.oldPos + oldPosInc,
10200
9957
  lastComponent: {
10201
9958
  count: last.count + 1,
10202
9959
  added,
@@ -10206,7 +9963,7 @@ var init_lib = __esm({
10206
9963
  };
10207
9964
  } else {
10208
9965
  return {
10209
- oldPos: path38.oldPos + oldPosInc,
9966
+ oldPos: path32.oldPos + oldPosInc,
10210
9967
  lastComponent: {
10211
9968
  count: 1,
10212
9969
  added,
@@ -10264,7 +10021,7 @@ var init_lib = __esm({
10264
10021
  tokenize: function tokenize(value) {
10265
10022
  return Array.from(value);
10266
10023
  },
10267
- join: function join4(chars) {
10024
+ join: function join3(chars) {
10268
10025
  return chars.join("");
10269
10026
  },
10270
10027
  postProcess: function postProcess(changeObjects) {
@@ -10637,10 +10394,10 @@ function attachmentToHistoryMessage(o, ts) {
10637
10394
  const memories = raw.map((m2) => {
10638
10395
  if (!m2 || typeof m2 !== "object") return null;
10639
10396
  const rec = m2;
10640
- const path38 = typeof rec.path === "string" ? rec.path : null;
10397
+ const path32 = typeof rec.path === "string" ? rec.path : null;
10641
10398
  const content = typeof rec.content === "string" ? rec.content : null;
10642
- if (!path38 || content == null) return null;
10643
- const entry = { path: path38, content };
10399
+ if (!path32 || content == null) return null;
10400
+ const entry = { path: path32, content };
10644
10401
  if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
10645
10402
  return entry;
10646
10403
  }).filter((m2) => m2 !== null);
@@ -11100,27 +10857,6 @@ var init_claude_history = __esm({
11100
10857
  }
11101
10858
  });
11102
10859
 
11103
- // src/tools/sandbox.ts
11104
- function shouldSandbox(cwd, personaRoot) {
11105
- if (!personaRoot) return false;
11106
- const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
11107
- return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
11108
- }
11109
- function inferSandboxSettingsPath(cwd, personaRoot) {
11110
- if (!shouldSandbox(cwd, personaRoot)) return null;
11111
- const rel = path12.relative(personaRoot, cwd);
11112
- const personaId = rel.split(path12.sep)[0];
11113
- if (!personaId) return null;
11114
- return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
11115
- }
11116
- var path12;
11117
- var init_sandbox = __esm({
11118
- "src/tools/sandbox.ts"() {
11119
- "use strict";
11120
- path12 = __toESM(require("path"), 1);
11121
- }
11122
- });
11123
-
11124
10860
  // src/tools/claude.ts
11125
10861
  function macOSDesktopCandidates(home) {
11126
10862
  return [
@@ -11185,8 +10921,7 @@ function buildSpawnArgs(ctx) {
11185
10921
  throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
11186
10922
  }
11187
10923
  }
11188
- const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
11189
- if (sandboxSettings) args.push("--settings", sandboxSettings);
10924
+ if (ctx.extraSettings) args.push("--settings", ctx.extraSettings);
11190
10925
  if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
11191
10926
  if (ctx.effort) args.push("--effort", ctx.effort);
11192
10927
  if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
@@ -11466,10 +11201,10 @@ function parseAttachment(obj) {
11466
11201
  const memories = raw.map((m2) => {
11467
11202
  if (!m2 || typeof m2 !== "object") return null;
11468
11203
  const rec = m2;
11469
- const path38 = typeof rec.path === "string" ? rec.path : null;
11204
+ const path32 = typeof rec.path === "string" ? rec.path : null;
11470
11205
  const content = typeof rec.content === "string" ? rec.content : null;
11471
- if (!path38 || content == null) return null;
11472
- const out = { path: path38, content };
11206
+ if (!path32 || content == null) return null;
11207
+ const out = { path: path32, content };
11473
11208
  if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
11474
11209
  return out;
11475
11210
  }).filter((m2) => m2 !== null);
@@ -11585,7 +11320,6 @@ var init_claude = __esm({
11585
11320
  init_protocol();
11586
11321
  init_claude_history();
11587
11322
  init_tool_result_extra();
11588
- init_sandbox();
11589
11323
  ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
11590
11324
  "hook_additional_context",
11591
11325
  "hook_success",
@@ -18968,7 +18702,7 @@ var require_websocket = __commonJS({
18968
18702
  var http2 = require("http");
18969
18703
  var net = require("net");
18970
18704
  var tls = require("tls");
18971
- var { randomBytes: randomBytes3, createHash: createHash3 } = require("crypto");
18705
+ var { randomBytes, createHash } = require("crypto");
18972
18706
  var { Duplex, Readable: Readable3 } = require("stream");
18973
18707
  var { URL: URL2 } = require("url");
18974
18708
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -19498,7 +19232,7 @@ var require_websocket = __commonJS({
19498
19232
  }
19499
19233
  }
19500
19234
  const defaultPort = isSecure ? 443 : 80;
19501
- const key = randomBytes3(16).toString("base64");
19235
+ const key = randomBytes(16).toString("base64");
19502
19236
  const request = isSecure ? https.request : http2.request;
19503
19237
  const protocolSet = /* @__PURE__ */ new Set();
19504
19238
  let perMessageDeflate;
@@ -19628,7 +19362,7 @@ var require_websocket = __commonJS({
19628
19362
  abortHandshake(websocket, socket, "Invalid Upgrade header");
19629
19363
  return;
19630
19364
  }
19631
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
19365
+ const digest = createHash("sha1").update(key + GUID).digest("base64");
19632
19366
  if (res.headers["sec-websocket-accept"] !== digest) {
19633
19367
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
19634
19368
  return;
@@ -19995,7 +19729,7 @@ var require_websocket_server = __commonJS({
19995
19729
  var EventEmitter2 = require("events");
19996
19730
  var http2 = require("http");
19997
19731
  var { Duplex } = require("stream");
19998
- var { createHash: createHash3 } = require("crypto");
19732
+ var { createHash } = require("crypto");
19999
19733
  var extension2 = require_extension();
20000
19734
  var PerMessageDeflate2 = require_permessage_deflate();
20001
19735
  var subprotocol2 = require_subprotocol();
@@ -20296,7 +20030,7 @@ var require_websocket_server = __commonJS({
20296
20030
  );
20297
20031
  }
20298
20032
  if (this._state > RUNNING) return abortHandshake(socket, 503);
20299
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
20033
+ const digest = createHash("sha1").update(key + GUID).digest("base64");
20300
20034
  const headers = [
20301
20035
  "HTTP/1.1 101 Switching Protocols",
20302
20036
  "Upgrade: websocket",
@@ -20384,7 +20118,7 @@ var require_websocket_server = __commonJS({
20384
20118
  // src/run-case/recorder.ts
20385
20119
  function startRunCaseRecorder(opts) {
20386
20120
  const now = opts.now ?? Date.now;
20387
- const dir = import_node_path27.default.dirname(opts.recordPath);
20121
+ const dir = import_node_path28.default.dirname(opts.recordPath);
20388
20122
  let stream = null;
20389
20123
  let closing = false;
20390
20124
  let closedSettled = false;
@@ -20424,12 +20158,12 @@ function startRunCaseRecorder(opts) {
20424
20158
  };
20425
20159
  return { tap, close, closed };
20426
20160
  }
20427
- var import_node_fs25, import_node_path27;
20161
+ var import_node_fs25, import_node_path28;
20428
20162
  var init_recorder = __esm({
20429
20163
  "src/run-case/recorder.ts"() {
20430
20164
  "use strict";
20431
20165
  import_node_fs25 = __toESM(require("fs"), 1);
20432
- import_node_path27 = __toESM(require("path"), 1);
20166
+ import_node_path28 = __toESM(require("path"), 1);
20433
20167
  }
20434
20168
  });
20435
20169
 
@@ -20472,7 +20206,7 @@ var init_wire = __esm({
20472
20206
  // src/run-case/controller.ts
20473
20207
  async function runController(opts) {
20474
20208
  const now = opts.now ?? Date.now;
20475
- const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20209
+ const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path29.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
20476
20210
  const ownsCwd = opts.cwd === void 0;
20477
20211
  const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
20478
20212
  const spawnCtx = { cwd };
@@ -20639,13 +20373,13 @@ async function runController(opts) {
20639
20373
  }
20640
20374
  return exitCode ?? 0;
20641
20375
  }
20642
- var import_node_fs26, import_node_os14, import_node_path28;
20376
+ var import_node_fs26, import_node_os14, import_node_path29;
20643
20377
  var init_controller = __esm({
20644
20378
  "src/run-case/controller.ts"() {
20645
20379
  "use strict";
20646
20380
  import_node_fs26 = require("fs");
20647
20381
  import_node_os14 = __toESM(require("os"), 1);
20648
- import_node_path28 = __toESM(require("path"), 1);
20382
+ import_node_path29 = __toESM(require("path"), 1);
20649
20383
  init_claude();
20650
20384
  init_stdout_splitter();
20651
20385
  init_permission_stdio();
@@ -20877,7 +20611,7 @@ Env (advanced):
20877
20611
  `;
20878
20612
 
20879
20613
  // src/index.ts
20880
- var import_node_path26 = __toESM(require("path"), 1);
20614
+ var import_node_path27 = __toESM(require("path"), 1);
20881
20615
  var import_node_fs24 = __toESM(require("fs"), 1);
20882
20616
 
20883
20617
  // src/logger.ts
@@ -20915,9 +20649,6 @@ function createLogger(opts = {}) {
20915
20649
  return wrap(base);
20916
20650
  }
20917
20651
 
20918
- // src/session/store-factory.ts
20919
- var path5 = __toESM(require("path"), 1);
20920
-
20921
20652
  // src/session/store.ts
20922
20653
  var import_node_fs3 = __toESM(require("fs"), 1);
20923
20654
  var import_node_path4 = __toESM(require("path"), 1);
@@ -20955,16 +20686,12 @@ function safeFileName(sessionId) {
20955
20686
  var SessionStore = class {
20956
20687
  root;
20957
20688
  constructor(opts) {
20958
- if ("root" in opts) {
20959
- this.root = opts.root;
20960
- } else {
20961
- const scope = opts.scope ?? { kind: "default" };
20962
- this.root = import_node_path4.default.join(
20963
- opts.dataDir,
20964
- "sessions",
20965
- ...scopeSubPath(scope).map(safeFileName)
20966
- );
20967
- }
20689
+ const scope = opts.scope ?? { kind: "default" };
20690
+ this.root = import_node_path4.default.join(
20691
+ opts.dataDir,
20692
+ "sessions",
20693
+ ...scopeSubPath(scope).map(safeFileName)
20694
+ );
20968
20695
  }
20969
20696
  filePath(sessionId) {
20970
20697
  return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
@@ -21029,66 +20756,6 @@ var SessionStore = class {
21029
20756
  }
21030
20757
  };
21031
20758
 
21032
- // src/session/store-factory.ts
21033
- var SessionStoreFactory = class {
21034
- dataDir;
21035
- bareStore = null;
21036
- vmOwnerStores = /* @__PURE__ */ new Map();
21037
- // vmGuest 索引 key = `${pid}::${personId}`(per-People token: 一 person 一目录)
21038
- vmGuestStores = /* @__PURE__ */ new Map();
21039
- constructor(opts) {
21040
- this.dataDir = opts.dataDir;
21041
- }
21042
- // ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
21043
- bareRoot() {
21044
- return path5.join(this.dataDir, "sessions");
21045
- }
21046
- vmOwnerRoot(personaId) {
21047
- return path5.join(
21048
- this.dataDir,
21049
- "personas",
21050
- safeFileName(personaId),
21051
- ".clawd",
21052
- "sessions",
21053
- "owner"
21054
- );
21055
- }
21056
- vmGuestRoot(personaId, personId) {
21057
- return path5.join(
21058
- this.dataDir,
21059
- "personas",
21060
- safeFileName(personaId),
21061
- ".clawd",
21062
- "sessions",
21063
- "guests",
21064
- safeFileName(personId)
21065
- );
21066
- }
21067
- // ---- SessionStore 工厂(缓存) ----
21068
- forBare() {
21069
- if (!this.bareStore) {
21070
- this.bareStore = new SessionStore({ root: this.bareRoot() });
21071
- }
21072
- return this.bareStore;
21073
- }
21074
- forVmOwner(personaId) {
21075
- const key = personaId;
21076
- const cached = this.vmOwnerStores.get(key);
21077
- if (cached) return cached;
21078
- const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
21079
- this.vmOwnerStores.set(key, st);
21080
- return st;
21081
- }
21082
- forVmGuest(personaId, personId) {
21083
- const key = `${personaId}::${personId}`;
21084
- const cached = this.vmGuestStores.get(key);
21085
- if (cached) return cached;
21086
- const st = new SessionStore({ root: this.vmGuestRoot(personaId, personId) });
21087
- this.vmGuestStores.set(key, st);
21088
- return st;
21089
- }
21090
- };
21091
-
21092
20759
  // src/session/manager.ts
21093
20760
  var import_node_fs5 = __toESM(require("fs"), 1);
21094
20761
  var import_node_path6 = __toESM(require("path"), 1);
@@ -21248,10 +20915,7 @@ function buildSpawnContext(state, deps) {
21248
20915
  toolSessionId: file.toolSessionId,
21249
20916
  model: file.model,
21250
20917
  permissionMode: file.permissionMode,
21251
- effort: file.effort,
21252
- // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
21253
- // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
21254
- personaRoot: deps.personaRoot
20918
+ effort: file.effort
21255
20919
  };
21256
20920
  const meta = state.subSessionMeta;
21257
20921
  if (meta?.personaMode) {
@@ -21978,8 +21642,7 @@ var SessionRunner = class {
21978
21642
  now: this.hooks.now ?? Date.now,
21979
21643
  resolveContextWindow: this.hooks.resolveContextWindow,
21980
21644
  genUuid: this.hooks.genUuid ?? v4_default,
21981
- ownerDisplayName: this.hooks.ownerDisplayName,
21982
- personaRoot: this.hooks.personaRoot
21645
+ ownerDisplayName: this.hooks.ownerDisplayName
21983
21646
  };
21984
21647
  const { state, effects } = reduceSession(this.state, inputMsg, deps);
21985
21648
  this.state = state;
@@ -22403,20 +22066,9 @@ var SessionManager = class {
22403
22066
  attachObserver(observer) {
22404
22067
  this.attachedObserver = observer;
22405
22068
  }
22406
- // 按 scope 拿对应的 SessionStore.
22407
- // Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
22408
- // default → factory.forBare() <dataDir>/sessions/
22409
- // persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
22410
- // persona/<pid>/listener → throw (listener 角色 #698 已下线, schema 字段保留但运行时不该出现)
22411
- // 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
22069
+ // 按 scope 拿对应的 SessionStore。default scope 复用 deps.store(保证与
22070
+ // bootstrap 注入的 store 一致);persona scope 第一次访问时按需创建并缓存。
22412
22071
  storeFor(scope) {
22413
- if (this.deps.storeFactory) {
22414
- if (scope.kind === "default") return this.deps.storeFactory.forBare();
22415
- if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
22416
- throw new Error(
22417
- `SessionManager: listener scope is deprecated (#698); use forVmGuest in Phase 3+ instead`
22418
- );
22419
- }
22420
22072
  if (scope.kind === "default") return this.deps.store;
22421
22073
  const key = scopeKey(scope);
22422
22074
  const cached = this.storesByScope.get(key);
@@ -22444,13 +22096,11 @@ var SessionManager = class {
22444
22096
  scopeForFile(file) {
22445
22097
  return file.ownerPersonaId ? { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" } : { kind: "default" };
22446
22098
  }
22447
- // 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned scope 查询.
22448
- // Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
22449
- // 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
22450
- // <dataDir>/sessions/ 列子目录 (排除 'default').
22099
+ // <dataDir>/sessions/ 列出所有 persona 命名空间(不含 'default')。
22100
+ // 用于 findOwnedSession / listAllOwned scope 查询。
22451
22101
  listPersonaIdsOnDisk() {
22452
22102
  if (!this.deps.dataDir) return [];
22453
- const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
22103
+ const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
22454
22104
  let entries;
22455
22105
  try {
22456
22106
  entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
@@ -22462,9 +22112,19 @@ var SessionManager = class {
22462
22112
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
22463
22113
  }
22464
22114
  // owner / default 两个分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
22465
- // SessionManager 在这里扫盘定位(default 直接试,未命中再轮询所有 persona owner 目录)。
22115
+ // SessionManager 在这里定位。三层快慢路径:
22116
+ // 1) active runner(用户当前在用的 session):runner.state.file 是 SessionFile 内存权威副本,
22117
+ // 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
22118
+ // 绝大多数场景
22119
+ // 2) default scope 磁盘:inactive default session 命中
22120
+ // 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
22466
22121
  // listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
22122
+ //
22123
+ // 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
22124
+ // (持 default-only store 是 file-sharing v1 的预存 bug 根因——见 fix #703)
22467
22125
  findOwnedSession(sessionId) {
22126
+ const runner = this.runners.get(sessionId);
22127
+ if (runner) return runner.getState().file;
22468
22128
  const dflt = this.deps.store.read(sessionId);
22469
22129
  if (dflt) return dflt;
22470
22130
  for (const personaId of this.listPersonaIdsOnDisk()) {
@@ -22474,6 +22134,13 @@ var SessionManager = class {
22474
22134
  }
22475
22135
  return null;
22476
22136
  }
22137
+ // findOwnedSession + scopeForFile 收口。把"sessionId → SessionScope"的派生
22138
+ // 集中到 manager(scope 单一来源),调用方拿 scope 不再自己拼 object 也不依赖
22139
+ // ownerPersonaId 字段语义。给 attachment handler 通过 deps.getSessionScope 闭包消费。
22140
+ findOwnedSessionScope(sessionId) {
22141
+ const file = this.findOwnedSession(sessionId);
22142
+ return file ? this.scopeForFile(file) : null;
22143
+ }
22477
22144
  // 合并 default + 所有 persona owner 的 SessionFile —— 桌面 App 主 session 列表入口。
22478
22145
  // 同样不含 listener sub-session(listener 走 persona:listSubSessions RPC 单独入口)。
22479
22146
  listAllOwned() {
@@ -22525,9 +22192,6 @@ var SessionManager = class {
22525
22192
  dataDir: this.deps.dataDir,
22526
22193
  personaStore: this.deps.personaStore,
22527
22194
  ownerDisplayName: this.deps.ownerDisplayName,
22528
- // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
22529
- // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
22530
- personaRoot: this.deps.personaRoot,
22531
22195
  // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
22532
22196
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
22533
22197
  scope,
@@ -22622,7 +22286,7 @@ var SessionManager = class {
22622
22286
  }
22623
22287
  }
22624
22288
  // ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
22625
- create(args, creatorPrincipalId) {
22289
+ create(args) {
22626
22290
  let cwd;
22627
22291
  if (args.ownerPersonaId) {
22628
22292
  cwd = derivePersonaSpawnCwd(
@@ -22663,7 +22327,6 @@ var SessionManager = class {
22663
22327
  iconKey: args.iconKey,
22664
22328
  forkedFromSessionId: args.forkedFromSessionId,
22665
22329
  ownerPersonaId: args.ownerPersonaId,
22666
- creatorPrincipalId,
22667
22330
  createdAt: iso,
22668
22331
  updatedAt: iso
22669
22332
  };
@@ -23541,7 +23204,7 @@ var SessionManager = class {
23541
23204
 
23542
23205
  // src/persona/store.ts
23543
23206
  var fs6 = __toESM(require("fs"), 1);
23544
- var path8 = __toESM(require("path"), 1);
23207
+ var path7 = __toESM(require("path"), 1);
23545
23208
  init_protocol();
23546
23209
  var DEFAULT_SETTINGS = {
23547
23210
  permissions: {
@@ -23570,13 +23233,13 @@ var PersonaStore = class {
23570
23233
  }
23571
23234
  root;
23572
23235
  personaDir(personaId) {
23573
- return path8.join(this.root, safeFileName(personaId));
23236
+ return path7.join(this.root, safeFileName(personaId));
23574
23237
  }
23575
23238
  metaPath(personaId) {
23576
- return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
23239
+ return path7.join(this.personaDir(personaId), ".clawd", "persona.json");
23577
23240
  }
23578
23241
  claudeMdPath(personaId) {
23579
- return path8.join(this.personaDir(personaId), "CLAUDE.md");
23242
+ return path7.join(this.personaDir(personaId), "CLAUDE.md");
23580
23243
  }
23581
23244
  /**
23582
23245
  * Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
@@ -23586,11 +23249,11 @@ var PersonaStore = class {
23586
23249
  * 加载 persona 人格,只有 listener 多一层 OS sandbox。
23587
23250
  */
23588
23251
  sandboxSettingsPath(personaId) {
23589
- return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23252
+ return path7.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
23590
23253
  }
23591
23254
  write(persona, personality) {
23592
23255
  const dir = this.personaDir(persona.personaId);
23593
- fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
23256
+ fs6.mkdirSync(path7.join(dir, ".clawd"), { recursive: true });
23594
23257
  this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
23595
23258
  this.atomicWrite(
23596
23259
  this.sandboxSettingsPath(persona.personaId),
@@ -23608,10 +23271,6 @@ var PersonaStore = class {
23608
23271
  const p2 = this.metaPath(personaId);
23609
23272
  if (!fs6.existsSync(p2)) return null;
23610
23273
  const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23611
- if (raw && typeof raw === "object" && "tokenMap" in raw) {
23612
- delete raw.tokenMap;
23613
- this.atomicWrite(p2, JSON.stringify(raw, null, 2));
23614
- }
23615
23274
  return PersonaFileSchema.parse(raw);
23616
23275
  }
23617
23276
  has(personaId) {
@@ -23637,12 +23296,48 @@ var PersonaStore = class {
23637
23296
  }
23638
23297
  /** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
23639
23298
  skillsDir(personaId) {
23640
- return path8.join(this.personaDir(personaId), ".claude", "skills");
23299
+ return path7.join(this.personaDir(personaId), ".claude", "skills");
23300
+ }
23301
+ /**
23302
+ * Claude Code 项目级 settings 路径:`<personaDir>/.claude/settings.json`。
23303
+ * 这里只读 `enabledPlugins` 字段,由 owner 通过 CC `/plugin` 之类命令维护,daemon 不写。
23304
+ */
23305
+ claudeSettingsPath(personaId) {
23306
+ return path7.join(this.personaDir(personaId), ".claude", "settings.json");
23307
+ }
23308
+ /**
23309
+ * 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
23310
+ * 的 key 转成 `{ id }[]` 给 UI 做只读展示。
23311
+ *
23312
+ * 兜底范围(来自老板规则:不要无事实依据的兜底):
23313
+ * - 文件不存在 → [](plugin 未被启用是合法初始态)
23314
+ * - JSON 解析失败 → [](owner 手改文件,schema 漂移交给 UI 兜底)
23315
+ * - `enabledPlugins` 字段缺失 / 非 object → []
23316
+ * - 单 value 非 boolean → 跳过该 entry
23317
+ * 其他错误(fs 权限等)原样抛出,由上层 handler 转 ws error 帧。
23318
+ */
23319
+ readEnabledPlugins(personaId) {
23320
+ const p2 = this.claudeSettingsPath(personaId);
23321
+ if (!fs6.existsSync(p2)) return [];
23322
+ let raw;
23323
+ try {
23324
+ raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
23325
+ } catch {
23326
+ return [];
23327
+ }
23328
+ if (!raw || typeof raw !== "object") return [];
23329
+ const enabled = raw.enabledPlugins;
23330
+ if (!enabled || typeof enabled !== "object" || Array.isArray(enabled)) return [];
23331
+ const out = [];
23332
+ for (const [id, value] of Object.entries(enabled)) {
23333
+ if (value === true && id.length > 0) out.push({ id });
23334
+ }
23335
+ return out;
23641
23336
  }
23642
23337
  list() {
23643
23338
  if (!fs6.existsSync(this.root)) return [];
23644
23339
  return fs6.readdirSync(this.root).filter((name) => {
23645
- return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
23340
+ return fs6.existsSync(path7.join(this.root, name, ".clawd", "persona.json"));
23646
23341
  });
23647
23342
  }
23648
23343
  remove(personaId) {
@@ -23667,19 +23362,12 @@ var PersonaRegistry = class {
23667
23362
  }
23668
23363
  store;
23669
23364
  cache = /* @__PURE__ */ new Map();
23670
- /**
23671
- * 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
23672
- * 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
23673
- * try/catch 隔离,让其它 persona 仍能加载。
23674
- */
23365
+ /** 从 store 全量重建缓存(boot 时 + 测试 setup 时) */
23675
23366
  reload() {
23676
23367
  this.cache.clear();
23677
23368
  for (const id of this.store.list()) {
23678
- try {
23679
- const p2 = this.store.read(id);
23680
- if (p2) this.cache.set(p2.personaId, p2);
23681
- } catch {
23682
- }
23369
+ const p2 = this.store.read(id);
23370
+ if (p2) this.cache.set(p2.personaId, p2);
23683
23371
  }
23684
23372
  }
23685
23373
  get(personaId) {
@@ -23695,6 +23383,21 @@ var PersonaRegistry = class {
23695
23383
  list() {
23696
23384
  return Array.from(this.cache.values());
23697
23385
  }
23386
+ /**
23387
+ * file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
23388
+ * 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
23389
+ * persona 数量通常 < 100,性能可忽略。
23390
+ */
23391
+ findByToken(token) {
23392
+ if (!token) return null;
23393
+ for (const persona of this.cache.values()) {
23394
+ if (!persona.public) continue;
23395
+ const entry = persona.tokenMap?.[token];
23396
+ if (!entry || entry.revoked) continue;
23397
+ return { personaId: persona.personaId, label: entry.label };
23398
+ }
23399
+ return null;
23400
+ }
23698
23401
  };
23699
23402
 
23700
23403
  // src/persona/manager.ts
@@ -23988,6 +23691,9 @@ var PersonaManager = class {
23988
23691
  model: args.model,
23989
23692
  public: args.public ?? false,
23990
23693
  iconKey: args.iconKey,
23694
+ // 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
23695
+ // file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
23696
+ tokenMap: {},
23991
23697
  createdAt: now,
23992
23698
  updatedAt: now
23993
23699
  };
@@ -24024,28 +23730,65 @@ var PersonaManager = class {
24024
23730
  listSkills(personaId) {
24025
23731
  return listSkillsForDir(this.deps.store.skillsDir(personaId), "project");
24026
23732
  }
23733
+ /**
23734
+ * 读取 persona `.claude/settings.json` 中已启用的插件列表(value === true 的 key)。
23735
+ * 文件不存在 / 解析失败 / 字段缺失 → 空数组。供 persona:get handler 拼响应。
23736
+ */
23737
+ listEnabledPlugins(personaId) {
23738
+ return this.deps.store.readEnabledPlugins(personaId);
23739
+ }
24027
23740
  /** 读 sandbox-settings.json;不存在 / 损坏返回 null。供 persona:get handler 拼响应。 */
24028
23741
  readSandboxSettings(personaId) {
24029
23742
  return this.deps.store.readSandboxSettings(personaId);
24030
23743
  }
24031
23744
  /**
24032
23745
  * 删除 persona。
24033
- * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
24034
- * Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
24035
- * .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
24036
- *
24037
- * ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
24038
- * ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
24039
- * 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
24040
- * 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
24041
- * 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
24042
- * force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
24043
- * "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
23746
+ * PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录。
24044
23747
  */
24045
23748
  delete(personaId) {
24046
23749
  this.deps.store.remove(personaId);
24047
23750
  this.deps.registry.remove(personaId);
24048
23751
  }
23752
+ /**
23753
+ * 生成 32-char base64url token 并写入 tokenMap,给 file-sharing HTTP Bearer
23754
+ * 鉴权用(PersonaRegistry.findByToken 反查 personaId/label)。
23755
+ */
23756
+ issueToken(personaId, label) {
23757
+ const existing = this.deps.registry.get(personaId);
23758
+ if (!existing) throw new Error(`persona not found: ${personaId}`);
23759
+ const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
23760
+ const entry = {
23761
+ label,
23762
+ issuedAt: Date.now(),
23763
+ revoked: false
23764
+ };
23765
+ const updated = {
23766
+ ...existing,
23767
+ tokenMap: { ...existing.tokenMap ?? {}, [token]: entry },
23768
+ updatedAt: Date.now()
23769
+ };
23770
+ this.deps.store.writeMeta(updated);
23771
+ this.deps.registry.set(updated);
23772
+ return { token, persona: updated };
23773
+ }
23774
+ /** 标记 token 为 revoked(保留条目用于审计);不存在的 token 抛错。 */
23775
+ revokeToken(personaId, token) {
23776
+ const existing = this.deps.registry.get(personaId);
23777
+ if (!existing) throw new Error(`persona not found: ${personaId}`);
23778
+ const tokenEntry = existing.tokenMap?.[token];
23779
+ if (!tokenEntry) throw new Error(`token not found in persona ${personaId}`);
23780
+ const updated = {
23781
+ ...existing,
23782
+ tokenMap: {
23783
+ ...existing.tokenMap ?? {},
23784
+ [token]: { ...tokenEntry, revoked: true }
23785
+ },
23786
+ updatedAt: Date.now()
23787
+ };
23788
+ this.deps.store.writeMeta(updated);
23789
+ this.deps.registry.set(updated);
23790
+ return updated;
23791
+ }
24049
23792
  /**
24050
23793
  * label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
24051
23794
  * base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
@@ -24065,7 +23808,7 @@ var PersonaManager = class {
24065
23808
 
24066
23809
  // src/persona/seed.ts
24067
23810
  var fs8 = __toESM(require("fs"), 1);
24068
- var path10 = __toESM(require("path"), 1);
23811
+ var path9 = __toESM(require("path"), 1);
24069
23812
  var import_node_url = require("url");
24070
23813
  var import_meta = {};
24071
23814
  var DEFAULT_PERSONAS = [
@@ -24101,14 +23844,14 @@ var DEFAULT_PERSONAS = [
24101
23844
  function findDefaultsRoot() {
24102
23845
  const candidates = [];
24103
23846
  try {
24104
- const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
24105
- candidates.push(path10.resolve(here, "defaults"));
24106
- candidates.push(path10.resolve(here, "persona-defaults"));
23847
+ const here = path9.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
23848
+ candidates.push(path9.resolve(here, "defaults"));
23849
+ candidates.push(path9.resolve(here, "persona-defaults"));
24107
23850
  } catch {
24108
23851
  }
24109
23852
  if (process.argv[1]) {
24110
- const argvDir = path10.dirname(process.argv[1]);
24111
- candidates.push(path10.resolve(argvDir, "persona-defaults"));
23853
+ const argvDir = path9.dirname(process.argv[1]);
23854
+ candidates.push(path9.resolve(argvDir, "persona-defaults"));
24112
23855
  }
24113
23856
  for (const c of candidates) {
24114
23857
  try {
@@ -24125,7 +23868,7 @@ function seedDefaultPersonas(args) {
24125
23868
  args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
24126
23869
  continue;
24127
23870
  }
24128
- const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
23871
+ const bundleDir = path9.join(args.defaultsRoot, entry.personaId);
24129
23872
  if (!fs8.existsSync(bundleDir)) {
24130
23873
  args.logger.warn("persona.seed.skip", {
24131
23874
  personaId: entry.personaId,
@@ -24134,7 +23877,7 @@ function seedDefaultPersonas(args) {
24134
23877
  });
24135
23878
  continue;
24136
23879
  }
24137
- const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
23880
+ const claudeMdPath = path9.join(bundleDir, "CLAUDE.md");
24138
23881
  if (!fs8.existsSync(claudeMdPath)) {
24139
23882
  args.logger.warn("persona.seed.skip", {
24140
23883
  personaId: entry.personaId,
@@ -24151,6 +23894,7 @@ function seedDefaultPersonas(args) {
24151
23894
  model: entry.model,
24152
23895
  public: entry.public,
24153
23896
  iconKey: entry.iconKey,
23897
+ tokenMap: {},
24154
23898
  createdAt: now,
24155
23899
  updatedAt: now
24156
23900
  };
@@ -24162,8 +23906,8 @@ function seedDefaultPersonas(args) {
24162
23906
  function copyBundleExtras(srcDir, dstDir) {
24163
23907
  for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
24164
23908
  if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
24165
- const srcPath = path10.join(srcDir, entry.name);
24166
- const dstPath = path10.join(dstDir, entry.name);
23909
+ const srcPath = path9.join(srcDir, entry.name);
23910
+ const dstPath = path9.join(dstDir, entry.name);
24167
23911
  if (entry.isDirectory()) {
24168
23912
  fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
24169
23913
  } else if (entry.isFile()) {
@@ -25545,9 +25289,6 @@ var LocalWsServer = class {
25545
25289
  httpServer = null;
25546
25290
  frameHandler = null;
25547
25291
  clients = /* @__PURE__ */ new Map();
25548
- // Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
25549
- // 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
25550
- capabilityIdToClients = /* @__PURE__ */ new Map();
25551
25292
  logger;
25552
25293
  pingIntervalMs;
25553
25294
  async start() {
@@ -25638,49 +25379,6 @@ var LocalWsServer = class {
25638
25379
  this.safeSend(c.ws, frame);
25639
25380
  }
25640
25381
  }
25641
- // Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
25642
- // 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
25643
- // guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
25644
- broadcastToOwners(frame) {
25645
- const gate = this.opts.authGate;
25646
- for (const c of this.clients.values()) {
25647
- if (gate && !gate.isAuthed(c.handle.id)) continue;
25648
- if (c.ctx && c.ctx.principal.kind !== "owner") continue;
25649
- this.safeSend(c.ws, frame);
25650
- }
25651
- }
25652
- // capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
25653
- // - 所有 owner ws (owner 看自家所有 channel)
25654
- // - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
25655
- // 一次调用同时推两端, inbox:event push 帧由此走.
25656
- broadcastToCapabilityChannel(capabilityId, frame) {
25657
- this.broadcastToOwners(frame);
25658
- const set = this.capabilityIdToClients.get(capabilityId);
25659
- if (!set) return;
25660
- for (const id of set) {
25661
- const c = this.clients.get(id);
25662
- if (!c) continue;
25663
- this.safeSend(c.ws, frame);
25664
- }
25665
- }
25666
- // Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
25667
- // principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
25668
- // principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
25669
- // principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
25670
- // 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
25671
- broadcastToPrincipal(principalId, frame) {
25672
- if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
25673
- this.broadcastToOwners(frame);
25674
- return;
25675
- }
25676
- const set = this.capabilityIdToClients.get(principalId);
25677
- if (!set) return;
25678
- for (const id of set) {
25679
- const c = this.clients.get(id);
25680
- if (!c) continue;
25681
- this.safeSend(c.ws, frame);
25682
- }
25683
- }
25684
25382
  firstSubscriber(sessionId) {
25685
25383
  for (const c of this.clients.values()) {
25686
25384
  if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
@@ -25702,40 +25400,6 @@ var LocalWsServer = class {
25702
25400
  if (!c) return;
25703
25401
  this.safeSend(c.ws, frame);
25704
25402
  }
25705
- // Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
25706
- // 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
25707
- attachClientContext(clientId, ctx) {
25708
- const c = this.clients.get(clientId);
25709
- if (!c) return;
25710
- c.ctx = ctx;
25711
- if (ctx.capabilityId) {
25712
- let set = this.capabilityIdToClients.get(ctx.capabilityId);
25713
- if (!set) {
25714
- set = /* @__PURE__ */ new Set();
25715
- this.capabilityIdToClients.set(ctx.capabilityId, set);
25716
- }
25717
- set.add(clientId);
25718
- }
25719
- }
25720
- // 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
25721
- getClientContext(clientId) {
25722
- return this.clients.get(clientId)?.ctx ?? null;
25723
- }
25724
- // Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
25725
- // 让客户端识别 'capability revoked' 而非普通断连。
25726
- closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
25727
- const set = this.capabilityIdToClients.get(capabilityId);
25728
- if (!set) return;
25729
- const ids = [...set];
25730
- for (const id of ids) {
25731
- const c = this.clients.get(id);
25732
- if (!c) continue;
25733
- try {
25734
- c.ws.close(code, reason);
25735
- } catch {
25736
- }
25737
- }
25738
- }
25739
25403
  // URL path 路由:'/' → 主连接路径;其他 → close 4404
25740
25404
  routeConnection(socket, req) {
25741
25405
  const remoteAddress = req?.socket?.remoteAddress;
@@ -25770,7 +25434,7 @@ var LocalWsServer = class {
25770
25434
  } catch {
25771
25435
  }
25772
25436
  }, this.pingIntervalMs);
25773
- this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
25437
+ this.clients.set(id, { handle, ws: socket, pingTimer });
25774
25438
  this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
25775
25439
  const authGate = this.opts.authGate;
25776
25440
  let authed = true;
@@ -25794,12 +25458,6 @@ var LocalWsServer = class {
25794
25458
  socket.on("close", () => {
25795
25459
  const c = this.clients.get(id);
25796
25460
  if (c?.pingTimer) clearInterval(c.pingTimer);
25797
- const capId = c?.ctx?.capabilityId;
25798
- if (capId) {
25799
- const set = this.capabilityIdToClients.get(capId);
25800
- set?.delete(id);
25801
- if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
25802
- }
25803
25461
  this.clients.delete(id);
25804
25462
  authGate?.unregister(id);
25805
25463
  this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
@@ -25832,11 +25490,6 @@ var LocalWsServer = class {
25832
25490
  return;
25833
25491
  }
25834
25492
  } else if (frame.type === "auth") {
25835
- const token = typeof frame.token === "string" ? frame.token ?? "" : "";
25836
- if (token && this.opts.tryVerifyCapabilityToken) {
25837
- const guestCtx = this.opts.tryVerifyCapabilityToken(token);
25838
- if (guestCtx) this.attachClientContext(client.id, guestCtx);
25839
- }
25840
25493
  this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
25841
25494
  return;
25842
25495
  }
@@ -25953,27 +25606,15 @@ var AuthGate = class {
25953
25606
  this.markFailed(handle.id);
25954
25607
  return frame.type === "auth" ? "consumed" : "reject";
25955
25608
  }
25956
- let ctx = null;
25957
- if (this.opts.authenticate) {
25958
- const r = this.opts.authenticate(parsed.data.token, parsed.data.selfPrincipalId);
25959
- if (!r.ok) {
25960
- this.opts.closeConnection(handle, 4401, r.code);
25961
- this.markFailed(handle.id);
25962
- return "consumed";
25963
- }
25964
- ctx = r.context;
25965
- } else {
25966
- if (!this.opts.expectedToken) {
25967
- this.opts.closeConnection(handle, 1008, "auth not configured");
25968
- this.markFailed(handle.id);
25969
- return "consumed";
25970
- }
25971
- if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25972
- this.opts.closeConnection(handle, 1008, "auth failed");
25973
- this.markFailed(handle.id);
25974
- return "consumed";
25975
- }
25976
- ctx = this.opts.buildOwnerContext?.() ?? null;
25609
+ if (!this.opts.expectedToken) {
25610
+ this.opts.closeConnection(handle, 1008, "auth not configured");
25611
+ this.markFailed(handle.id);
25612
+ return "consumed";
25613
+ }
25614
+ if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
25615
+ this.opts.closeConnection(handle, 1008, "auth failed");
25616
+ this.markFailed(handle.id);
25617
+ return "consumed";
25977
25618
  }
25978
25619
  if (st.timer != null) {
25979
25620
  const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -25981,7 +25622,6 @@ var AuthGate = class {
25981
25622
  }
25982
25623
  st.authed = true;
25983
25624
  st.timer = null;
25984
- if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
25985
25625
  this.opts.sendOk(handle, { type: "auth:ok" });
25986
25626
  return "consumed";
25987
25627
  }
@@ -26017,7 +25657,7 @@ var AuthContextResolver = class {
26017
25657
  }
26018
25658
  opts;
26019
25659
  /**
26020
- * 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
25660
+ * 从 `Authorization` 头解析。null = 无凭证 / 不识别 / revoked,调用方一律 401。
26021
25661
  * remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
26022
25662
  */
26023
25663
  resolveFromHeader(authHeader, remoteAddress) {
@@ -26027,6 +25667,15 @@ var AuthContextResolver = class {
26027
25667
  if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
26028
25668
  return { role: "owner", isLoopback };
26029
25669
  }
25670
+ const personalHit = this.opts.personaRegistry.findByToken(token);
25671
+ if (personalHit) {
25672
+ return {
25673
+ role: "personal",
25674
+ personaId: personalHit.personaId,
25675
+ label: personalHit.label,
25676
+ isLoopback
25677
+ };
25678
+ }
26030
25679
  return null;
26031
25680
  }
26032
25681
  };
@@ -26048,759 +25697,206 @@ function constantTimeEqual2(a, b2) {
26048
25697
  return diff2 === 0;
26049
25698
  }
26050
25699
 
26051
- // ../protocol/src/index.ts
26052
- init_runtime();
26053
-
26054
- // src/transport/connection-context.ts
26055
- function ownerContext(ownerPrincipalId, displayName) {
26056
- return {
26057
- principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
26058
- grants: [{ resource: { type: "*" }, actions: ["admin"] }]
26059
- };
26060
- }
26061
- function guestContext(cap) {
26062
- return {
26063
- principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
26064
- grants: cap.grants,
26065
- capabilityId: cap.id
26066
- };
26067
- }
26068
- function authenticate(token, deps) {
26069
- if (!token) return { ok: false, code: "NO_TOKEN" };
26070
- if (deps.isOwnerToken(token)) {
26071
- return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
26072
- }
26073
- if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
26074
- const v2 = deps.capabilityRegistry.verifyToken(token);
26075
- if (v2.ok) return { ok: true, context: guestContext(v2.capability) };
26076
- if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
26077
- return { ok: false, code: v2.code };
26078
- }
25700
+ // src/transport/http-router.ts
25701
+ var import_node_fs14 = __toESM(require("fs"), 1);
25702
+ var import_node_path16 = __toESM(require("path"), 1);
26079
25703
 
26080
- // src/permission/capability-store.ts
26081
- var fs15 = __toESM(require("fs"), 1);
26082
- var path18 = __toESM(require("path"), 1);
26083
- var CAPABILITIES_FILE_NAME = "capabilities.json";
26084
- var FILE_VERSION = 1;
26085
- var CapabilityStore = class {
26086
- constructor(dataDir) {
26087
- this.dataDir = dataDir;
26088
- fs15.mkdirSync(dataDir, { recursive: true });
26089
- this.cache = this.readFromDisk();
26090
- }
25704
+ // src/attachment/group.ts
25705
+ var import_node_fs13 = __toESM(require("fs"), 1);
25706
+ var import_node_path14 = __toESM(require("path"), 1);
25707
+ var import_node_crypto4 = __toESM(require("crypto"), 1);
25708
+ init_protocol();
25709
+ var GroupFileStore = class {
26091
25710
  dataDir;
26092
- cache;
26093
- list() {
26094
- return [...this.cache];
25711
+ logger;
25712
+ cache = /* @__PURE__ */ new Map();
25713
+ constructor(opts) {
25714
+ this.dataDir = opts.dataDir;
25715
+ this.logger = opts.logger;
26095
25716
  }
26096
- upsert(cap) {
26097
- const idx = this.cache.findIndex((c) => c.id === cap.id);
26098
- if (idx >= 0) {
26099
- this.cache[idx] = cap;
26100
- } else {
26101
- this.cache.push(cap);
26102
- }
26103
- this.flush();
25717
+ rootForScope(scope) {
25718
+ return import_node_path14.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
26104
25719
  }
26105
- remove(id) {
26106
- const next = this.cache.filter((c) => c.id !== id);
26107
- if (next.length === this.cache.length) return;
26108
- this.cache = next;
26109
- this.flush();
26110
- }
26111
- filePath() {
26112
- return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
26113
- }
26114
- readFromDisk() {
26115
- const file = this.filePath();
26116
- let raw;
26117
- try {
26118
- raw = fs15.readFileSync(file, "utf8");
26119
- } catch (err) {
26120
- if (err?.code === "ENOENT") return [];
26121
- return [];
26122
- }
26123
- if (!raw.trim()) return [];
26124
- let parsed;
26125
- try {
26126
- parsed = JSON.parse(raw);
26127
- } catch {
26128
- return [];
26129
- }
26130
- if (!parsed || typeof parsed !== "object") return [];
26131
- const arr = parsed.capabilities;
26132
- if (!Array.isArray(arr)) return [];
26133
- const out = [];
26134
- for (const item of arr) {
26135
- const r = CapabilitySchema.safeParse(item);
26136
- if (r.success) out.push(r.data);
26137
- }
26138
- return out;
26139
- }
26140
- flush() {
26141
- const content = { version: FILE_VERSION, capabilities: this.cache };
26142
- this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
26143
- }
26144
- atomicWrite(file, content) {
26145
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26146
- fs15.writeFileSync(tmp, content, { mode: 384 });
26147
- fs15.renameSync(tmp, file);
26148
- try {
26149
- fs15.chmodSync(file, 384);
26150
- } catch {
26151
- }
26152
- }
26153
- };
26154
-
26155
- // src/permission/capability-registry.ts
26156
- var crypto4 = __toESM(require("crypto"), 1);
26157
- var CapabilityRegistry = class {
26158
- constructor(store, now = () => Date.now()) {
26159
- this.store = store;
26160
- this.now = now;
26161
- for (const cap of store.list()) {
26162
- this.bySecretHash.set(cap.secretHash, cap);
26163
- }
26164
- }
26165
- store;
26166
- now;
26167
- // sha256(token) → Capability
26168
- bySecretHash = /* @__PURE__ */ new Map();
26169
- list() {
26170
- return this.store.list();
26171
- }
26172
- verifyToken(token) {
26173
- if (!token) return { ok: false, code: "TOKEN_INVALID" };
26174
- const hash = sha256Hex(token);
26175
- const cap = this.bySecretHash.get(hash);
26176
- if (!cap) return { ok: false, code: "TOKEN_INVALID" };
26177
- if (cap.expiresAt && this.now() >= cap.expiresAt) {
26178
- return { ok: false, code: "TOKEN_EXPIRED" };
26179
- }
26180
- if (cap.maxUses && cap.usedCount >= cap.maxUses) {
26181
- return { ok: false, code: "TOKEN_EXHAUSTED" };
26182
- }
26183
- const updated = { ...cap, usedCount: cap.usedCount + 1 };
26184
- this.bySecretHash.set(hash, updated);
26185
- this.store.upsert(updated);
26186
- return { ok: true, capability: updated };
26187
- }
26188
- upsertCapability(cap) {
26189
- this.bySecretHash.set(cap.secretHash, cap);
26190
- this.store.upsert(cap);
26191
- }
26192
- /**
26193
- * Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
26194
- * 后续 verifyToken 该 token 走 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
26195
- * idempotent:不存在 → null。
26196
- */
26197
- delete(id) {
26198
- const current = this.store.list().find((c) => c.id === id);
26199
- if (!current) return null;
26200
- this.bySecretHash.delete(current.secretHash);
26201
- this.store.remove(id);
26202
- return current;
26203
- }
26204
- findById(id) {
26205
- return this.store.list().find((c) => c.id === id) ?? null;
26206
- }
26207
- };
26208
- function sha256Hex(s) {
26209
- return crypto4.createHash("sha256").update(s).digest("hex");
26210
- }
26211
-
26212
- // src/permission/capability-manager.ts
26213
- var crypto5 = __toESM(require("crypto"), 1);
26214
- var CapabilityManager = class {
26215
- constructor(registry2, hooks = {}) {
26216
- this.registry = registry2;
26217
- this.hooks = hooks;
26218
- }
26219
- registry;
26220
- hooks;
26221
- list() {
26222
- return this.registry.list();
26223
- }
26224
- // whoami handler 用:根据 capabilityId 反查 caller 的 capability 实体。
26225
- // 返回 Capability 持久化形态;handler 调 stripSecretHash 后再上 wire。
26226
- findById(id) {
26227
- return this.registry.findById(id);
26228
- }
26229
- issue(args) {
26230
- if (!args.displayName.trim()) {
26231
- throw new Error("CapabilityManager.issue: displayName must be non-empty");
26232
- }
26233
- const token = (this.hooks.generateToken ?? defaultGenerateToken)();
26234
- const id = (this.hooks.generateId ?? defaultGenerateId)();
26235
- const now = (this.hooks.now ?? Date.now)();
26236
- const cap = {
26237
- id,
26238
- secretHash: sha256Hex2(token),
26239
- displayName: args.displayName,
26240
- grants: args.grants,
26241
- issuedAt: now,
26242
- usedCount: 0,
26243
- personId: args.personId,
26244
- ...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
26245
- ...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
26246
- };
26247
- this.registry.upsertCapability(cap);
26248
- this.hooks.onIssued?.(cap, token);
26249
- return { token, capability: cap };
26250
- }
26251
- /**
26252
- * Per-People token: 同 personId 已有 cap 时 add grants(merge unique by resource+action)。
26253
- * 返回 updated cap。如果 personId 没有 cap 返 null(caller 应该调 issue 新建)。
26254
- */
26255
- addGrantsToPerson(personId, newGrants) {
26256
- const existing = this.registry.list().find((c) => c.personId === personId);
26257
- if (!existing) return null;
26258
- const merged = mergeGrants(existing.grants, newGrants);
26259
- const updated = { ...existing, grants: merged };
26260
- this.registry.upsertCapability(updated);
26261
- this.hooks.onIssued?.(updated, "__merged__");
26262
- return updated;
26263
- }
26264
- /** 按 personId 反查现有 cap(per-People token dedupe)。 */
26265
- findByPerson(personId) {
26266
- return this.registry.list().find((c) => c.personId === personId) ?? null;
26267
- }
26268
- /**
26269
- * Hard delete:从 registry / store 物理移除 capability。
26270
- * idempotent:不存在 → null(handler 翻译成 VALIDATION_ERROR)
26271
- * onDeleted hook 在 daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
26272
- */
26273
- delete(id) {
26274
- const removed = this.registry.delete(id);
26275
- if (!removed) return null;
26276
- this.hooks.onDeleted?.(removed);
26277
- return { capability: removed };
26278
- }
26279
- };
26280
- function mergeGrants(existing, add) {
26281
- const key = (g2) => JSON.stringify({ r: g2.resource, a: [...g2.actions].sort() });
26282
- const seen = new Set(existing.map(key));
26283
- const out = [...existing];
26284
- for (const g2 of add) {
26285
- if (!seen.has(key(g2))) {
26286
- out.push(g2);
26287
- seen.add(key(g2));
26288
- }
26289
- }
26290
- return out;
26291
- }
26292
- function defaultGenerateToken() {
26293
- return crypto5.randomBytes(24).toString("base64url");
26294
- }
26295
- function defaultGenerateId() {
26296
- return "cap_" + crypto5.randomBytes(6).toString("base64url");
26297
- }
26298
- function sha256Hex2(s) {
26299
- return crypto5.createHash("sha256").update(s).digest("hex");
26300
- }
26301
-
26302
- // src/permission/cleanup.ts
26303
- var fs16 = __toESM(require("fs"), 1);
26304
- function cleanupGuestSessionsForCapability(cap, factory) {
26305
- const removed = [];
26306
- for (const g2 of cap.grants) {
26307
- if (g2.resource.type !== "persona") continue;
26308
- const dir = factory.vmGuestRoot(g2.resource.id, cap.personId);
26309
- try {
26310
- fs16.rmSync(dir, { recursive: true, force: true });
26311
- removed.push(dir);
26312
- } catch {
26313
- }
26314
- }
26315
- return { removed };
26316
- }
26317
-
26318
- // src/inbox/inbox-store.ts
26319
- var fs17 = __toESM(require("fs"), 1);
26320
- var path19 = __toESM(require("path"), 1);
26321
- var INBOX_SUBDIR = "inbox";
26322
- var InboxStore = class {
26323
- constructor(dataDir) {
26324
- this.dataDir = dataDir;
26325
- fs17.mkdirSync(this.dirPath(), { recursive: true });
25720
+ /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
25721
+ filePath(scope, sessionId) {
25722
+ return import_node_path14.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
26326
25723
  }
26327
- dataDir;
26328
- /**
26329
- * 列出某条 channel 的消息, 按 createdAt 升序.
26330
- * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
26331
- * channel 文件不存在时返回 [] (不抛, 不创建).
26332
- */
26333
- list(capabilityId, sinceCreatedAt) {
26334
- const file = this.filePath(capabilityId);
26335
- let raw;
26336
- try {
26337
- raw = fs17.readFileSync(file, "utf8");
26338
- } catch (err) {
26339
- if (err?.code === "ENOENT") return [];
26340
- return [];
26341
- }
26342
- const all = parseAllLines(raw);
26343
- if (sinceCreatedAt === void 0) return all;
26344
- return all.filter((m2) => m2.createdAt > sinceCreatedAt);
25724
+ cacheKey(scope, sessionId) {
25725
+ return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
26345
25726
  }
26346
- /**
26347
- * 列出所有 channel 的 capabilityId (扫目录所有 *.jsonl 文件名).
26348
- * 用于 daemon 启动时构建 index ( cap 聚合 unread 计数等).
26349
- */
26350
- listAllCapabilityIds() {
26351
- const dir = this.dirPath();
26352
- let entries;
25727
+ /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
25728
+ readFile(scope, sessionId) {
25729
+ const file = this.filePath(scope, sessionId);
26353
25730
  try {
26354
- entries = fs17.readdirSync(dir);
25731
+ const raw = import_node_fs13.default.readFileSync(file, "utf8");
25732
+ const parsed = JSON.parse(raw);
25733
+ if (!Array.isArray(parsed)) {
25734
+ this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
25735
+ file
25736
+ });
25737
+ return [];
25738
+ }
25739
+ const out = [];
25740
+ for (const entry of parsed) {
25741
+ const r = GroupFileEntrySchema.safeParse(entry);
25742
+ if (r.success) out.push(r.data);
25743
+ }
25744
+ return out;
26355
25745
  } catch (err) {
26356
- if (err?.code === "ENOENT") return [];
25746
+ const code = err?.code;
25747
+ if (code === "ENOENT") return [];
25748
+ this.logger?.warn("GroupFileStore.readFile failed", {
25749
+ file,
25750
+ err: err.message
25751
+ });
26357
25752
  return [];
26358
25753
  }
26359
- return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
26360
25754
  }
26361
- append(message) {
26362
- const file = this.filePath(message.capabilityId);
26363
- const line = JSON.stringify(message) + "\n";
26364
- fs17.appendFileSync(file, line, { mode: 384 });
26365
- try {
26366
- fs17.chmodSync(file, 384);
26367
- } catch {
26368
- }
25755
+ writeFile(scope, sessionId, entries) {
25756
+ const file = this.filePath(scope, sessionId);
25757
+ import_node_fs13.default.mkdirSync(import_node_path14.default.dirname(file), { recursive: true });
25758
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
25759
+ import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
25760
+ import_node_fs13.default.renameSync(tmp, file);
26369
25761
  }
26370
- /**
26371
- * 把该 channel 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
26372
- * readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个 channel 文件.
26373
- * 返写入了几条 (用于测试断言 / index 维护).
26374
- */
26375
- markRead(capabilityId, principalId, upToCreatedAt) {
26376
- const messages = this.list(capabilityId);
26377
- let changed = 0;
26378
- const next = messages.map((m2) => {
26379
- if (m2.senderPrincipalId === principalId) return m2;
26380
- if (m2.createdAt > upToCreatedAt) return m2;
26381
- const cur = m2.readBy[principalId];
26382
- if (cur !== void 0 && cur >= upToCreatedAt) return m2;
26383
- changed++;
26384
- return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
26385
- });
26386
- if (changed === 0) return 0;
26387
- this.rewriteChannel(capabilityId, next);
26388
- return changed;
25762
+ /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
25763
+ list(scope, sessionId) {
25764
+ const key = this.cacheKey(scope, sessionId);
25765
+ const cached = this.cache.get(key);
25766
+ if (cached) return cached.entries;
25767
+ const entries = this.readFile(scope, sessionId);
25768
+ this.cache.set(key, { entries, scope });
25769
+ return entries;
26389
25770
  }
26390
25771
  /**
26391
- * 删整条 channel (capability:delete cascade). 文件不存在 no-op.
25772
+ * upsert:
25773
+ * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
25774
+ * (保留首次入群人 / 入群时刻)
25775
+ * - 不存在 → append,id 派生稳定 uuid,addedAt = now
25776
+ *
25777
+ * 返回最新 entry(caller 可用来 broadcast 通知)。
26392
25778
  */
26393
- removeByCapabilityId(capabilityId) {
26394
- const file = this.filePath(capabilityId);
26395
- try {
26396
- fs17.unlinkSync(file);
26397
- } catch (err) {
26398
- if (err?.code === "ENOENT") return;
26399
- }
26400
- }
26401
- rewriteChannel(capabilityId, messages) {
26402
- const file = this.filePath(capabilityId);
26403
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26404
- const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
26405
- fs17.writeFileSync(tmp, content, { mode: 384 });
26406
- fs17.renameSync(tmp, file);
26407
- try {
26408
- fs17.chmodSync(file, 384);
26409
- } catch {
26410
- }
26411
- }
26412
- dirPath() {
26413
- return path19.join(this.dataDir, INBOX_SUBDIR);
26414
- }
26415
- filePath(capabilityId) {
26416
- return path19.join(this.dirPath(), `${capabilityId}.jsonl`);
26417
- }
26418
- };
26419
- function parseAllLines(raw) {
26420
- const out = [];
26421
- for (const line of raw.split("\n")) {
26422
- const trimmed = line.trim();
26423
- if (!trimmed) continue;
26424
- let parsed;
26425
- try {
26426
- parsed = JSON.parse(trimmed);
26427
- } catch {
26428
- continue;
25779
+ upsert(scope, sessionId, input, now = Date.now()) {
25780
+ const entries = this.list(scope, sessionId).slice();
25781
+ const idx = entries.findIndex((e) => e.relPath === input.relPath);
25782
+ let next;
25783
+ if (idx >= 0) {
25784
+ const prev = entries[idx];
25785
+ next = {
25786
+ ...prev,
25787
+ size: input.size,
25788
+ mime: input.mime,
25789
+ lastEditedAt: now,
25790
+ stale: false
25791
+ // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
25792
+ };
25793
+ entries[idx] = next;
25794
+ } else {
25795
+ next = {
25796
+ id: `gf-${import_node_crypto4.default.randomBytes(6).toString("base64url")}`,
25797
+ relPath: input.relPath,
25798
+ from: input.from,
25799
+ label: input.label,
25800
+ size: input.size,
25801
+ mime: input.mime,
25802
+ addedAt: now
25803
+ // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
25804
+ };
25805
+ entries.push(next);
26429
25806
  }
26430
- const r = InboxMessageSchema.safeParse(parsed);
26431
- if (r.success) out.push(r.data);
26432
- }
26433
- return out;
26434
- }
26435
-
26436
- // src/inbox/inbox-manager.ts
26437
- var crypto6 = __toESM(require("crypto"), 1);
26438
- var InboxManager = class {
26439
- constructor(store, broadcast, opts = {}) {
26440
- this.store = store;
26441
- this.broadcast = broadcast;
26442
- this.now = opts.now ?? Date.now;
26443
- this.genId = opts.genId ?? defaultGenId;
25807
+ this.writeFile(scope, sessionId, entries);
25808
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25809
+ return next;
26444
25810
  }
26445
- store;
26446
- broadcast;
26447
- now;
26448
- genId;
26449
25811
  /**
26450
- * 写入一条消息到 channel + broadcast 给该 channel 两端 ws.
26451
- * caller 必须已通过 handler 鉴权 (guest ctx.capabilityId === args.capabilityId, 否则拦截).
25812
+ * 标记一个 relPath stale(agent rm / mv 后文件不在)。
25813
+ * - 命中 stale=true,UI 灰显
25814
+ * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
25815
+ *
25816
+ * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
26452
25817
  */
26453
- postMessage(args) {
26454
- const msg = {
26455
- id: this.genId(),
26456
- capabilityId: args.capabilityId,
26457
- senderPrincipalId: args.senderPrincipalId,
26458
- text: args.text,
26459
- createdAt: this.now(),
26460
- readBy: {}
26461
- };
26462
- this.store.append(msg);
26463
- this.broadcast(args.capabilityId, { type: "inbox:event", message: msg });
26464
- return msg;
26465
- }
26466
- list(capabilityId, sinceCreatedAt) {
26467
- return this.store.list(capabilityId, sinceCreatedAt);
25818
+ markStale(scope, sessionId, relPath) {
25819
+ const entries = this.list(scope, sessionId).slice();
25820
+ const idx = entries.findIndex((e) => e.relPath === relPath);
25821
+ if (idx < 0) return;
25822
+ if (entries[idx].stale) return;
25823
+ entries[idx] = { ...entries[idx], stale: true };
25824
+ this.writeFile(scope, sessionId, entries);
25825
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
26468
25826
  }
26469
25827
  /**
26470
- * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
26471
- * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道),
26472
- * 让对端 UI 实时看到 "对方已读到哪了".
25828
+ * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
25829
+ * agent 自动入群的不应走这条 —— markStale 表达"文件不在了"语义;
25830
+ * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
25831
+ *
25832
+ * 返回值:true=命中并删除;false=relPath 不在群里。
26473
25833
  */
26474
- markRead(args) {
26475
- const before = this.store.list(args.capabilityId);
26476
- const changed = this.store.markRead(args.capabilityId, args.principalId, args.upToCreatedAt);
26477
- if (changed === 0) return 0;
26478
- const after = this.store.list(args.capabilityId);
26479
- const beforeById = new Map(before.map((m2) => [m2.id, m2]));
26480
- for (const m2 of after) {
26481
- const prev = beforeById.get(m2.id);
26482
- if (!prev) continue;
26483
- if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
26484
- this.broadcast(args.capabilityId, { type: "inbox:event", message: m2 });
26485
- }
26486
- return changed;
26487
- }
26488
- /** capability:delete cascade. 删整个 channel 文件. */
26489
- removeChannel(capabilityId) {
26490
- this.store.removeByCapabilityId(capabilityId);
26491
- }
26492
- };
26493
- function defaultGenId() {
26494
- return "m_" + crypto6.randomBytes(6).toString("base64url");
26495
- }
26496
-
26497
- // src/remote-persona/store.ts
26498
- var fs18 = __toESM(require("fs"), 1);
26499
- var path20 = __toESM(require("path"), 1);
26500
- var REMOTE_PERSONAS_DIR = "remote-personas";
26501
- var RemotePersonaStore = class {
26502
- constructor(dataDir) {
26503
- this.dataDir = dataDir;
26504
- fs18.mkdirSync(this.rootDir(), { recursive: true });
26505
- }
26506
- dataDir;
26507
- list() {
26508
- let entries;
26509
- try {
26510
- entries = fs18.readdirSync(this.rootDir());
26511
- } catch (err) {
26512
- if (err?.code === "ENOENT") return [];
26513
- return [];
26514
- }
26515
- const out = [];
26516
- for (const name of entries) {
26517
- if (!name.endsWith(".json")) continue;
26518
- if (name.includes(".tmp-")) continue;
26519
- const file = path20.join(this.rootDir(), name);
26520
- let raw;
26521
- try {
26522
- raw = fs18.readFileSync(file, "utf8");
26523
- } catch {
26524
- continue;
26525
- }
26526
- let parsed;
26527
- try {
26528
- parsed = JSON.parse(raw);
26529
- } catch {
26530
- continue;
26531
- }
26532
- const r = RemotePersonaSchema.safeParse(parsed);
26533
- if (r.success) out.push(r.data);
26534
- }
26535
- return out.sort((a, b2) => a.addedAt > b2.addedAt ? -1 : a.addedAt < b2.addedAt ? 1 : 0);
26536
- }
26537
- get(alias) {
26538
- const file = this.filePath(alias);
26539
- let raw;
26540
- try {
26541
- raw = fs18.readFileSync(file, "utf8");
26542
- } catch {
26543
- return null;
26544
- }
26545
- try {
26546
- const parsed = JSON.parse(raw);
26547
- const r = RemotePersonaSchema.safeParse(parsed);
26548
- return r.success ? r.data : null;
26549
- } catch {
26550
- return null;
26551
- }
26552
- }
26553
- add(rp) {
26554
- const file = this.filePath(rp.alias);
26555
- if (fs18.existsSync(file)) {
26556
- throw new Error(`RemotePersonaStore.add: alias already exists: ${rp.alias}`);
26557
- }
26558
- this.atomicWrite(file, rp);
26559
- }
26560
- remove(alias) {
26561
- const file = this.filePath(alias);
26562
- try {
26563
- fs18.unlinkSync(file);
26564
- return true;
26565
- } catch (err) {
26566
- if (err?.code === "ENOENT") return false;
26567
- throw err;
26568
- }
25834
+ remove(scope, sessionId, relPath) {
25835
+ const entries = this.list(scope, sessionId).slice();
25836
+ const idx = entries.findIndex((e) => e.relPath === relPath);
25837
+ if (idx < 0) return false;
25838
+ entries.splice(idx, 1);
25839
+ this.writeFile(scope, sessionId, entries);
25840
+ this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
25841
+ return true;
26569
25842
  }
26570
25843
  /**
26571
- * patch lastConnectedAt (v2 outgoing client 连上时更新). 其他字段不动.
26572
- * 不存在 alias → no-op.
25844
+ * session 聚合查询(spec §4 HTTP ACL:personal 视野并集)。
25845
+ *
25846
+ * 扫 <dataDir>/sessions/<personaId>/owner/*.group-files.json 和
25847
+ * <dataDir>/sessions/<personaId>/listener/*.group-files.json,每文件读一份。
25848
+ *
25849
+ * 复杂度 O(N sessions × N entries),N 通常 < 100,可接受。
26573
25850
  */
26574
- updateLastConnectedAt(alias, at) {
26575
- const cur = this.get(alias);
26576
- if (!cur) return;
26577
- this.atomicWrite(this.filePath(alias), { ...cur, lastConnectedAt: at });
26578
- }
26579
- rootDir() {
26580
- return path20.join(this.dataDir, REMOTE_PERSONAS_DIR);
26581
- }
26582
- filePath(alias) {
26583
- return path20.join(this.rootDir(), `${safeFileName(alias)}.json`);
26584
- }
26585
- atomicWrite(file, content) {
26586
- fs18.mkdirSync(this.rootDir(), { recursive: true });
26587
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26588
- fs18.writeFileSync(tmp, JSON.stringify(content, null, 2), { mode: 384 });
26589
- fs18.renameSync(tmp, file);
26590
- try {
26591
- fs18.chmodSync(file, 384);
26592
- } catch {
26593
- }
26594
- }
26595
- };
26596
-
26597
- // src/person/store.ts
26598
- var fs19 = __toESM(require("fs"), 1);
26599
- var path21 = __toESM(require("path"), 1);
26600
- var PERSONS_DIR = "persons";
26601
- var PersonStore = class {
26602
- constructor(dataDir) {
26603
- this.dataDir = dataDir;
26604
- fs19.mkdirSync(this.rootDir(), { recursive: true });
26605
- }
26606
- dataDir;
26607
- list() {
26608
- let entries;
26609
- try {
26610
- entries = fs19.readdirSync(this.rootDir());
26611
- } catch (err) {
26612
- if (err?.code === "ENOENT") return [];
26613
- return [];
26614
- }
25851
+ listByPersona(personaId) {
26615
25852
  const out = [];
26616
- for (const name of entries) {
26617
- if (!name.endsWith(".json")) continue;
26618
- if (name.includes(".tmp-")) continue;
26619
- const file = path21.join(this.rootDir(), name);
26620
- let raw;
25853
+ for (const mode of ["owner", "listener"]) {
25854
+ const scope = { kind: "persona", personaId, mode };
25855
+ const root = this.rootForScope(scope);
25856
+ let names;
26621
25857
  try {
26622
- raw = fs19.readFileSync(file, "utf8");
26623
- } catch {
25858
+ names = import_node_fs13.default.readdirSync(root);
25859
+ } catch (err) {
25860
+ const code = err?.code;
25861
+ if (code === "ENOENT") continue;
26624
25862
  continue;
26625
25863
  }
26626
- let parsed;
26627
- try {
26628
- parsed = JSON.parse(raw);
26629
- } catch {
26630
- continue;
25864
+ for (const name of names) {
25865
+ if (!name.endsWith(".group-files.json")) continue;
25866
+ const sessionId = name.slice(0, -".group-files.json".length);
25867
+ if (!sessionId) continue;
25868
+ const entries = this.list(scope, sessionId);
25869
+ out.push({ sessionId, entries });
26631
25870
  }
26632
- const r = PersonSchema.safeParse(parsed);
26633
- if (r.success) out.push(r.data);
26634
- }
26635
- return out.sort((a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0);
26636
- }
26637
- get(id) {
26638
- const file = this.filePath(id);
26639
- let raw;
26640
- try {
26641
- raw = fs19.readFileSync(file, "utf8");
26642
- } catch {
26643
- return null;
26644
- }
26645
- try {
26646
- const parsed = JSON.parse(raw);
26647
- const r = PersonSchema.safeParse(parsed);
26648
- return r.success ? r.data : null;
26649
- } catch {
26650
- return null;
26651
- }
26652
- }
26653
- add(person) {
26654
- const file = this.filePath(person.id);
26655
- if (fs19.existsSync(file)) {
26656
- throw new Error(`PersonStore.add: person id already exists: ${person.id}`);
26657
- }
26658
- this.atomicWrite(file, person);
26659
- }
26660
- /**
26661
- * Merge patch into existing person + bump updatedAt to `now`.
26662
- * 不存在 id → no-op(caller 应预先 get 校验)。
26663
- * 只允许 patch displayName / notes / dmEnabled(id / createdAt 不可改)。
26664
- */
26665
- update(id, patch, now) {
26666
- const cur = this.get(id);
26667
- if (!cur) return;
26668
- const next = {
26669
- ...cur,
26670
- ...patch.displayName !== void 0 ? { displayName: patch.displayName } : {},
26671
- ...patch.notes !== void 0 ? { notes: patch.notes } : {},
26672
- ...patch.dmEnabled !== void 0 ? { dmEnabled: patch.dmEnabled } : {},
26673
- updatedAt: now
26674
- };
26675
- this.atomicWrite(this.filePath(id), next);
26676
- }
26677
- remove(id) {
26678
- const file = this.filePath(id);
26679
- try {
26680
- fs19.unlinkSync(file);
26681
- return true;
26682
- } catch (err) {
26683
- if (err?.code === "ENOENT") return false;
26684
- throw err;
26685
- }
26686
- }
26687
- rootDir() {
26688
- return path21.join(this.dataDir, PERSONS_DIR);
26689
- }
26690
- filePath(id) {
26691
- return path21.join(this.rootDir(), `${safeFileName(id)}.json`);
26692
- }
26693
- atomicWrite(file, content) {
26694
- fs19.mkdirSync(this.rootDir(), { recursive: true });
26695
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
26696
- fs19.writeFileSync(tmp, JSON.stringify(content, null, 2), { mode: 384 });
26697
- fs19.renameSync(tmp, file);
26698
- try {
26699
- fs19.chmodSync(file, 384);
26700
- } catch {
26701
25871
  }
25872
+ return out;
26702
25873
  }
26703
25874
  };
26704
-
26705
- // src/migrations/2026-05-20-flatten-sessions.ts
26706
- var fs20 = __toESM(require("fs"), 1);
26707
- var path22 = __toESM(require("path"), 1);
26708
- var MIGRATION_FLAG_NAME = ".migration.v1.done";
26709
- function migrateFlattenSessions(opts) {
26710
- const dataDir = opts.dataDir;
26711
- const now = opts.now ?? Date.now;
26712
- const sessionsDir = path22.join(dataDir, "sessions");
26713
- const flagPath = path22.join(sessionsDir, MIGRATION_FLAG_NAME);
26714
- if (existsSync5(flagPath)) {
26715
- return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
26716
- }
26717
- let movedBare = 0;
26718
- let movedVmOwner = 0;
26719
- let archivedListener = 0;
26720
- const defaultDir = path22.join(sessionsDir, "default");
26721
- if (existsSync5(defaultDir)) {
26722
- for (const entry of readdirSafe(defaultDir)) {
26723
- if (!entry.endsWith(".json")) continue;
26724
- const src = path22.join(defaultDir, entry);
26725
- const dst = path22.join(sessionsDir, entry);
26726
- fs20.renameSync(src, dst);
26727
- movedBare += 1;
26728
- }
26729
- rmdirIfEmpty(defaultDir);
26730
- }
26731
- for (const pid of readdirSafe(sessionsDir)) {
26732
- const personaDir = path22.join(sessionsDir, pid);
26733
- if (!isDir(personaDir)) continue;
26734
- if (pid === "default") continue;
26735
- const ownerSrc = path22.join(personaDir, "owner");
26736
- if (existsSync5(ownerSrc) && isDir(ownerSrc)) {
26737
- const ownerDst = path22.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
26738
- fs20.mkdirSync(ownerDst, { recursive: true });
26739
- for (const file of readdirSafe(ownerSrc)) {
26740
- if (!file.endsWith(".json")) continue;
26741
- fs20.renameSync(path22.join(ownerSrc, file), path22.join(ownerDst, file));
26742
- movedVmOwner += 1;
26743
- }
26744
- rmdirIfEmpty(ownerSrc);
26745
- }
26746
- const listenerSrc = path22.join(personaDir, "listener");
26747
- if (existsSync5(listenerSrc) && isDir(listenerSrc)) {
26748
- const archiveDst = path22.join(dataDir, ".legacy", `listener-${pid}`);
26749
- fs20.mkdirSync(archiveDst, { recursive: true });
26750
- for (const file of readdirSafe(listenerSrc)) {
26751
- if (!file.endsWith(".json")) continue;
26752
- fs20.renameSync(path22.join(listenerSrc, file), path22.join(archiveDst, file));
26753
- archivedListener += 1;
26754
- }
26755
- rmdirIfEmpty(listenerSrc);
26756
- }
26757
- rmdirIfEmpty(personaDir);
26758
- }
26759
- fs20.mkdirSync(sessionsDir, { recursive: true });
26760
- fs20.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
26761
- return {
26762
- skipped: false,
26763
- flagWritten: true,
26764
- movedBare,
26765
- movedVmOwner,
26766
- archivedListener
26767
- };
26768
- }
26769
- function existsSync5(p2) {
26770
- try {
26771
- fs20.statSync(p2);
26772
- return true;
26773
- } catch {
26774
- return false;
26775
- }
26776
- }
26777
- function isDir(p2) {
26778
- try {
26779
- return fs20.statSync(p2).isDirectory();
26780
- } catch {
25875
+ function personalViewable(groupStore, personaDir, personaId, absPath) {
25876
+ const realTarget = safeRealpath(absPath);
25877
+ if (!realTarget) {
26781
25878
  return false;
26782
25879
  }
26783
- }
26784
- function readdirSafe(p2) {
26785
- try {
26786
- return fs20.readdirSync(p2);
26787
- } catch {
26788
- return [];
25880
+ const personasUnion = groupStore.listByPersona(personaId);
25881
+ for (const { entries } of personasUnion) {
25882
+ for (const e of entries) {
25883
+ if (e.stale) continue;
25884
+ const realEntry = safeRealpath(import_node_path14.default.join(personaDir, e.relPath));
25885
+ if (realEntry && realEntry === realTarget) return true;
25886
+ }
26789
25887
  }
25888
+ return false;
26790
25889
  }
26791
- function rmdirIfEmpty(p2) {
25890
+ function safeRealpath(p2) {
26792
25891
  try {
26793
- fs20.rmdirSync(p2);
25892
+ return import_node_fs13.default.realpathSync(p2);
26794
25893
  } catch {
25894
+ return null;
26795
25895
  }
26796
25896
  }
26797
25897
 
26798
- // src/transport/http-router.ts
26799
- var import_node_fs13 = __toESM(require("fs"), 1);
26800
- var import_node_path15 = __toESM(require("path"), 1);
26801
-
26802
25898
  // src/attachment/mime.ts
26803
- var import_node_path14 = __toESM(require("path"), 1);
25899
+ var import_node_path15 = __toESM(require("path"), 1);
26804
25900
  var TEXT_PLAIN = "text/plain; charset=utf-8";
26805
25901
  var EXT_TO_NATIVE_MIME = {
26806
25902
  // 图片
@@ -26907,14 +26003,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
26907
26003
  ".mk"
26908
26004
  ]);
26909
26005
  function lookupMime(filePathOrName) {
26910
- const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
26006
+ const ext = import_node_path15.default.extname(filePathOrName).toLowerCase();
26911
26007
  if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
26912
26008
  if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
26913
26009
  return "application/octet-stream";
26914
26010
  }
26915
26011
 
26916
26012
  // src/attachment/sign-url.ts
26917
- var import_node_crypto4 = __toESM(require("crypto"), 1);
26013
+ var import_node_crypto5 = __toESM(require("crypto"), 1);
26918
26014
  var HMAC_ALGO = "sha256";
26919
26015
  function base64urlEncode(buf) {
26920
26016
  const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
@@ -26931,7 +26027,7 @@ function decodeAbsPathFromUrl(encoded) {
26931
26027
  }
26932
26028
  function computeSig(secret, absPath, e) {
26933
26029
  const msg = e === null ? absPath : `${absPath}|${e}`;
26934
- return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26030
+ return import_node_crypto5.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
26935
26031
  }
26936
26032
  function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
26937
26033
  const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
@@ -26966,7 +26062,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
26966
26062
  if (provided.length !== expected.length) {
26967
26063
  return { ok: false, code: "BAD_SIG" };
26968
26064
  }
26969
- if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
26065
+ if (!import_node_crypto5.default.timingSafeEqual(provided, expected)) {
26970
26066
  return { ok: false, code: "BAD_SIG" };
26971
26067
  }
26972
26068
  if (e !== null && now() / 1e3 > e) {
@@ -26987,7 +26083,7 @@ function createHttpRouter(deps) {
26987
26083
  sendJson(res, 200, { ok: true, version: deps.daemonVersion });
26988
26084
  return true;
26989
26085
  }
26990
- if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26086
+ if (!url.pathname.startsWith("/persona/") && !url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
26991
26087
  return false;
26992
26088
  }
26993
26089
  if (url.pathname.startsWith("/files/") && req.method === "GET") {
@@ -27027,8 +26123,43 @@ function createHttpRouter(deps) {
27027
26123
  sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
27028
26124
  return true;
27029
26125
  }
26126
+ const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
26127
+ if (personaFilesMatch && req.method === "GET") {
26128
+ const pid = personaFilesMatch[1];
26129
+ const pathParam = url.searchParams.get("path");
26130
+ if (!pathParam) {
26131
+ sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
26132
+ return true;
26133
+ }
26134
+ if (!deps.personaStore || !deps.groupFileStore) {
26135
+ sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
26136
+ return true;
26137
+ }
26138
+ const personaDir = deps.personaStore.personaDirPath(pid);
26139
+ const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
26140
+ if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
26141
+ sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
26142
+ return true;
26143
+ }
26144
+ if (ctx.role === "personal") {
26145
+ if (ctx.personaId !== pid) {
26146
+ sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
26147
+ return true;
26148
+ }
26149
+ if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
26150
+ sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
26151
+ return true;
26152
+ }
26153
+ }
26154
+ streamFile(res, absPath, deps.logger);
26155
+ return true;
26156
+ }
27030
26157
  const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
27031
26158
  if (sessionFilesMatch && req.method === "GET") {
26159
+ if (ctx.role !== "owner") {
26160
+ sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
26161
+ return true;
26162
+ }
27032
26163
  const sid = sessionFilesMatch[1];
27033
26164
  const pathParam = url.searchParams.get("path");
27034
26165
  if (!pathParam) {
@@ -27036,7 +26167,7 @@ function createHttpRouter(deps) {
27036
26167
  return true;
27037
26168
  }
27038
26169
  let absPath;
27039
- if (import_node_path15.default.isAbsolute(pathParam)) {
26170
+ if (import_node_path16.default.isAbsolute(pathParam)) {
27040
26171
  absPath = pathParam;
27041
26172
  } else if (deps.sessionStore) {
27042
26173
  const file = deps.sessionStore.read(sid);
@@ -27044,7 +26175,7 @@ function createHttpRouter(deps) {
27044
26175
  sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
27045
26176
  return true;
27046
26177
  }
27047
- absPath = import_node_path15.default.join(file.cwd, pathParam);
26178
+ absPath = import_node_path16.default.join(file.cwd, pathParam);
27048
26179
  } else {
27049
26180
  sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
27050
26181
  return true;
@@ -27069,12 +26200,18 @@ function sendJson(res, status, body) {
27069
26200
  res.end(JSON.stringify(body));
27070
26201
  }
27071
26202
  function withCtx(ctx, body) {
27072
- return { ...body, role: ctx.role };
26203
+ return { ...body, role: ctx.role, personaId: ctx.personaId };
26204
+ }
26205
+ function isContainedIn(abs, root) {
26206
+ const normalized = import_node_path16.default.resolve(abs);
26207
+ const normalizedRoot = import_node_path16.default.resolve(root);
26208
+ if (normalized === normalizedRoot) return true;
26209
+ return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
27073
26210
  }
27074
26211
  function streamFile(res, absPath, logger) {
27075
26212
  let stat;
27076
26213
  try {
27077
- stat = import_node_fs13.default.statSync(absPath);
26214
+ stat = import_node_fs14.default.statSync(absPath);
27078
26215
  } catch (err) {
27079
26216
  const code = err?.code;
27080
26217
  if (code === "ENOENT") {
@@ -27089,7 +26226,7 @@ function streamFile(res, absPath, logger) {
27089
26226
  return;
27090
26227
  }
27091
26228
  const mime = lookupMime(absPath);
27092
- const basename = import_node_path15.default.basename(absPath);
26229
+ const basename = import_node_path16.default.basename(absPath);
27093
26230
  res.writeHead(200, {
27094
26231
  "Content-Type": mime,
27095
26232
  "Content-Length": String(stat.size),
@@ -27097,154 +26234,13 @@ function streamFile(res, absPath, logger) {
27097
26234
  // 防止浏览器把任意 mime 当 html 渲染
27098
26235
  "X-Content-Type-Options": "nosniff"
27099
26236
  });
27100
- const stream = import_node_fs13.default.createReadStream(absPath);
26237
+ const stream = import_node_fs14.default.createReadStream(absPath);
27101
26238
  stream.on("error", (err) => {
27102
- logger?.warn("streamFile read error", { absPath, err: err.message });
27103
- res.destroy();
27104
- });
27105
- stream.pipe(res);
27106
- }
27107
-
27108
- // src/attachment/group.ts
27109
- var import_node_fs14 = __toESM(require("fs"), 1);
27110
- var import_node_path16 = __toESM(require("path"), 1);
27111
- var import_node_crypto5 = __toESM(require("crypto"), 1);
27112
- init_protocol();
27113
- var GroupFileStore = class {
27114
- dataDir;
27115
- logger;
27116
- cache = /* @__PURE__ */ new Map();
27117
- constructor(opts) {
27118
- this.dataDir = opts.dataDir;
27119
- this.logger = opts.logger;
27120
- }
27121
- rootForScope(scope) {
27122
- return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
27123
- }
27124
- /** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
27125
- filePath(scope, sessionId) {
27126
- return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
27127
- }
27128
- cacheKey(scope, sessionId) {
27129
- return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
27130
- }
27131
- /** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
27132
- readFile(scope, sessionId) {
27133
- const file = this.filePath(scope, sessionId);
27134
- try {
27135
- const raw = import_node_fs14.default.readFileSync(file, "utf8");
27136
- const parsed = JSON.parse(raw);
27137
- if (!Array.isArray(parsed)) {
27138
- this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
27139
- file
27140
- });
27141
- return [];
27142
- }
27143
- const out = [];
27144
- for (const entry of parsed) {
27145
- const r = GroupFileEntrySchema.safeParse(entry);
27146
- if (r.success) out.push(r.data);
27147
- }
27148
- return out;
27149
- } catch (err) {
27150
- const code = err?.code;
27151
- if (code === "ENOENT") return [];
27152
- this.logger?.warn("GroupFileStore.readFile failed", {
27153
- file,
27154
- err: err.message
27155
- });
27156
- return [];
27157
- }
27158
- }
27159
- writeFile(scope, sessionId, entries) {
27160
- const file = this.filePath(scope, sessionId);
27161
- import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
27162
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
27163
- import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
27164
- import_node_fs14.default.renameSync(tmp, file);
27165
- }
27166
- /** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
27167
- list(scope, sessionId) {
27168
- const key = this.cacheKey(scope, sessionId);
27169
- const cached = this.cache.get(key);
27170
- if (cached) return cached.entries;
27171
- const entries = this.readFile(scope, sessionId);
27172
- this.cache.set(key, { entries });
27173
- return entries;
27174
- }
27175
- /**
27176
- * upsert:
27177
- * - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
27178
- * (保留首次入群人 / 入群时刻)
27179
- * - 不存在 → append,id 派生稳定 uuid,addedAt = now
27180
- *
27181
- * 返回最新 entry(caller 可用来 broadcast 通知)。
27182
- */
27183
- upsert(scope, sessionId, input, now = Date.now()) {
27184
- const entries = this.list(scope, sessionId).slice();
27185
- const idx = entries.findIndex((e) => e.relPath === input.relPath);
27186
- let next;
27187
- if (idx >= 0) {
27188
- const prev = entries[idx];
27189
- next = {
27190
- ...prev,
27191
- size: input.size,
27192
- mime: input.mime,
27193
- lastEditedAt: now,
27194
- stale: false
27195
- // label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
27196
- };
27197
- entries[idx] = next;
27198
- } else {
27199
- next = {
27200
- id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
27201
- relPath: input.relPath,
27202
- from: input.from,
27203
- label: input.label,
27204
- size: input.size,
27205
- mime: input.mime,
27206
- addedAt: now
27207
- // agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
27208
- };
27209
- entries.push(next);
27210
- }
27211
- this.writeFile(scope, sessionId, entries);
27212
- this.cache.set(this.cacheKey(scope, sessionId), { entries });
27213
- return next;
27214
- }
27215
- /**
27216
- * 标记一个 relPath stale(agent rm / mv 后文件不在)。
27217
- * - 命中 → stale=true,UI 灰显
27218
- * - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
27219
- *
27220
- * 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
27221
- */
27222
- markStale(scope, sessionId, relPath) {
27223
- const entries = this.list(scope, sessionId).slice();
27224
- const idx = entries.findIndex((e) => e.relPath === relPath);
27225
- if (idx < 0) return;
27226
- if (entries[idx].stale) return;
27227
- entries[idx] = { ...entries[idx], stale: true };
27228
- this.writeFile(scope, sessionId, entries);
27229
- this.cache.set(this.cacheKey(scope, sessionId), { entries });
27230
- }
27231
- /**
27232
- * 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
27233
- * agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
27234
- * owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
27235
- *
27236
- * 返回值:true=命中并删除;false=relPath 不在群里。
27237
- */
27238
- remove(scope, sessionId, relPath) {
27239
- const entries = this.list(scope, sessionId).slice();
27240
- const idx = entries.findIndex((e) => e.relPath === relPath);
27241
- if (idx < 0) return false;
27242
- entries.splice(idx, 1);
27243
- this.writeFile(scope, sessionId, entries);
27244
- this.cache.set(this.cacheKey(scope, sessionId), { entries });
27245
- return true;
27246
- }
27247
- };
26239
+ logger?.warn("streamFile read error", { absPath, err: err.message });
26240
+ res.destroy();
26241
+ });
26242
+ stream.pipe(res);
26243
+ }
27248
26244
 
27249
26245
  // src/discovery/state-file.ts
27250
26246
  var import_node_fs15 = __toESM(require("fs"), 1);
@@ -27922,44 +26918,39 @@ var AUTH_FILE_NAME = "auth.json";
27922
26918
  function authFilePath(dataDir) {
27923
26919
  return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
27924
26920
  }
27925
- function loadOrCreateAuth(opts) {
26921
+ function loadOrCreateAuthFile(opts) {
27926
26922
  const file = authFilePath(opts.dataDir);
26923
+ const generate = opts.generate ?? defaultGenerate;
26924
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
27927
26925
  const existing = readAuthFile(file);
27928
- const genToken = opts.generate ?? defaultGenerateToken2;
27929
- const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
27930
- const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
27931
- if (existing) {
27932
- if (!existing.ownerPrincipalId) {
27933
- const ownerPrincipalId2 = genId();
27934
- writeAuthFile(file, {
27935
- token: existing.token,
27936
- ownerPrincipalId: ownerPrincipalId2,
27937
- createdAt: existing.createdAt
27938
- });
27939
- return { token: existing.token, ownerPrincipalId: ownerPrincipalId2 };
27940
- }
27941
- return { token: existing.token, ownerPrincipalId: existing.ownerPrincipalId };
26926
+ if (existing && existing.token && existing.signSecret) {
26927
+ return {
26928
+ token: existing.token,
26929
+ signSecret: existing.signSecret,
26930
+ createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
26931
+ };
27942
26932
  }
27943
- const token = genToken();
27944
- const ownerPrincipalId = genId();
27945
- writeAuthFile(file, { token, ownerPrincipalId, createdAt: now.toISOString() });
27946
- return { token, ownerPrincipalId };
26933
+ const token = existing?.token || generate();
26934
+ const signSecret = existing?.signSecret || generate();
26935
+ const createdAt = existing?.createdAt || now().toISOString();
26936
+ const next = { token, signSecret, createdAt };
26937
+ writeAuthFile(file, next);
26938
+ return next;
27947
26939
  }
27948
- function defaultGenerateToken2() {
26940
+ function defaultGenerate() {
27949
26941
  return import_node_crypto8.default.randomBytes(32).toString("base64url");
27950
26942
  }
27951
- function defaultGenerateOwnerPrincipalId() {
27952
- return `owner-${import_node_crypto8.default.randomUUID()}`;
27953
- }
27954
26943
  function readAuthFile(file) {
27955
26944
  try {
27956
26945
  const raw = import_node_fs20.default.readFileSync(file, "utf8");
27957
26946
  const parsed = JSON.parse(raw);
27958
- if (typeof parsed?.token !== "string" || parsed.token.length === 0) return null;
26947
+ if (typeof parsed?.token !== "string" || parsed.token.length === 0) {
26948
+ return null;
26949
+ }
27959
26950
  return {
27960
26951
  token: parsed.token,
27961
- ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : "",
27962
- createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
26952
+ signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
26953
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
27963
26954
  };
27964
26955
  } catch (err) {
27965
26956
  const code = err?.code;
@@ -28072,62 +27063,10 @@ function forkSession(input) {
28072
27063
  return { forkedToolSessionId, forkedFilePath };
28073
27064
  }
28074
27065
 
28075
- // src/permission/capability.ts
28076
- function matchResource(grant, target) {
28077
- if (grant.type === "*") return true;
28078
- if (grant.type !== target.type) return false;
28079
- return grant.id === target.id;
28080
- }
28081
- function assertGrant(grants, resource, action) {
28082
- for (const g2 of grants) {
28083
- if (!matchResource(g2.resource, resource)) continue;
28084
- if (g2.actions.includes(action)) return true;
28085
- if (g2.actions.includes("admin")) return true;
28086
- }
28087
- return false;
28088
- }
28089
-
28090
- // src/permission/session-access.ts
28091
- function canAccessSession(ctx, sessionId, action, deps) {
28092
- if (ctx.principal.kind === "owner") return true;
28093
- const file = deps.readSession(sessionId);
28094
- if (!file) return true;
28095
- return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
28096
- }
28097
- function canAccessPersona(grants, personaId, action) {
28098
- if (!personaId) return false;
28099
- const resource = { type: "persona", id: personaId };
28100
- return assertGrant(grants, resource, action);
28101
- }
28102
-
28103
27066
  // src/handlers/session.ts
28104
- init_protocol();
28105
27067
  function buildSessionHandlers(deps) {
28106
- const { manager, observer, getAdapter: getAdapter2, store } = deps;
28107
- const ensureSessionAccess = (ctx, sessionId, action) => {
28108
- if (!ctx) return;
28109
- const ok = canAccessSession(ctx, sessionId, action, {
28110
- readSession: (sid) => store.read(sid)
28111
- });
28112
- if (!ok) {
28113
- throw new ClawdError(
28114
- ERROR_CODES.UNAUTHORIZED,
28115
- `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} session ${sessionId}`
28116
- );
28117
- }
28118
- };
28119
- const ensurePersonaAccess = (ctx, personaId, action) => {
28120
- if (!ctx) return;
28121
- if (ctx.principal.kind === "owner") return;
28122
- const ok = canAccessPersona(ctx.grants, personaId, action);
28123
- if (!ok) {
28124
- throw new ClawdError(
28125
- ERROR_CODES.UNAUTHORIZED,
28126
- `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} on persona:${personaId ?? "<none>"}`
28127
- );
28128
- }
28129
- };
28130
- const create = async (frame, _client, ctx) => {
27068
+ const { manager, observer, getAdapter: getAdapter2 } = deps;
27069
+ const create = async (frame) => {
28131
27070
  const args = SessionCreateArgs.parse(frame);
28132
27071
  if (args.ownerPersonaId) {
28133
27072
  const persona = deps.personaRegistry.get(args.ownerPersonaId);
@@ -28135,84 +27074,58 @@ function buildSessionHandlers(deps) {
28135
27074
  throw new Error(`persona not found: ${args.ownerPersonaId}`);
28136
27075
  }
28137
27076
  }
28138
- ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
28139
- let creatorPrincipalId = "";
28140
- if (ctx) {
28141
- if (ctx.principal.kind === "guest" && ctx.capabilityId && deps.capabilityManager) {
28142
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
28143
- creatorPrincipalId = cap?.personId ?? ctx.principal.id;
28144
- } else {
28145
- creatorPrincipalId = ctx.principal.id;
28146
- }
28147
- }
28148
- const { response, broadcast } = manager.create(args, creatorPrincipalId);
27077
+ const { response, broadcast } = manager.create(args);
28149
27078
  return { response: { type: "session:info", ...response }, broadcast };
28150
27079
  };
28151
- const list = async (_frame, _client, ctx) => {
27080
+ const list = async () => {
28152
27081
  const { response } = manager.list();
28153
- if (ctx && ctx.principal.kind === "guest") {
28154
- const sessions = response.sessions ?? [];
28155
- const filtered = sessions.filter(
28156
- (s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")
28157
- );
28158
- return { response: { type: "session:list", sessions: filtered } };
28159
- }
28160
27082
  return { response: { type: "session:list", ...response } };
28161
27083
  };
28162
- const get = async (frame, _client, ctx) => {
27084
+ const get = async (frame) => {
28163
27085
  const args = SessionIdArgs.parse(frame);
28164
- ensureSessionAccess(ctx, args.sessionId, "read");
28165
27086
  const { response } = manager.get(args);
28166
27087
  return { response: { type: "session:info", ...response } };
28167
27088
  };
28168
- const update = async (frame, _client, ctx) => {
27089
+ const update = async (frame) => {
28169
27090
  const args = SessionUpdateArgs.parse(frame);
28170
- ensureSessionAccess(ctx, args.sessionId, "send");
28171
27091
  const { response, broadcast } = manager.update(args);
28172
27092
  return { response: { type: "session:info", ...response }, broadcast };
28173
27093
  };
28174
- const del = async (frame, _client, ctx) => {
27094
+ const del = async (frame) => {
28175
27095
  const args = SessionIdArgs.parse(frame);
28176
- ensureSessionAccess(ctx, args.sessionId, "send");
28177
27096
  const { broadcast } = manager.delete(args);
28178
27097
  return {
28179
27098
  response: { type: "session:deleted", sessionId: args.sessionId },
28180
27099
  broadcast
28181
27100
  };
28182
27101
  };
28183
- const send = async (frame, _client, ctx) => {
27102
+ const send = async (frame) => {
28184
27103
  const args = SessionSendArgs.parse(frame);
28185
- ensureSessionAccess(ctx, args.sessionId, "send");
28186
27104
  const { broadcast } = manager.send(args);
28187
27105
  return { response: { type: "session:send", ok: true }, broadcast };
28188
27106
  };
28189
- const stop = async (frame, _client, ctx) => {
27107
+ const stop = async (frame) => {
28190
27108
  const args = SessionIdArgs.parse(frame);
28191
- ensureSessionAccess(ctx, args.sessionId, "send");
28192
27109
  const { broadcast } = await manager.stop(args);
28193
27110
  return { response: { type: "session:stop", ok: true }, broadcast };
28194
27111
  };
28195
- const interrupt = async (frame, _client, ctx) => {
27112
+ const interrupt = async (frame) => {
28196
27113
  const args = SessionIdArgs.parse(frame);
28197
- ensureSessionAccess(ctx, args.sessionId, "send");
28198
27114
  const { broadcast } = await manager.interrupt(args);
28199
27115
  return { response: { type: "session:interrupt", ok: true }, broadcast };
28200
27116
  };
28201
- const rewind = async (frame, _client, ctx) => {
27117
+ const rewind = async (frame) => {
28202
27118
  const args = SessionRewindArgs.parse(frame);
28203
- ensureSessionAccess(ctx, args.sessionId, "send");
28204
27119
  const { response, broadcast } = await manager.rewind(args);
28205
27120
  return { response: { type: "session:rewind", ...response }, broadcast };
28206
27121
  };
28207
- const rewindDiff = async (frame, _client, ctx) => {
27122
+ const rewindDiff = async (frame) => {
28208
27123
  const args = SessionRewindDiffArgs.parse(frame);
28209
- ensureSessionAccess(ctx, args.sessionId, "read");
28210
27124
  const { response } = await manager.rewindDiff(args);
28211
27125
  return { response: { type: "session:rewind-diff", ...response } };
28212
27126
  };
28213
- const rewindableMessageIds = async (frame, _client, ctx) => {
27127
+ const rewindableMessageIds = async (frame) => {
28214
27128
  const args = SessionRewindableMessageIdsArgs.parse(frame);
28215
- ensureSessionAccess(ctx, args.sessionId, "read");
28216
27129
  const { response } = manager.rewindableMessageIds(args);
28217
27130
  return {
28218
27131
  response: { type: "session:rewindable-message-ids", ...response }
@@ -28223,22 +27136,19 @@ function buildSessionHandlers(deps) {
28223
27136
  const result = forkSession(args);
28224
27137
  return { response: { type: "session:fork", ...result } };
28225
27138
  };
28226
- const newSession = async (frame, _client, ctx) => {
27139
+ const newSession = async (frame) => {
28227
27140
  const args = SessionIdArgs.parse(frame);
28228
- ensureSessionAccess(ctx, args.sessionId, "send");
28229
27141
  observer.stop(args.sessionId);
28230
27142
  const { response, broadcast } = manager.newSession(args);
28231
27143
  return { response: { type: "session:info", ...response }, broadcast };
28232
27144
  };
28233
- const resume = async (frame, _client, ctx) => {
27145
+ const resume = async (frame) => {
28234
27146
  const args = SessionResumeArgs.parse(frame);
28235
- ensureSessionAccess(ctx, args.sessionId, "send");
28236
27147
  const { response, broadcast } = manager.resume(args);
28237
27148
  return { response: { type: "session:info", ...response }, broadcast };
28238
27149
  };
28239
- const observe = async (frame, _client, ctx) => {
27150
+ const observe = async (frame) => {
28240
27151
  const args = SessionObserveArgs.parse(frame);
28241
- ensureSessionAccess(ctx, args.sessionId, "read");
28242
27152
  const { response: file } = manager.get({ sessionId: args.sessionId });
28243
27153
  const sessionFile = file;
28244
27154
  manager.ensureSession(sessionFile);
@@ -28252,17 +27162,14 @@ function buildSessionHandlers(deps) {
28252
27162
  });
28253
27163
  return { response: { type: "session:observe", ok: true } };
28254
27164
  };
28255
- const events = async (frame, _client, ctx) => {
27165
+ const events = async (frame) => {
28256
27166
  const args = SessionEventsArgs.parse(frame);
28257
- ensureSessionAccess(ctx, args.sessionId, "read");
28258
27167
  const { response } = manager.getEvents(args);
28259
27168
  return { response: { type: "session:events", ...response } };
28260
27169
  };
28261
- const subscribe = async (frame, client, ctx) => {
28262
- if (typeof frame.sessionId === "string") {
28263
- ensureSessionAccess(ctx, frame.sessionId, "read");
27170
+ const subscribe = async (frame, client) => {
27171
+ if (typeof frame.sessionId === "string")
28264
27172
  addSubscription(client, frame.sessionId);
28265
- }
28266
27173
  return {
28267
27174
  response: { type: "subscribed", sessionId: frame.sessionId }
28268
27175
  };
@@ -28274,9 +27181,8 @@ function buildSessionHandlers(deps) {
28274
27181
  response: { type: "unsubscribed", sessionId: frame.sessionId }
28275
27182
  };
28276
27183
  };
28277
- const pin = async (frame, _client, ctx) => {
27184
+ const pin = async (frame) => {
28278
27185
  const args = SessionPinArgs.parse(frame);
28279
- ensureSessionAccess(ctx, args.sessionId, "send");
28280
27186
  const { response, broadcast } = manager.pin(args);
28281
27187
  return { response: { type: "session:info", ...response }, broadcast };
28282
27188
  };
@@ -28285,15 +27191,13 @@ function buildSessionHandlers(deps) {
28285
27191
  const { response, broadcast } = manager.reorderPins(args);
28286
27192
  return { response: { type: "session:reorderPins", ...response }, broadcast };
28287
27193
  };
28288
- const answerQuestion = async (frame, _client, ctx) => {
27194
+ const answerQuestion = async (frame) => {
28289
27195
  const args = AnswerQuestionArgs.parse(frame);
28290
- ensureSessionAccess(ctx, args.sessionId, "send");
28291
27196
  const { response, broadcast } = manager.answerQuestion(args);
28292
27197
  return { response: { type: "session:answerQuestion", ...response }, broadcast };
28293
27198
  };
28294
- const cancelQuestion = async (frame, _client, ctx) => {
27199
+ const cancelQuestion = async (frame) => {
28295
27200
  const args = CancelQuestionArgs.parse(frame);
28296
- ensureSessionAccess(ctx, args.sessionId, "send");
28297
27201
  const { response, broadcast } = await manager.cancelQuestion(args);
28298
27202
  return { response: { type: "session:cancelQuestion", ...response }, broadcast };
28299
27203
  };
@@ -28591,282 +27495,6 @@ function buildCapabilitiesHandlers(deps) {
28591
27495
  };
28592
27496
  }
28593
27497
 
28594
- // src/handlers/capability.ts
28595
- init_zod();
28596
- init_protocol();
28597
- var DeleteArgsSchema = external_exports.object({
28598
- capabilityId: external_exports.string().min(1)
28599
- }).strict();
28600
- function buildCapabilityHandlers(deps) {
28601
- const { manager, getShareBaseUrl, personStore } = deps;
28602
- const issue = async (frame) => {
28603
- const { type: _type, requestId: _requestId, ...rest } = frame;
28604
- const args = CapabilityIssueArgsSchema.parse(rest);
28605
- if (!personStore.get(args.personId)) {
28606
- throw new ClawdError(
28607
- ERROR_CODES.VALIDATION_ERROR,
28608
- `Person not found: ${args.personId}`
28609
- );
28610
- }
28611
- const existing = manager.findByPerson(args.personId);
28612
- if (existing) {
28613
- const updated = manager.addGrantsToPerson(args.personId, args.grants);
28614
- if (!updated) {
28615
- throw new ClawdError(ERROR_CODES.INTERNAL, "addGrantsToPerson returned null");
28616
- }
28617
- return {
28618
- response: {
28619
- type: "capability:issued",
28620
- token: "__merged__",
28621
- shareUrl: "",
28622
- capability: stripSecretHash(updated)
28623
- }
28624
- };
28625
- }
28626
- const { personId, ...managerArgs } = args;
28627
- const { token, capability } = manager.issue({
28628
- ...managerArgs,
28629
- personId
28630
- });
28631
- const base = getShareBaseUrl().replace(/\/$/, "");
28632
- const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
28633
- return {
28634
- response: {
28635
- type: "capability:issued",
28636
- token,
28637
- shareUrl,
28638
- capability: stripSecretHash(capability)
28639
- }
28640
- };
28641
- };
28642
- const list = async () => {
28643
- return {
28644
- response: {
28645
- type: "capability:list",
28646
- capabilities: manager.list().map(stripSecretHash)
28647
- }
28648
- };
28649
- };
28650
- const del = async (frame) => {
28651
- const { type: _type, requestId: _requestId, ...rest } = frame;
28652
- const args = DeleteArgsSchema.parse(rest);
28653
- const result = manager.delete(args.capabilityId);
28654
- if (!result) {
28655
- throw new ClawdError(
28656
- ERROR_CODES.VALIDATION_ERROR,
28657
- `capability not found: ${args.capabilityId}`
28658
- );
28659
- }
28660
- return {
28661
- response: {
28662
- type: "capability:deleted",
28663
- capabilityId: args.capabilityId
28664
- }
28665
- };
28666
- };
28667
- return {
28668
- "capability:issue": issue,
28669
- "capability:list": list,
28670
- "capability:delete": del
28671
- };
28672
- }
28673
-
28674
- // src/handlers/inbox.ts
28675
- init_protocol();
28676
- function assertChannelAccess(ctx, capabilityId) {
28677
- if (ctx.principal.kind === "owner") return;
28678
- if (ctx.capabilityId === capabilityId) return;
28679
- throw new ClawdError(
28680
- ERROR_CODES.UNAUTHORIZED,
28681
- `inbox: guest can only access own channel (capabilityId=${capabilityId}, ctx.capabilityId=${ctx.capabilityId ?? "none"})`
28682
- );
28683
- }
28684
- function buildInboxHandlers(deps) {
28685
- const { manager, personStore, capabilityManager } = deps;
28686
- const postMessage = async (frame, _client, ctx) => {
28687
- const { type: _t, requestId: _r, ...rest } = frame;
28688
- const args = InboxPostMessageArgsSchema.parse(rest);
28689
- if (!ctx) {
28690
- throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
28691
- }
28692
- assertChannelAccess(ctx, args.capabilityId);
28693
- if (ctx.principal.kind === "guest" && ctx.capabilityId) {
28694
- const cap = capabilityManager.findById(ctx.capabilityId);
28695
- if (cap) {
28696
- const person = personStore.get(cap.personId);
28697
- if (person && !person.dmEnabled) {
28698
- throw new ClawdError(
28699
- ERROR_CODES.VALIDATION_ERROR,
28700
- `DM_BLOCKED: Person ${person.displayName} has DM disabled`
28701
- );
28702
- }
28703
- }
28704
- }
28705
- const message = manager.postMessage({
28706
- capabilityId: args.capabilityId,
28707
- senderPrincipalId: ctx.principal.id,
28708
- text: args.text
28709
- });
28710
- return {
28711
- response: { type: "inbox:postMessage:ok", message }
28712
- };
28713
- };
28714
- const list = async (frame, _client, ctx) => {
28715
- const { type: _t, requestId: _r, ...rest } = frame;
28716
- const args = InboxListArgsSchema.parse(rest);
28717
- if (!ctx) {
28718
- throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
28719
- }
28720
- assertChannelAccess(ctx, args.capabilityId);
28721
- const messages = manager.list(args.capabilityId, args.sinceCreatedAt);
28722
- return {
28723
- response: { type: "inbox:list:ok", capabilityId: args.capabilityId, messages }
28724
- };
28725
- };
28726
- const markRead = async (frame, _client, ctx) => {
28727
- const { type: _t, requestId: _r, ...rest } = frame;
28728
- const args = InboxMarkReadArgsSchema.parse(rest);
28729
- if (!ctx) {
28730
- throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
28731
- }
28732
- assertChannelAccess(ctx, args.capabilityId);
28733
- const updated = manager.markRead({
28734
- capabilityId: args.capabilityId,
28735
- principalId: ctx.principal.id,
28736
- upToCreatedAt: args.upToCreatedAt
28737
- });
28738
- return {
28739
- response: {
28740
- type: "inbox:markRead:ok",
28741
- capabilityId: args.capabilityId,
28742
- upToCreatedAt: args.upToCreatedAt,
28743
- updated
28744
- }
28745
- };
28746
- };
28747
- return {
28748
- "inbox:postMessage": postMessage,
28749
- "inbox:list": list,
28750
- "inbox:markRead": markRead
28751
- };
28752
- }
28753
-
28754
- // src/handlers/remote-persona.ts
28755
- init_protocol();
28756
- function buildRemotePersonaHandlers(deps) {
28757
- const { store, personStore } = deps;
28758
- const now = deps.now ?? Date.now;
28759
- const add = async (frame) => {
28760
- const { type: _t, requestId: _r, ...rest } = frame;
28761
- const args = RemotePersonaAddArgsSchema.parse(rest);
28762
- if (!personStore.get(args.personId)) {
28763
- throw new ClawdError(
28764
- ERROR_CODES.VALIDATION_ERROR,
28765
- `Person not found: ${args.personId}`
28766
- );
28767
- }
28768
- const rp = {
28769
- ...args,
28770
- addedAt: now()
28771
- };
28772
- try {
28773
- store.add(rp);
28774
- } catch (err) {
28775
- if (err.message?.includes("alias already exists")) {
28776
- throw new ClawdError(
28777
- ERROR_CODES.VALIDATION_ERROR,
28778
- `remote-persona alias already exists: ${args.alias}`
28779
- );
28780
- }
28781
- throw err;
28782
- }
28783
- return {
28784
- response: { type: "remote-persona:add:ok", remotePersona: stripRemotePersonaSecret(rp) }
28785
- };
28786
- };
28787
- const list = async (_frame, _client, ctx) => {
28788
- const all = store.list();
28789
- const projected = ctx?.principal.kind === "owner" ? all : all.map(stripRemotePersonaSecret);
28790
- return {
28791
- response: {
28792
- type: "remote-persona:list",
28793
- remotePersonas: projected
28794
- }
28795
- };
28796
- };
28797
- const remove = async (frame) => {
28798
- const { type: _t, requestId: _r, ...rest } = frame;
28799
- const args = RemotePersonaRemoveArgsSchema.parse(rest);
28800
- const removed = store.remove(args.alias);
28801
- return {
28802
- response: { type: "remote-persona:remove:ok", alias: args.alias, removed }
28803
- };
28804
- };
28805
- return {
28806
- "remote-persona:add": add,
28807
- "remote-persona:list": list,
28808
- "remote-persona:remove": remove
28809
- };
28810
- }
28811
-
28812
- // src/handlers/whoami.ts
28813
- init_protocol();
28814
- function buildWhoamiHandler(deps) {
28815
- return async (_frame, _client, ctx) => {
28816
- if (!ctx) {
28817
- throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
28818
- }
28819
- const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
28820
- let capability;
28821
- if (ctx.principal.kind === "owner") {
28822
- capability = {
28823
- id: deps.ownerPrincipalId,
28824
- displayName: deps.ownerDisplayName,
28825
- grants: ctx.grants,
28826
- issuedAt: 0,
28827
- usedCount: 0,
28828
- // owner self cap 没有真实 Person 主人;用 ownerPrincipalId 自指,UI 不会反查这条
28829
- personId: deps.ownerPrincipalId
28830
- };
28831
- } else {
28832
- if (!ctx.capabilityId) {
28833
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
28834
- }
28835
- const cap = deps.capabilityManager.findById(ctx.capabilityId);
28836
- if (!cap) {
28837
- throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
28838
- }
28839
- capability = stripSecretHash(cap);
28840
- }
28841
- const grantedPersonas = [];
28842
- const isGuest = ctx.principal.kind === "guest";
28843
- const hasWildcard = capability.grants.some((g2) => g2.resource.type === "*");
28844
- if (hasWildcard) {
28845
- for (const id of deps.personaStore.list()) {
28846
- const file = deps.personaStore.read(id);
28847
- if (!file) continue;
28848
- if (isGuest && !file.public) continue;
28849
- grantedPersonas.push({ id: file.personaId, displayName: file.label });
28850
- }
28851
- } else {
28852
- for (const g2 of capability.grants) {
28853
- if (g2.resource.type !== "persona") continue;
28854
- const file = deps.personaStore.read(g2.resource.id);
28855
- if (!file) continue;
28856
- grantedPersonas.push({ id: file.personaId, displayName: file.label });
28857
- }
28858
- }
28859
- return {
28860
- response: {
28861
- type: "whoami:ok",
28862
- owner,
28863
- capability,
28864
- grantedPersonas
28865
- }
28866
- };
28867
- };
28868
- }
28869
-
28870
27498
  // src/handlers/meta.ts
28871
27499
  var import_node_os13 = __toESM(require("os"), 1);
28872
27500
  init_protocol();
@@ -28943,6 +27571,7 @@ function buildPersonaHandlers(deps) {
28943
27571
  }
28944
27572
  const personality = personaManager.readPersonality(args.personaId) ?? "";
28945
27573
  const skills = personaManager.listSkills(args.personaId);
27574
+ const plugins = personaManager.listEnabledPlugins(args.personaId);
28946
27575
  const sandboxSettings = personaManager.readSandboxSettings(args.personaId);
28947
27576
  return {
28948
27577
  response: {
@@ -28950,6 +27579,7 @@ function buildPersonaHandlers(deps) {
28950
27579
  ...persona,
28951
27580
  personality,
28952
27581
  skills,
27582
+ plugins,
28953
27583
  sandboxSettings
28954
27584
  }
28955
27585
  };
@@ -28968,129 +27598,31 @@ function buildPersonaHandlers(deps) {
28968
27598
  response: { type: "persona:deleted", personaId: args.personaId }
28969
27599
  };
28970
27600
  };
27601
+ const issueToken = async (frame) => {
27602
+ const args = PersonaIssueTokenArgsSchema.parse(frame);
27603
+ const { token, persona } = personaManager.issueToken(args.personaId, args.label);
27604
+ return {
27605
+ response: { type: "persona:tokenIssued", token, persona }
27606
+ };
27607
+ };
27608
+ const revokeToken = async (frame) => {
27609
+ const args = PersonaRevokeTokenArgsSchema.parse(frame);
27610
+ const persona = personaManager.revokeToken(args.personaId, args.token);
27611
+ return { response: { type: "persona:info", ...persona } };
27612
+ };
28971
27613
  return {
28972
27614
  "persona:create": create,
28973
27615
  "persona:list": list,
28974
27616
  "persona:get": get,
28975
27617
  "persona:update": update,
28976
- "persona:delete": del
27618
+ "persona:delete": del,
27619
+ "persona:issueToken": issueToken,
27620
+ "persona:revokeToken": revokeToken
28977
27621
  };
28978
27622
  }
28979
27623
 
28980
- // src/handlers/person.ts
28981
- var import_node_crypto9 = require("crypto");
28982
- init_protocol();
28983
-
28984
- // src/person/cascade.ts
28985
- var EMPTY_RESULT = {
28986
- deletedCapabilityIds: [],
28987
- removedRemoteAliases: [],
28988
- deletedInboxEvents: 0
28989
- };
28990
- function cascadeDeletePerson(personId, deps) {
28991
- if (!deps.personStore.get(personId)) return { ...EMPTY_RESULT };
28992
- const allCaps = deps.capabilityManager.list();
28993
- const capsToDelete = allCaps.filter((c) => c.personId === personId);
28994
- let deletedInboxEvents = 0;
28995
- for (const c of capsToDelete) {
28996
- deletedInboxEvents += deps.inboxStore.list(c.id).length;
28997
- }
28998
- const deletedCapabilityIds = [];
28999
- for (const c of capsToDelete) {
29000
- if (deps.capabilityManager.delete(c.id) !== null) {
29001
- deletedCapabilityIds.push(c.id);
29002
- }
29003
- }
29004
- const allRemotes = deps.remotePersonaStore.list();
29005
- const remotesToRemove = allRemotes.filter((r) => r.personId === personId);
29006
- const removedRemoteAliases = [];
29007
- for (const r of remotesToRemove) {
29008
- if (deps.remotePersonaStore.remove(r.alias)) {
29009
- removedRemoteAliases.push(r.alias);
29010
- }
29011
- }
29012
- deps.personStore.remove(personId);
29013
- return { deletedCapabilityIds, removedRemoteAliases, deletedInboxEvents };
29014
- }
29015
-
29016
- // src/handlers/person.ts
29017
- function buildPersonHandlers(deps) {
29018
- const now = deps.now ?? Date.now;
29019
- const genId = deps.genId ?? defaultGenId2;
29020
- const list = async () => {
29021
- const persons = deps.personStore.list();
29022
- const allCaps = deps.capabilityManager.list();
29023
- const allRemotes = deps.remotePersonaStore.list();
29024
- const out = persons.map((p2) => ({
29025
- ...p2,
29026
- linkedCapabilityIds: allCaps.filter((c) => c.personId === p2.id).map((c) => c.id),
29027
- linkedRemoteAliases: allRemotes.filter((r) => r.personId === p2.id).map((r) => r.alias)
29028
- }));
29029
- return { response: { type: "person:list:ok", persons: out } };
29030
- };
29031
- const create = async (frame) => {
29032
- const { type: _t, requestId: _r, ...rest } = frame;
29033
- const args = PersonCreateArgsSchema.parse(rest);
29034
- const t = now();
29035
- const person = {
29036
- id: genId(),
29037
- displayName: args.displayName,
29038
- ...args.notes !== void 0 ? { notes: args.notes } : {},
29039
- dmEnabled: args.dmEnabled ?? true,
29040
- createdAt: t,
29041
- updatedAt: t
29042
- };
29043
- deps.personStore.add(person);
29044
- return { response: { type: "person:create:ok", person } };
29045
- };
29046
- const update = async (frame) => {
29047
- const { type: _t, requestId: _r, ...rest } = frame;
29048
- const args = PersonUpdateArgsSchema.parse(rest);
29049
- const existing = deps.personStore.get(args.id);
29050
- if (!existing) {
29051
- throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `Person not found: ${args.id}`);
29052
- }
29053
- const t = now();
29054
- deps.personStore.update(
29055
- args.id,
29056
- {
29057
- ...args.displayName !== void 0 ? { displayName: args.displayName } : {},
29058
- ...args.notes !== void 0 ? { notes: args.notes } : {},
29059
- ...args.dmEnabled !== void 0 ? { dmEnabled: args.dmEnabled } : {}
29060
- },
29061
- t
29062
- );
29063
- const next = deps.personStore.get(args.id);
29064
- return { response: { type: "person:update:ok", person: next } };
29065
- };
29066
- const del = async (frame) => {
29067
- const { type: _t, requestId: _r, ...rest } = frame;
29068
- const args = PersonDeleteArgsSchema.parse(rest);
29069
- if (!deps.personStore.get(args.id)) {
29070
- throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, `Person not found: ${args.id}`);
29071
- }
29072
- const result = cascadeDeletePerson(args.id, deps);
29073
- return {
29074
- response: {
29075
- type: "person:delete:ok",
29076
- deletedCapabilityIds: result.deletedCapabilityIds,
29077
- removedRemoteAliases: result.removedRemoteAliases,
29078
- deletedInboxEvents: result.deletedInboxEvents
29079
- }
29080
- };
29081
- };
29082
- return {
29083
- "person:list": list,
29084
- "person:create": create,
29085
- "person:update": update,
29086
- "person:delete": del
29087
- };
29088
- }
29089
- function defaultGenId2() {
29090
- return `p_${(0, import_node_crypto9.randomUUID)()}`;
29091
- }
29092
-
29093
27624
  // src/handlers/attachment.ts
27625
+ var import_node_path26 = __toESM(require("path"), 1);
29094
27626
  init_protocol();
29095
27627
  init_protocol();
29096
27628
  var DEFAULT_TTL_SECONDS = 24 * 3600;
@@ -29115,8 +27647,41 @@ function buildAttachmentHandlers(deps) {
29115
27647
  "httpBaseUrl unavailable (daemon HTTP not ready)"
29116
27648
  );
29117
27649
  }
27650
+ if (!deps.sessionStore || !deps.getSessionScope || !deps.groupFileStore) {
27651
+ throw new ClawdError(
27652
+ ERROR_CODES.METHOD_NOT_IMPLEMENTED,
27653
+ "signUrl requires session/group stores"
27654
+ );
27655
+ }
27656
+ const sessionFile = deps.sessionStore.read(args.sessionId);
27657
+ if (!sessionFile) {
27658
+ throw new ClawdError(
27659
+ ERROR_CODES.VALIDATION_ERROR,
27660
+ `session ${args.sessionId} not found`
27661
+ );
27662
+ }
27663
+ const scope = deps.getSessionScope(args.sessionId);
27664
+ if (!scope) {
27665
+ throw new ClawdError(
27666
+ ERROR_CODES.VALIDATION_ERROR,
27667
+ `session ${args.sessionId} scope unresolved`
27668
+ );
27669
+ }
27670
+ const cwdAbs = import_node_path26.default.resolve(sessionFile.cwd);
27671
+ const candidateAbs = import_node_path26.default.isAbsolute(args.relPath) ? import_node_path26.default.resolve(args.relPath) : import_node_path26.default.resolve(cwdAbs, args.relPath);
27672
+ const entries = deps.groupFileStore.list(scope, args.sessionId);
27673
+ const entry = entries.find((e) => {
27674
+ const storedAbs = import_node_path26.default.isAbsolute(e.relPath) ? import_node_path26.default.resolve(e.relPath) : import_node_path26.default.resolve(cwdAbs, e.relPath);
27675
+ return storedAbs === candidateAbs && !e.stale;
27676
+ });
27677
+ if (!entry) {
27678
+ throw new ClawdError(
27679
+ ERROR_CODES.VALIDATION_ERROR,
27680
+ `relPath not in session group files or stale: ${args.relPath}`
27681
+ );
27682
+ }
29118
27683
  const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
29119
- const parts = signUrlParts(secret, args.absPath, ttl);
27684
+ const parts = signUrlParts(secret, candidateAbs, ttl);
29120
27685
  const url = buildSignedFileUrl(httpBaseUrl, parts);
29121
27686
  return {
29122
27687
  response: {
@@ -29183,11 +27748,23 @@ function buildAttachmentHandlers(deps) {
29183
27748
  const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
29184
27749
  return { response: { type: "attachment.groupList", entries } };
29185
27750
  };
27751
+ const groupListPersona = async (frame) => {
27752
+ if (!deps.groupFileStore) {
27753
+ throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
27754
+ }
27755
+ const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
27756
+ if (!parsed.success) {
27757
+ throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
27758
+ }
27759
+ const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
27760
+ return { response: { type: "attachment.groupListPersona", perSession } };
27761
+ };
29186
27762
  return {
29187
27763
  "attachment.signUrl": signUrl,
29188
27764
  "attachment.groupAdd": groupAdd,
29189
27765
  "attachment.groupRemove": groupRemove,
29190
- "attachment.groupList": groupList
27766
+ "attachment.groupList": groupList,
27767
+ "attachment.groupListPersona": groupListPersona
29191
27768
  };
29192
27769
  }
29193
27770
 
@@ -29205,143 +27782,15 @@ function buildMethodHandlers(deps) {
29205
27782
  personaManager: deps.personaManager,
29206
27783
  personaRegistry: deps.personaRegistry
29207
27784
  }),
29208
- ...buildCapabilityHandlers({
29209
- manager: deps.capabilityManager,
29210
- getShareBaseUrl: deps.getShareBaseUrl,
29211
- personStore: deps.personStore
29212
- }),
29213
- ...buildInboxHandlers({
29214
- manager: deps.inboxManager,
29215
- personStore: deps.personStore,
29216
- capabilityManager: deps.capabilityManager
29217
- }),
29218
- ...buildRemotePersonaHandlers({
29219
- store: deps.remotePersonaStore,
29220
- personStore: deps.personStore
29221
- }),
29222
- ...buildPersonHandlers({
29223
- personStore: deps.personStore,
29224
- capabilityManager: deps.capabilityManager,
29225
- remotePersonaStore: deps.remotePersonaStore,
29226
- inboxStore: deps.inboxStore
29227
- }),
29228
- whoami: buildWhoamiHandler({
29229
- ownerDisplayName: deps.ownerDisplayName,
29230
- ownerPrincipalId: deps.ownerPrincipalId,
29231
- personaStore: deps.personaStore,
29232
- capabilityManager: deps.capabilityManager
29233
- }),
29234
27785
  ...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
29235
27786
  };
29236
27787
  }
29237
27788
 
29238
- // src/handlers/method-grants.ts
29239
- var ADMIN_ANY = {
29240
- kind: "fixed",
29241
- resource: { type: "*" },
29242
- action: "admin"
29243
- };
29244
- var CAPABILITY_SCOPED = { kind: "capability-scoped" };
29245
- var METHOD_GRANT_MAP = {
29246
- // ---- public(meta-only,guest 也能调) ----
29247
- "info": { kind: "public" },
29248
- "ping": { kind: "public" },
29249
- // v2 Phase 6: whoami 是身份反查,任何 authed connection (owner / guest) 都能调;
29250
- // handler 内部按 ctx.principal.kind 决定返回 "合成 owner self capability" 还是
29251
- // "查 capabilityManager 拿 caller 的 guest capability"。
29252
- "whoami": { kind: "public" },
29253
- // ---- capability platform(admin-only,本 PR 新增) ----
29254
- "capability:issue": ADMIN_ANY,
29255
- "capability:list": ADMIN_ANY,
29256
- "capability:delete": ADMIN_ANY,
29257
- // ---- inbox: channel-based IM (capability platform v3) ----
29258
- // 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
29259
- // guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
29260
- "inbox:list": { kind: "public" },
29261
- "inbox:markRead": { kind: "public" },
29262
- "inbox:postMessage": { kind: "public" },
29263
- // Phase 4 Task 4.3: 远程 persona 仅 owner 管理 (admin-only)
29264
- "remote-persona:add": ADMIN_ANY,
29265
- "remote-persona:list": ADMIN_ANY,
29266
- "remote-persona:remove": ADMIN_ANY,
29267
- // Person identity Phase 1: 联系人本质是 owner 视角的 People 列表,仅 owner 管理。
29268
- "person:list": ADMIN_ANY,
29269
- "person:create": ADMIN_ANY,
29270
- "person:update": ADMIN_ANY,
29271
- "person:delete": ADMIN_ANY,
29272
- // ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
29273
- // dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
29274
- // owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
29275
- "session:create": CAPABILITY_SCOPED,
29276
- "session:list": CAPABILITY_SCOPED,
29277
- "session:get": CAPABILITY_SCOPED,
29278
- "session:update": CAPABILITY_SCOPED,
29279
- "session:delete": CAPABILITY_SCOPED,
29280
- "session:send": CAPABILITY_SCOPED,
29281
- "session:stop": CAPABILITY_SCOPED,
29282
- "session:interrupt": CAPABILITY_SCOPED,
29283
- "session:rewind": CAPABILITY_SCOPED,
29284
- "session:rewind-diff": CAPABILITY_SCOPED,
29285
- "session:rewindable-message-ids": CAPABILITY_SCOPED,
29286
- "session:fork": CAPABILITY_SCOPED,
29287
- "session:new": CAPABILITY_SCOPED,
29288
- "session:resume": CAPABILITY_SCOPED,
29289
- "session:observe": CAPABILITY_SCOPED,
29290
- "session:events": CAPABILITY_SCOPED,
29291
- "session:subscribe": CAPABILITY_SCOPED,
29292
- "session:unsubscribe": CAPABILITY_SCOPED,
29293
- "session:pin": CAPABILITY_SCOPED,
29294
- "session:reorderPins": ADMIN_ANY,
29295
- // owner 全局操作,无 personaId 维度
29296
- "permission:respond": CAPABILITY_SCOPED,
29297
- "session:answerQuestion": CAPABILITY_SCOPED,
29298
- "session:cancelQuestion": CAPABILITY_SCOPED,
29299
- "history:projects": ADMIN_ANY,
29300
- "history:list": ADMIN_ANY,
29301
- "history:read": ADMIN_ANY,
29302
- "history:subagents": ADMIN_ANY,
29303
- "history:subagent-read": ADMIN_ANY,
29304
- "history:recentDirs": ADMIN_ANY,
29305
- "workspace:list": ADMIN_ANY,
29306
- "workspace:read": ADMIN_ANY,
29307
- "skills:list": ADMIN_ANY,
29308
- "agents:list": ADMIN_ANY,
29309
- "git:root": ADMIN_ANY,
29310
- "git:branch": ADMIN_ANY,
29311
- "git:branches": ADMIN_ANY,
29312
- "capabilities:get": ADMIN_ANY,
29313
- "persona:create": ADMIN_ANY,
29314
- "persona:list": ADMIN_ANY,
29315
- "persona:get": ADMIN_ANY,
29316
- "persona:update": ADMIN_ANY,
29317
- "persona:delete": ADMIN_ANY,
29318
- "session:pty:input": ADMIN_ANY,
29319
- "session:pty:resize": ADMIN_ANY,
29320
- // file-sharing attachment.* RPC:dispatcher 用 admin-only 兜底(wire-level 拦),
29321
- // 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
29322
- "attachment.signUrl": ADMIN_ANY,
29323
- "attachment.groupAdd": ADMIN_ANY,
29324
- "attachment.groupRemove": ADMIN_ANY,
29325
- "attachment.groupList": ADMIN_ANY
29326
- };
29327
- function computeGrantForFrame(method, frame) {
29328
- const rule = METHOD_GRANT_MAP[method];
29329
- if (!rule) return { kind: "public" };
29330
- if (rule.kind === "public") return { kind: "public" };
29331
- if (rule.kind === "capability-scoped") return { kind: "public" };
29332
- if (rule.kind === "fixed") {
29333
- return { kind: "check", resource: rule.resource, action: rule.action };
29334
- }
29335
- const picked = rule.pick(frame);
29336
- if (!picked) return { kind: "public" };
29337
- return { kind: "check", resource: picked.resource, action: picked.action };
29338
- }
29339
-
29340
27789
  // src/index.ts
29341
27790
  async function startDaemon(config) {
29342
27791
  const logger = createLogger({
29343
27792
  level: config.logLevel,
29344
- file: import_node_path26.default.join(config.dataDir, "clawd.log")
27793
+ file: import_node_path27.default.join(config.dataDir, "clawd.log")
29345
27794
  });
29346
27795
  logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
29347
27796
  const stateMgr = new StateFileManager({ dataDir: config.dataDir });
@@ -29352,103 +27801,45 @@ async function startDaemon(config) {
29352
27801
  if (pre.status === "stale") {
29353
27802
  logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
29354
27803
  }
29355
- const auth = loadOrCreateAuth({ dataDir: config.dataDir });
29356
27804
  let resolvedAuthToken = null;
27805
+ let authFile = null;
29357
27806
  if (config.authToken && config.authToken.trim()) {
29358
27807
  resolvedAuthToken = config.authToken.trim();
29359
27808
  } else if (config.tunnel) {
29360
- resolvedAuthToken = auth.token;
27809
+ authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
27810
+ resolvedAuthToken = authFile.token;
29361
27811
  }
29362
- const ownerPrincipalId = auth.ownerPrincipalId;
29363
- const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
29364
27812
  const authMode = resolvedAuthToken == null ? "none" : "first-message";
29365
- const inboxStore = new InboxStore(config.dataDir);
29366
- const inboxManager = new InboxManager(inboxStore, (capabilityId, frame) => {
29367
- wsServer?.broadcastToCapabilityChannel(capabilityId, frame);
29368
- });
29369
- const capabilityStore = new CapabilityStore(config.dataDir);
29370
- const capabilityRegistry = new CapabilityRegistry(capabilityStore);
29371
- const capabilityManager = new CapabilityManager(capabilityRegistry, {
29372
- onIssued: (cap, token) => {
29373
- wsServer?.broadcastToOwners({
29374
- type: "capability:tokenIssued",
29375
- capability: stripSecretHash(cap),
29376
- token
29377
- });
29378
- },
29379
- onDeleted: (cap) => {
29380
- const deletedAt = Date.now();
29381
- wsServer?.broadcastToOwners({
29382
- type: "capability:tokenDeleted",
29383
- capabilityId: cap.id,
29384
- deletedAt
29385
- });
29386
- wsServer?.closeConnectionsByCapability(cap.id);
29387
- const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
29388
- if (cleanup.removed.length > 0) {
29389
- logger.info("capability delete cascade: guest sessions removed", {
29390
- capabilityId: cap.id,
29391
- removedDirs: cleanup.removed
29392
- });
29393
- }
29394
- inboxManager.removeChannel(cap.id);
29395
- }
29396
- });
29397
- const remotePersonaStore = new RemotePersonaStore(config.dataDir);
29398
- const personStore = new PersonStore(config.dataDir);
29399
27813
  let wsServer = null;
29400
27814
  const authGate = authMode === "first-message" ? new AuthGate({
29401
27815
  shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
29402
- // Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
29403
- // owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
29404
27816
  expectedToken: resolvedAuthToken,
29405
- authenticate: (t, _selfPrincipalId) => {
29406
- return authenticate(t, {
29407
- isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
29408
- ownerPrincipalId,
29409
- ownerDisplayName,
29410
- capabilityRegistry
29411
- });
29412
- },
29413
- onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
29414
- buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
29415
27817
  closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
29416
27818
  sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
29417
27819
  }) : null;
29418
27820
  resetRegistry();
29419
- const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
29420
- if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
29421
- logger.info("sessions migration applied", {
29422
- movedBare: migrateResult.movedBare,
29423
- movedVmOwner: migrateResult.movedVmOwner,
29424
- archivedListener: migrateResult.archivedListener
29425
- });
29426
- }
29427
- const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
29428
- const store = sessionStoreFactory.forBare();
27821
+ const store = new SessionStore({ dataDir: config.dataDir });
29429
27822
  const workspace = new WorkspaceBrowser();
29430
27823
  const skills = new SkillsScanner();
29431
27824
  const agents = new AgentsScanner();
29432
27825
  const history = new ClaudeHistoryReader();
29433
27826
  let transport = null;
29434
- const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
27827
+ const personaStore = new PersonaStore(import_node_path27.default.join(config.dataDir, "personas"));
29435
27828
  const defaultsRoot = findDefaultsRoot();
29436
27829
  if (defaultsRoot) {
29437
27830
  seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
29438
27831
  } else {
29439
27832
  logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
29440
27833
  }
27834
+ const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
29441
27835
  const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
29442
27836
  const manager = new SessionManager({
29443
27837
  store,
29444
- // Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
29445
- // 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
29446
- storeFactory: sessionStoreFactory,
29447
27838
  logger,
29448
27839
  getAdapter,
29449
27840
  historyReader: history,
29450
27841
  dataDir: config.dataDir,
29451
- personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
27842
+ personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
29452
27843
  personaStore,
29453
27844
  ownerDisplayName,
29454
27845
  mode: config.mode,
@@ -29471,7 +27862,7 @@ async function startDaemon(config) {
29471
27862
  // 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
29472
27863
  attachmentGroup: {
29473
27864
  onFileEdit: (input) => {
29474
- const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
27865
+ const absPath = import_node_path27.default.isAbsolute(input.relPath) ? input.relPath : import_node_path27.default.join(input.cwd, input.relPath);
29475
27866
  let size = 0;
29476
27867
  try {
29477
27868
  size = import_node_fs24.default.statSync(absPath).size;
@@ -29570,62 +27961,43 @@ async function startDaemon(config) {
29570
27961
  httpToken: resolvedAuthToken,
29571
27962
  // file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
29572
27963
  // 根据 sessionId 反查 scope 写盘。
27964
+ //
27965
+ // sessionStore / getSessionScope 都走 manager 的跨 scope 公开 API(findOwnedSession /
27966
+ // findOwnedSessionScope)—— 否则 default-only SessionStore 找不到 persona owner-mode
27967
+ // session,attachment 整套 RPC 对 persona session 都会报 "session not found"。
27968
+ // 详见 fix(daemon) #703:file-sharing v1 预存 wiring bug,#701 把 desktop 默认 --tunnel
27969
+ // 后首次让 desktop 用户用上 file-sharing 才被暴露。
29573
27970
  attachment: {
29574
27971
  groupFileStore,
27972
+ sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
29575
27973
  getHttpBaseUrl,
29576
- // HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
29577
- // noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
29578
- getSignSecret: () => resolvedAuthToken ?? "",
29579
- // group RPC:根据 sessionId 反查 scopeowner-mode persona session 走
27974
+ // HMAC sign secret:~/.clawd/auth.json signSecret 字段(与 WS Bearer token 独立)。
27975
+ // --auth-token CLI 模式 / noAuth 模式 authFile 为 null → handler 自己返 NOT_IMPLEMENTED。
27976
+ getSignSecret: () => authFile?.signSecret ?? "",
27977
+ // group RPC + sign 都用:根据 sessionId 反查 scopeowner-mode persona session 走
29580
27978
  // 'persona/<pid>/owner',default 走 'default'。
29581
- getSessionScope: (sessionId) => {
29582
- const file = store.read(sessionId);
29583
- if (!file) return null;
29584
- if (file.ownerPersonaId) {
29585
- return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
29586
- }
29587
- return { kind: "default" };
29588
- }
29589
- },
29590
- // Task 1.9: capability:issue/list/revoke handler 依赖
29591
- capabilityManager,
29592
- // v2 Phase 5: capability:issue 返回的 shareUrl 用此 base URL 拼接。
29593
- // tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
29594
- getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
29595
- // v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
29596
- ownerDisplayName,
29597
- // owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
29598
- ownerPrincipalId,
29599
- personaStore,
29600
- // capability handler 也用 (capability:issue 走 registry / capability:list)
29601
- capabilityRegistry,
29602
- // capability platform v3 inbox: handler 接 manager (post/list/markRead),
29603
- // cascade 接 store (list 统计 deletedInboxEvents).
29604
- inboxManager,
29605
- inboxStore,
29606
- // Phase 4 Task 4.3: remote-persona:* handler 依赖 (本地存储, v1 不接 outgoing WS)
29607
- remotePersonaStore,
29608
- // Per-People token (2026-05-21): person:* RPC handlers + cap/remote 自带
29609
- // personId 字段(PersonAlias 表已删)
29610
- personStore
27979
+ getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
27980
+ }
29611
27981
  });
29612
27982
  const authResolver = new AuthContextResolver({
29613
- ownerToken: resolvedAuthToken
27983
+ ownerToken: resolvedAuthToken,
27984
+ personaRegistry
29614
27985
  });
29615
27986
  const httpRouter = createHttpRouter({
29616
27987
  authResolver,
29617
27988
  daemonVersion: version,
29618
27989
  logger,
27990
+ personaStore,
27991
+ groupFileStore,
29619
27992
  sessionStore: store,
29620
- // /files HMAC verify 用同一份 owner token secret(与 attachment.signUrl 同源)
29621
- getSignSecret: () => resolvedAuthToken ?? null
27993
+ // /files HMAC verify auth.json signSecret 字段(与 attachment.signUrl 同源)。
27994
+ // --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
27995
+ getSignSecret: () => authFile?.signSecret ?? null
29622
27996
  });
29623
27997
  wsServer = new LocalWsServer({
29624
27998
  host: config.host,
29625
27999
  port: config.port,
29626
28000
  logger,
29627
- // broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
29628
- ownerPrincipalId,
29629
28001
  readyFrameBuilder: (ctx) => buildReadyFrame(
29630
28002
  {
29631
28003
  manager,
@@ -29641,19 +28013,6 @@ async function startDaemon(config) {
29641
28013
  ),
29642
28014
  protocolVersion: PROTOCOL_VERSION,
29643
28015
  authGate: authGate ?? void 0,
29644
- // noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
29645
- // 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
29646
- // capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
29647
- // 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
29648
- tryVerifyCapabilityToken: (token) => {
29649
- const v2 = capabilityRegistry.verifyToken(token);
29650
- if (!v2.ok) return null;
29651
- return {
29652
- principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
29653
- grants: v2.capability.grants,
29654
- capabilityId: v2.capability.id
29655
- };
29656
- },
29657
28016
  // file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
29658
28017
  httpRequestHandler: httpRouter,
29659
28018
  // 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
@@ -29706,18 +28065,7 @@ async function startDaemon(config) {
29706
28065
  const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
29707
28066
  const handler = handlers[type];
29708
28067
  if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
29709
- const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
29710
- const verdict = computeGrantForFrame(type, frame);
29711
- if (verdict.kind === "check") {
29712
- const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
29713
- if (!ok) {
29714
- throw new ClawdError(
29715
- ERROR_CODES.UNAUTHORIZED,
29716
- `principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
29717
- );
29718
- }
29719
- }
29720
- const result = await handler(frame, client, ctx);
28068
+ const result = await handler(frame, client);
29721
28069
  if (requestId && result.response) {
29722
28070
  client.send({ ...result.response, requestId });
29723
28071
  }
@@ -29775,15 +28123,15 @@ async function startDaemon(config) {
29775
28123
  });
29776
28124
  try {
29777
28125
  const r = await tunnelMgr.start({ localPort: config.port });
29778
- stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
28126
+ stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url, tunnelError: void 0 };
29779
28127
  stateMgr.write(stateSnapshot);
29780
28128
  currentTunnelUrl = r.url;
29781
28129
  const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
29782
28130
  const lines = [
29783
28131
  `Tunnel: ${r.url}`,
29784
28132
  ...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
29785
- `Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
29786
- `Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
28133
+ `Frpc config: ${import_node_path27.default.join(config.dataDir, "frpc.toml")}`,
28134
+ `Frpc log: ${import_node_path27.default.join(config.dataDir, "frpc.log")}`
29787
28135
  ];
29788
28136
  const width = Math.max(...lines.map((l) => l.length));
29789
28137
  const bar = "\u2550".repeat(width + 4);
@@ -29796,19 +28144,30 @@ ${bar}
29796
28144
 
29797
28145
  `);
29798
28146
  try {
29799
- const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
28147
+ const connectPath = import_node_path27.default.join(config.dataDir, "connect.txt");
29800
28148
  import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
29801
28149
  } catch {
29802
28150
  }
29803
28151
  } catch (err) {
28152
+ const tunnelError = err?.message ?? String(err);
29804
28153
  try {
29805
28154
  await tunnelMgr.stop();
29806
28155
  } catch {
29807
28156
  }
29808
28157
  tunnelMgr = null;
29809
- stateMgr.delete();
29810
- await wss.stop();
29811
- throw err;
28158
+ stateSnapshot = { ...stateSnapshot, tunnelUrl: void 0, tunnelError };
28159
+ try {
28160
+ stateMgr.write(stateSnapshot);
28161
+ } catch {
28162
+ }
28163
+ wss.broadcastAll({
28164
+ type: "tunnel:unavailable",
28165
+ reason: tunnelError,
28166
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
28167
+ });
28168
+ process.stdout.write(`Tunnel: unavailable (local mode) \u2014 ${tunnelError}
28169
+ `);
28170
+ logger.warn("tunnel unavailable, degraded to local mode", { reason: tunnelError });
29812
28171
  }
29813
28172
  }
29814
28173
  const shutdown = async () => {