@clawos-dev/clawd 0.2.71-beta.138.2a35bba → 0.2.72-beta.139.d7294dd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2385 -763
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -87,15 +87,16 @@ var init_methods = __esm({
|
|
|
87
87
|
"git:branches",
|
|
88
88
|
"capabilities:get",
|
|
89
89
|
// ---- persona:* (persona 管理) ----
|
|
90
|
+
// persona:list 响应 shape 不分流(统一返 PersonaFile[]),但 daemon handler 按
|
|
91
|
+
// ConnectionContext 投影不同子集:
|
|
92
|
+
// owner ctx → 返全量 personas(含 private)
|
|
93
|
+
// guest ctx → persona.public === true ∧ assertGrant(ctx.grants, {persona,p.id}, 'read')
|
|
94
|
+
// method-grants.ts: persona:list 是 CAPABILITY_SCOPED;其它 persona:* 仍 ADMIN_ANY。
|
|
90
95
|
"persona:create",
|
|
91
96
|
"persona:list",
|
|
92
97
|
"persona:get",
|
|
93
98
|
"persona:update",
|
|
94
99
|
"persona:delete",
|
|
95
|
-
// token 颁发 / 吊销:file-sharing HTTP Bearer 鉴权用(PersonaRegistry.findByToken
|
|
96
|
-
// 反查 personaId / label)。owner UI 在 PersonaSettingsDrawer "分享 Token" section 管理。
|
|
97
|
-
"persona:issueToken",
|
|
98
|
-
"persona:revokeToken",
|
|
99
100
|
// ---- session:pty 双向透传(仅 CLAWD_CC_MODE=tui 才生效,对应 session:pty push 帧的上行) ----
|
|
100
101
|
// session:pty:input — UI 把用户键盘字节(base64 UTF-8)发给 daemon,daemon 直写 pty stdin。
|
|
101
102
|
// 高频帧(每次按键 1 帧),不入 reducer / 不广播。response 是 fire-and-forget 风格 ack。
|
|
@@ -106,14 +107,45 @@ var init_methods = __esm({
|
|
|
106
107
|
// ---- attachment.* file-sharing(详见 attachment-schemas.ts) ----
|
|
107
108
|
// 命名警告:这里的 `attachment.*` RPC 与 CC v2.x 上行 `type:"attachment"` 系统行
|
|
108
109
|
// (attachment-skills / attachment-deferred-tools / attachment_memories)是不同概念。
|
|
109
|
-
// 全部管理类 RPC
|
|
110
|
-
// 实际文件传输走 HTTP 路由(GET /files?p=&e=&s
|
|
110
|
+
// 全部管理类 RPC dispatcher grant 限 owner(capability admin-only);
|
|
111
|
+
// 实际文件传输走 HTTP 路由(GET /files?p=&e=&s=,HMAC 签名自包含,不在白名单内)。
|
|
111
112
|
"attachment.signUrl",
|
|
112
113
|
"attachment.groupAdd",
|
|
113
114
|
"attachment.groupRemove",
|
|
114
115
|
"attachment.groupList",
|
|
115
|
-
//
|
|
116
|
-
|
|
116
|
+
// ---- capability:* (capability platform 鉴权底座) ----
|
|
117
|
+
// owner 颁发 / 列出 / 删除给 guest 的 capability。三者均需 admin 权限(METHOD_GRANT_MAP
|
|
118
|
+
// 在 daemon 端固定为 `{ resource: '*', action: 'admin' }`,owner 自动满足)。
|
|
119
|
+
// 颁发后 daemon 推 'capability:tokenIssued' 帧;删除推 'capability:tokenDeleted' 帧。
|
|
120
|
+
// 删除是 hard delete:CapabilityStore 物理移除 + 关该 cap 的所有活跃 ws + rm
|
|
121
|
+
// personas/<pid>/.clawd/sessions/guests/<capId>/ guest sessions 目录。
|
|
122
|
+
"capability:issue",
|
|
123
|
+
"capability:list",
|
|
124
|
+
"capability:delete",
|
|
125
|
+
// bidirectional cap 视角 B 2026-05-23: owner 主动告诉 daemon "我颁的某 cap 给了谁"
|
|
126
|
+
// (UI 在 accept dialog 4 步流程里调; 等价于 recordPeerHello 但不经 ws auth path).
|
|
127
|
+
// 否则 B 端颁回的 cap 因为 A 不连 B → peerOwnerId 永不回写 → useContacts 派生 pending.
|
|
128
|
+
"capability:bindPeer",
|
|
129
|
+
// ---- inbox:* (capability platform Phase 3 跨用户通知 + Phase 4 DM) ----
|
|
130
|
+
// owner 接 guest 的 cross-principal 消息事件 + DM 双向私聊.
|
|
131
|
+
// inbox:list / markRead: admin-only (Phase 3); inbox:postMessage: DM 自带能力,
|
|
132
|
+
// 任何 principal (owner/guest) 可调 (Phase 4).
|
|
133
|
+
"inbox:list",
|
|
134
|
+
"inbox:markRead",
|
|
135
|
+
"inbox:postMessage",
|
|
136
|
+
// ---- received-capability:* (对方颁给我的 cap, 视角 B 双向授权模型, 2026-05-23) ----
|
|
137
|
+
// owner 入口: list / add / remove
|
|
138
|
+
// guest 端: autoAttach (B 接 A 的 token 后通过 cap channel ws 推 cap 给 A)
|
|
139
|
+
// 详见 received-capability.ts JSDoc + spec 2026-05-23-bidirectional-cap-design.md
|
|
140
|
+
"received-capability:list",
|
|
141
|
+
"received-capability:add",
|
|
142
|
+
"received-capability:remove",
|
|
143
|
+
"received-capability:autoAttach",
|
|
144
|
+
// ---- whoami (v2 capability platform) ----
|
|
145
|
+
// 任何 authed connection 都能调;返回 owner 显示名 + 当前 capability wire 形态 +
|
|
146
|
+
// grants 对应的 persona 元数据 (id + displayName)。UI 端 AddRemotePersonaDialog
|
|
147
|
+
// 的 preview 一次性临时 client 用它判定身份和可用 persona。
|
|
148
|
+
"whoami",
|
|
117
149
|
"info",
|
|
118
150
|
"ping"
|
|
119
151
|
];
|
|
@@ -605,8 +637,8 @@ var init_parseUtil = __esm({
|
|
|
605
637
|
init_errors2();
|
|
606
638
|
init_en();
|
|
607
639
|
makeIssue = (params) => {
|
|
608
|
-
const { data, path:
|
|
609
|
-
const fullPath = [...
|
|
640
|
+
const { data, path: path38, errorMaps, issueData } = params;
|
|
641
|
+
const fullPath = [...path38, ...issueData.path || []];
|
|
610
642
|
const fullIssue = {
|
|
611
643
|
...issueData,
|
|
612
644
|
path: fullPath
|
|
@@ -917,11 +949,11 @@ var init_types = __esm({
|
|
|
917
949
|
init_parseUtil();
|
|
918
950
|
init_util();
|
|
919
951
|
ParseInputLazyPath = class {
|
|
920
|
-
constructor(parent, value,
|
|
952
|
+
constructor(parent, value, path38, key) {
|
|
921
953
|
this._cachedPath = [];
|
|
922
954
|
this.parent = parent;
|
|
923
955
|
this.data = value;
|
|
924
|
-
this._path =
|
|
956
|
+
this._path = path38;
|
|
925
957
|
this._key = key;
|
|
926
958
|
}
|
|
927
959
|
get path() {
|
|
@@ -4305,12 +4337,12 @@ var init_zod = __esm({
|
|
|
4305
4337
|
});
|
|
4306
4338
|
|
|
4307
4339
|
// ../protocol/src/attachment-schemas.ts
|
|
4308
|
-
var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema
|
|
4340
|
+
var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
|
|
4309
4341
|
var init_attachment_schemas = __esm({
|
|
4310
4342
|
"../protocol/src/attachment-schemas.ts"() {
|
|
4311
4343
|
"use strict";
|
|
4312
4344
|
init_zod();
|
|
4313
|
-
TOKEN_ROLES = ["owner"
|
|
4345
|
+
TOKEN_ROLES = ["owner"];
|
|
4314
4346
|
GROUP_FILE_SOURCES = ["agent", "owner"];
|
|
4315
4347
|
GroupFileEntrySchema = external_exports.object({
|
|
4316
4348
|
/** daemon 派发的稳定 id(用于 RPC remove / UI key) */
|
|
@@ -4331,11 +4363,9 @@ var init_attachment_schemas = __esm({
|
|
|
4331
4363
|
stale: external_exports.boolean().optional()
|
|
4332
4364
|
});
|
|
4333
4365
|
AttachmentSignUrlArgs = external_exports.object({
|
|
4334
|
-
/**
|
|
4335
|
-
|
|
4336
|
-
/**
|
|
4337
|
-
relPath: external_exports.string().min(1),
|
|
4338
|
-
/** TTL 秒数;缺省 24h;null 走永久 URL(不带 exp 字段) */
|
|
4366
|
+
/** 要分享的绝对路径;签名只关心 absPath,不区分 persona/session */
|
|
4367
|
+
absPath: external_exports.string().min(1),
|
|
4368
|
+
/** TTL 秒数;缺省 24h;'never' 走 null 不带 exp 字段(永久有效) */
|
|
4339
4369
|
ttlSeconds: external_exports.number().int().positive().nullable().optional()
|
|
4340
4370
|
});
|
|
4341
4371
|
AttachmentSignUrlResponseSchema = external_exports.object({
|
|
@@ -4366,31 +4396,15 @@ var init_attachment_schemas = __esm({
|
|
|
4366
4396
|
AttachmentGroupListResponseSchema = external_exports.object({
|
|
4367
4397
|
entries: external_exports.array(GroupFileEntrySchema)
|
|
4368
4398
|
});
|
|
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
|
-
});
|
|
4380
4399
|
}
|
|
4381
4400
|
});
|
|
4382
4401
|
|
|
4383
4402
|
// ../protocol/src/persona-schemas.ts
|
|
4384
|
-
var
|
|
4403
|
+
var PersonaFileSchema, PersonaSkillSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
|
|
4385
4404
|
var init_persona_schemas = __esm({
|
|
4386
4405
|
"../protocol/src/persona-schemas.ts"() {
|
|
4387
4406
|
"use strict";
|
|
4388
4407
|
init_zod();
|
|
4389
|
-
PersonaTokenEntrySchema = external_exports.object({
|
|
4390
|
-
label: external_exports.string(),
|
|
4391
|
-
issuedAt: external_exports.number(),
|
|
4392
|
-
revoked: external_exports.boolean()
|
|
4393
|
-
});
|
|
4394
4408
|
PersonaFileSchema = external_exports.object({
|
|
4395
4409
|
personaId: external_exports.string(),
|
|
4396
4410
|
label: external_exports.string(),
|
|
@@ -4399,13 +4413,6 @@ var init_persona_schemas = __esm({
|
|
|
4399
4413
|
// 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
|
|
4400
4414
|
// for forward compatibility, consumer fallbacks to default when key not found.
|
|
4401
4415
|
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(),
|
|
4409
4416
|
createdAt: external_exports.number(),
|
|
4410
4417
|
updatedAt: external_exports.number()
|
|
4411
4418
|
}).strict();
|
|
@@ -4415,10 +4422,6 @@ var init_persona_schemas = __esm({
|
|
|
4415
4422
|
// SKILL.md frontmatter `description:`;缺省时省略字段
|
|
4416
4423
|
description: external_exports.string().optional()
|
|
4417
4424
|
});
|
|
4418
|
-
PersonaPluginSummarySchema = external_exports.object({
|
|
4419
|
-
// settings.json 中 enabledPlugins 的 key,例如 'superpowers@claude-plugins-official'
|
|
4420
|
-
id: external_exports.string().min(1)
|
|
4421
|
-
});
|
|
4422
4425
|
PersonaSandboxSettingsSchema = external_exports.object({
|
|
4423
4426
|
permissions: external_exports.object({
|
|
4424
4427
|
defaultMode: external_exports.string().optional()
|
|
@@ -4439,8 +4442,6 @@ var init_persona_schemas = __esm({
|
|
|
4439
4442
|
personality: external_exports.string().optional(),
|
|
4440
4443
|
// 仅 'persona:get' 携带;null = 文件不存在 / parse 失败;undefined = 不在该响应类型上
|
|
4441
4444
|
skills: external_exports.array(PersonaSkillSummarySchema).optional(),
|
|
4442
|
-
// 仅 'persona:get' 携带;空数组 = 没启用任何插件 / settings.json 不存在;undefined = 不在该响应类型上
|
|
4443
|
-
plugins: external_exports.array(PersonaPluginSummarySchema).optional(),
|
|
4444
4445
|
sandboxSettings: PersonaSandboxSettingsSchema.nullable().optional()
|
|
4445
4446
|
});
|
|
4446
4447
|
PersonaCreateArgsSchema = external_exports.object({
|
|
@@ -4464,19 +4465,11 @@ var init_persona_schemas = __esm({
|
|
|
4464
4465
|
iconKey: external_exports.string().nullable().optional()
|
|
4465
4466
|
}).strict()
|
|
4466
4467
|
}).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
|
-
});
|
|
4475
4468
|
}
|
|
4476
4469
|
});
|
|
4477
4470
|
|
|
4478
4471
|
// ../protocol/src/schemas.ts
|
|
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,
|
|
4472
|
+
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;
|
|
4480
4473
|
var init_schemas = __esm({
|
|
4481
4474
|
"../protocol/src/schemas.ts"() {
|
|
4482
4475
|
"use strict";
|
|
@@ -4578,6 +4571,15 @@ var init_schemas = __esm({
|
|
|
4578
4571
|
// 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
|
|
4579
4572
|
// optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
|
|
4580
4573
|
chatId: external_exports.string().min(1).optional(),
|
|
4574
|
+
/**
|
|
4575
|
+
* 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
|
|
4576
|
+
* 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
|
|
4577
|
+
* UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
|
|
4578
|
+
* 后者再通过 PersonAlias 表反查 personId 二级分组。
|
|
4579
|
+
*
|
|
4580
|
+
* optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
|
|
4581
|
+
*/
|
|
4582
|
+
creatorPrincipalId: external_exports.string().min(1).optional(),
|
|
4581
4583
|
createdAt: external_exports.string().min(1),
|
|
4582
4584
|
updatedAt: external_exports.string().min(1)
|
|
4583
4585
|
});
|
|
@@ -4987,7 +4989,20 @@ var init_schemas = __esm({
|
|
|
4987
4989
|
AuthRequestFrameSchema = external_exports.object({
|
|
4988
4990
|
type: external_exports.literal("auth"),
|
|
4989
4991
|
token: external_exports.string().min(1),
|
|
4990
|
-
scheme: external_exports.literal("bearer").optional()
|
|
4992
|
+
scheme: external_exports.literal("bearer").optional(),
|
|
4993
|
+
/**
|
|
4994
|
+
* guest 端 ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
|
|
4995
|
+
* 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
|
|
4996
|
+
* 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
|
|
4997
|
+
* tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
|
|
4998
|
+
*/
|
|
4999
|
+
selfPrincipalId: external_exports.string().min(1).optional(),
|
|
5000
|
+
/**
|
|
5001
|
+
* 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
|
|
5002
|
+
* 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
|
|
5003
|
+
* 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
|
|
5004
|
+
*/
|
|
5005
|
+
selfDisplayName: external_exports.string().optional()
|
|
4991
5006
|
});
|
|
4992
5007
|
AuthOkFrameSchema = external_exports.object({
|
|
4993
5008
|
type: external_exports.literal("auth:ok"),
|
|
@@ -5000,11 +5015,6 @@ var init_schemas = __esm({
|
|
|
5000
5015
|
subdomain: external_exports.string().nullable(),
|
|
5001
5016
|
url: external_exports.string().nullable()
|
|
5002
5017
|
});
|
|
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
|
-
});
|
|
5008
5018
|
InfoRunningSessionSchema = external_exports.object({
|
|
5009
5019
|
sessionId: external_exports.string().min(1),
|
|
5010
5020
|
status: SessionStatusSchema,
|
|
@@ -5025,10 +5035,9 @@ var init_schemas = __esm({
|
|
|
5025
5035
|
// PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
|
|
5026
5036
|
// 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
|
|
5027
5037
|
// 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
|
|
5028
|
-
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP
|
|
5038
|
+
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
|
|
5039
|
+
* 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
|
|
5029
5040
|
tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
|
|
5030
|
-
/** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
|
|
5031
|
-
tokenPersonaId: external_exports.string().min(1).optional(),
|
|
5032
5041
|
/** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
|
|
5033
5042
|
isLoopback: external_exports.boolean().optional(),
|
|
5034
5043
|
/** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
|
|
@@ -5055,6 +5064,194 @@ var init_persona_mode = __esm({
|
|
|
5055
5064
|
}
|
|
5056
5065
|
});
|
|
5057
5066
|
|
|
5067
|
+
// ../protocol/src/principal.ts
|
|
5068
|
+
function makeOwnerPrincipal(id, displayName) {
|
|
5069
|
+
return PrincipalSchema.parse({ id, kind: "owner", displayName });
|
|
5070
|
+
}
|
|
5071
|
+
var PrincipalKindSchema, PrincipalSchema;
|
|
5072
|
+
var init_principal = __esm({
|
|
5073
|
+
"../protocol/src/principal.ts"() {
|
|
5074
|
+
"use strict";
|
|
5075
|
+
init_zod();
|
|
5076
|
+
PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
|
|
5077
|
+
PrincipalSchema = external_exports.object({
|
|
5078
|
+
id: external_exports.string().min(1),
|
|
5079
|
+
kind: PrincipalKindSchema,
|
|
5080
|
+
displayName: external_exports.string()
|
|
5081
|
+
}).strict();
|
|
5082
|
+
}
|
|
5083
|
+
});
|
|
5084
|
+
|
|
5085
|
+
// ../protocol/src/capability.ts
|
|
5086
|
+
function stripSecretHash(cap) {
|
|
5087
|
+
const { secretHash: _hash, ...wire } = cap;
|
|
5088
|
+
return wire;
|
|
5089
|
+
}
|
|
5090
|
+
var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema, CapabilityBindPeerArgsSchema;
|
|
5091
|
+
var init_capability = __esm({
|
|
5092
|
+
"../protocol/src/capability.ts"() {
|
|
5093
|
+
"use strict";
|
|
5094
|
+
init_zod();
|
|
5095
|
+
ResourceSchema = external_exports.discriminatedUnion("type", [
|
|
5096
|
+
external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
|
|
5097
|
+
external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
|
|
5098
|
+
external_exports.object({ type: external_exports.literal("*") }).strict()
|
|
5099
|
+
]);
|
|
5100
|
+
ActionSchema = external_exports.enum(["read", "send", "admin"]);
|
|
5101
|
+
GrantSchema = external_exports.object({
|
|
5102
|
+
resource: ResourceSchema,
|
|
5103
|
+
actions: external_exports.array(ActionSchema).min(1)
|
|
5104
|
+
}).strict();
|
|
5105
|
+
CapabilitySchema = external_exports.object({
|
|
5106
|
+
id: external_exports.string().min(1),
|
|
5107
|
+
// sha256(token) hex 64
|
|
5108
|
+
secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
5109
|
+
displayName: external_exports.string(),
|
|
5110
|
+
// 空数组合法 = 纯访客(只能 DM)
|
|
5111
|
+
grants: external_exports.array(GrantSchema),
|
|
5112
|
+
issuedAt: external_exports.number().int().nonnegative(),
|
|
5113
|
+
expiresAt: external_exports.number().int().positive().optional(),
|
|
5114
|
+
maxUses: external_exports.number().int().positive().optional(),
|
|
5115
|
+
usedCount: external_exports.number().int().nonnegative(),
|
|
5116
|
+
/**
|
|
5117
|
+
* guest 在 auth 帧捎带 selfPrincipalId 时由 AuthGate.authenticate 回写;
|
|
5118
|
+
* 未被消费的 cap 此字段缺失。
|
|
5119
|
+
*/
|
|
5120
|
+
peerOwnerId: external_exports.string().min(1).optional(),
|
|
5121
|
+
/** 首次 client hello 时间戳(与 peerOwnerId 同步写入) */
|
|
5122
|
+
firstUsedByPeerAt: external_exports.number().int().positive().optional(),
|
|
5123
|
+
/**
|
|
5124
|
+
* 对方 daemon owner 的 displayName,guest auth 帧 selfDisplayName 触发
|
|
5125
|
+
* recordPeerHello 时同步回写。视角 B (2026-05-23) UI 派生联系人列表用此字段
|
|
5126
|
+
* 显示对方名字(避免 fallback 到我颁 cap 时填的 displayName)。
|
|
5127
|
+
*/
|
|
5128
|
+
peerDisplayName: external_exports.string().optional()
|
|
5129
|
+
}).strict();
|
|
5130
|
+
CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
|
|
5131
|
+
CapabilityErrorCodeSchema = external_exports.enum([
|
|
5132
|
+
"TOKEN_INVALID",
|
|
5133
|
+
"TOKEN_REVOKED",
|
|
5134
|
+
"TOKEN_EXPIRED",
|
|
5135
|
+
"TOKEN_EXHAUSTED"
|
|
5136
|
+
]);
|
|
5137
|
+
WhoamiResponseSchema = external_exports.object({
|
|
5138
|
+
type: external_exports.literal("whoami:ok"),
|
|
5139
|
+
owner: external_exports.object({
|
|
5140
|
+
id: external_exports.string(),
|
|
5141
|
+
kind: external_exports.enum(["owner", "guest"]),
|
|
5142
|
+
displayName: external_exports.string()
|
|
5143
|
+
}).strict(),
|
|
5144
|
+
capability: CapabilityWireSchema,
|
|
5145
|
+
grantedPersonas: external_exports.array(
|
|
5146
|
+
external_exports.object({
|
|
5147
|
+
id: external_exports.string().min(1),
|
|
5148
|
+
displayName: external_exports.string()
|
|
5149
|
+
}).strict()
|
|
5150
|
+
)
|
|
5151
|
+
}).strict();
|
|
5152
|
+
CapabilityIssueArgsSchema = external_exports.object({
|
|
5153
|
+
displayName: external_exports.string().min(1),
|
|
5154
|
+
grants: external_exports.array(GrantSchema),
|
|
5155
|
+
expiresAt: external_exports.number().int().positive().optional(),
|
|
5156
|
+
maxUses: external_exports.number().int().positive().optional()
|
|
5157
|
+
}).strict();
|
|
5158
|
+
CapabilityBindPeerArgsSchema = external_exports.object({
|
|
5159
|
+
capabilityId: external_exports.string().min(1),
|
|
5160
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5161
|
+
peerDisplayName: external_exports.string()
|
|
5162
|
+
}).strict();
|
|
5163
|
+
}
|
|
5164
|
+
});
|
|
5165
|
+
|
|
5166
|
+
// ../protocol/src/inbox.ts
|
|
5167
|
+
var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
|
|
5168
|
+
var init_inbox = __esm({
|
|
5169
|
+
"../protocol/src/inbox.ts"() {
|
|
5170
|
+
"use strict";
|
|
5171
|
+
init_zod();
|
|
5172
|
+
InboxMessageSchema = external_exports.object({
|
|
5173
|
+
id: external_exports.string().min(1),
|
|
5174
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5175
|
+
senderPrincipalId: external_exports.string().min(1),
|
|
5176
|
+
text: external_exports.string().min(1),
|
|
5177
|
+
createdAt: external_exports.number().int().nonnegative(),
|
|
5178
|
+
readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
|
|
5179
|
+
}).strict();
|
|
5180
|
+
InboxPostMessageArgsSchema = external_exports.object({
|
|
5181
|
+
peerOwnerId: external_exports.string().min(1).optional(),
|
|
5182
|
+
id: external_exports.string().min(1),
|
|
5183
|
+
text: external_exports.string().min(1),
|
|
5184
|
+
createdAt: external_exports.number().int().nonnegative()
|
|
5185
|
+
}).strict();
|
|
5186
|
+
InboxListArgsSchema = external_exports.object({
|
|
5187
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5188
|
+
sinceCreatedAt: external_exports.number().int().nonnegative().optional()
|
|
5189
|
+
}).strict();
|
|
5190
|
+
InboxMarkReadArgsSchema = external_exports.object({
|
|
5191
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5192
|
+
upToCreatedAt: external_exports.number().int().nonnegative()
|
|
5193
|
+
}).strict();
|
|
5194
|
+
}
|
|
5195
|
+
});
|
|
5196
|
+
|
|
5197
|
+
// ../protocol/src/received-capability.ts
|
|
5198
|
+
var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
|
|
5199
|
+
var init_received_capability = __esm({
|
|
5200
|
+
"../protocol/src/received-capability.ts"() {
|
|
5201
|
+
"use strict";
|
|
5202
|
+
init_zod();
|
|
5203
|
+
init_capability();
|
|
5204
|
+
ReceivedCapabilitySchema = external_exports.object({
|
|
5205
|
+
ownerPrincipalId: external_exports.string().min(1),
|
|
5206
|
+
ownerDisplayName: external_exports.string(),
|
|
5207
|
+
remoteUrl: external_exports.string().min(1),
|
|
5208
|
+
capabilityId: external_exports.string().min(1),
|
|
5209
|
+
capabilityToken: external_exports.string().min(1),
|
|
5210
|
+
grants: external_exports.array(GrantSchema),
|
|
5211
|
+
receivedAt: external_exports.number().int().nonnegative()
|
|
5212
|
+
}).strict();
|
|
5213
|
+
ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
|
|
5214
|
+
ReceivedCapabilityAddArgsSchema = external_exports.object({
|
|
5215
|
+
remoteUrl: external_exports.string().min(1),
|
|
5216
|
+
token: external_exports.string().min(1)
|
|
5217
|
+
}).strict();
|
|
5218
|
+
ReceivedCapabilityAddOkSchema = external_exports.object({
|
|
5219
|
+
type: external_exports.literal("received-capability:add:ok"),
|
|
5220
|
+
receivedCap: ReceivedCapabilityWireSchema
|
|
5221
|
+
}).strict();
|
|
5222
|
+
ReceivedCapabilityRemoveArgsSchema = external_exports.object({
|
|
5223
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5224
|
+
}).strict();
|
|
5225
|
+
ReceivedCapabilityRemoveOkSchema = external_exports.object({
|
|
5226
|
+
type: external_exports.literal("received-capability:remove:ok"),
|
|
5227
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5228
|
+
}).strict();
|
|
5229
|
+
ReceivedCapabilityListOkSchema = external_exports.object({
|
|
5230
|
+
type: external_exports.literal("received-capability:list:ok"),
|
|
5231
|
+
receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
|
|
5232
|
+
}).strict();
|
|
5233
|
+
ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
|
|
5234
|
+
ownerPrincipalId: external_exports.string().min(1),
|
|
5235
|
+
ownerDisplayName: external_exports.string(),
|
|
5236
|
+
remoteUrl: external_exports.string().min(1),
|
|
5237
|
+
capabilityId: external_exports.string().min(1),
|
|
5238
|
+
token: external_exports.string().min(1),
|
|
5239
|
+
grants: external_exports.array(GrantSchema)
|
|
5240
|
+
}).strict();
|
|
5241
|
+
ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
|
|
5242
|
+
type: external_exports.literal("received-capability:autoAttach:ok")
|
|
5243
|
+
}).strict();
|
|
5244
|
+
ReceivedCapabilityAddedFrameSchema = external_exports.object({
|
|
5245
|
+
type: external_exports.literal("received-capability:added"),
|
|
5246
|
+
receivedCap: ReceivedCapabilityWireSchema
|
|
5247
|
+
}).strict();
|
|
5248
|
+
ReceivedCapabilityRemovedFrameSchema = external_exports.object({
|
|
5249
|
+
type: external_exports.literal("received-capability:removed"),
|
|
5250
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5251
|
+
}).strict();
|
|
5252
|
+
}
|
|
5253
|
+
});
|
|
5254
|
+
|
|
5058
5255
|
// ../protocol/src/runtime.ts
|
|
5059
5256
|
var init_runtime = __esm({
|
|
5060
5257
|
"../protocol/src/runtime.ts"() {
|
|
@@ -5067,6 +5264,10 @@ var init_runtime = __esm({
|
|
|
5067
5264
|
init_persona_schemas();
|
|
5068
5265
|
init_persona_mode();
|
|
5069
5266
|
init_attachment_schemas();
|
|
5267
|
+
init_principal();
|
|
5268
|
+
init_capability();
|
|
5269
|
+
init_inbox();
|
|
5270
|
+
init_received_capability();
|
|
5070
5271
|
}
|
|
5071
5272
|
});
|
|
5072
5273
|
|
|
@@ -5341,8 +5542,8 @@ var require_req = __commonJS({
|
|
|
5341
5542
|
if (req.originalUrl) {
|
|
5342
5543
|
_req.url = req.originalUrl;
|
|
5343
5544
|
} else {
|
|
5344
|
-
const
|
|
5345
|
-
_req.url = typeof
|
|
5545
|
+
const path38 = req.path;
|
|
5546
|
+
_req.url = typeof path38 === "string" ? path38 : req.url ? req.url.path || req.url : void 0;
|
|
5346
5547
|
}
|
|
5347
5548
|
if (req.query) {
|
|
5348
5549
|
_req.query = req.query;
|
|
@@ -5507,14 +5708,14 @@ var require_redact = __commonJS({
|
|
|
5507
5708
|
}
|
|
5508
5709
|
return obj;
|
|
5509
5710
|
}
|
|
5510
|
-
function parsePath(
|
|
5711
|
+
function parsePath(path38) {
|
|
5511
5712
|
const parts = [];
|
|
5512
5713
|
let current = "";
|
|
5513
5714
|
let inBrackets = false;
|
|
5514
5715
|
let inQuotes = false;
|
|
5515
5716
|
let quoteChar = "";
|
|
5516
|
-
for (let i = 0; i <
|
|
5517
|
-
const char =
|
|
5717
|
+
for (let i = 0; i < path38.length; i++) {
|
|
5718
|
+
const char = path38[i];
|
|
5518
5719
|
if (!inBrackets && char === ".") {
|
|
5519
5720
|
if (current) {
|
|
5520
5721
|
parts.push(current);
|
|
@@ -5645,10 +5846,10 @@ var require_redact = __commonJS({
|
|
|
5645
5846
|
return current;
|
|
5646
5847
|
}
|
|
5647
5848
|
function redactPaths(obj, paths, censor, remove = false) {
|
|
5648
|
-
for (const
|
|
5649
|
-
const parts = parsePath(
|
|
5849
|
+
for (const path38 of paths) {
|
|
5850
|
+
const parts = parsePath(path38);
|
|
5650
5851
|
if (parts.includes("*")) {
|
|
5651
|
-
redactWildcardPath(obj, parts, censor,
|
|
5852
|
+
redactWildcardPath(obj, parts, censor, path38, remove);
|
|
5652
5853
|
} else {
|
|
5653
5854
|
if (remove) {
|
|
5654
5855
|
removeKey(obj, parts);
|
|
@@ -5733,8 +5934,8 @@ var require_redact = __commonJS({
|
|
|
5733
5934
|
}
|
|
5734
5935
|
} else {
|
|
5735
5936
|
if (afterWildcard.includes("*")) {
|
|
5736
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
5737
|
-
const fullPath = [...pathArray.slice(0, pathLength), ...
|
|
5937
|
+
const wrappedCensor = typeof censor === "function" ? (value, path38) => {
|
|
5938
|
+
const fullPath = [...pathArray.slice(0, pathLength), ...path38];
|
|
5738
5939
|
return censor(value, fullPath);
|
|
5739
5940
|
} : censor;
|
|
5740
5941
|
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
|
|
@@ -5769,8 +5970,8 @@ var require_redact = __commonJS({
|
|
|
5769
5970
|
return null;
|
|
5770
5971
|
}
|
|
5771
5972
|
const pathStructure = /* @__PURE__ */ new Map();
|
|
5772
|
-
for (const
|
|
5773
|
-
const parts = parsePath(
|
|
5973
|
+
for (const path38 of pathsToClone) {
|
|
5974
|
+
const parts = parsePath(path38);
|
|
5774
5975
|
let current = pathStructure;
|
|
5775
5976
|
for (let i = 0; i < parts.length; i++) {
|
|
5776
5977
|
const part = parts[i];
|
|
@@ -5822,24 +6023,24 @@ var require_redact = __commonJS({
|
|
|
5822
6023
|
}
|
|
5823
6024
|
return cloneSelectively(obj, pathStructure);
|
|
5824
6025
|
}
|
|
5825
|
-
function validatePath(
|
|
5826
|
-
if (typeof
|
|
6026
|
+
function validatePath(path38) {
|
|
6027
|
+
if (typeof path38 !== "string") {
|
|
5827
6028
|
throw new Error("Paths must be (non-empty) strings");
|
|
5828
6029
|
}
|
|
5829
|
-
if (
|
|
6030
|
+
if (path38 === "") {
|
|
5830
6031
|
throw new Error("Invalid redaction path ()");
|
|
5831
6032
|
}
|
|
5832
|
-
if (
|
|
5833
|
-
throw new Error(`Invalid redaction path (${
|
|
6033
|
+
if (path38.includes("..")) {
|
|
6034
|
+
throw new Error(`Invalid redaction path (${path38})`);
|
|
5834
6035
|
}
|
|
5835
|
-
if (
|
|
5836
|
-
throw new Error(`Invalid redaction path (${
|
|
6036
|
+
if (path38.includes(",")) {
|
|
6037
|
+
throw new Error(`Invalid redaction path (${path38})`);
|
|
5837
6038
|
}
|
|
5838
6039
|
let bracketCount = 0;
|
|
5839
6040
|
let inQuotes = false;
|
|
5840
6041
|
let quoteChar = "";
|
|
5841
|
-
for (let i = 0; i <
|
|
5842
|
-
const char =
|
|
6042
|
+
for (let i = 0; i < path38.length; i++) {
|
|
6043
|
+
const char = path38[i];
|
|
5843
6044
|
if ((char === '"' || char === "'") && bracketCount > 0) {
|
|
5844
6045
|
if (!inQuotes) {
|
|
5845
6046
|
inQuotes = true;
|
|
@@ -5853,20 +6054,20 @@ var require_redact = __commonJS({
|
|
|
5853
6054
|
} else if (char === "]" && !inQuotes) {
|
|
5854
6055
|
bracketCount--;
|
|
5855
6056
|
if (bracketCount < 0) {
|
|
5856
|
-
throw new Error(`Invalid redaction path (${
|
|
6057
|
+
throw new Error(`Invalid redaction path (${path38})`);
|
|
5857
6058
|
}
|
|
5858
6059
|
}
|
|
5859
6060
|
}
|
|
5860
6061
|
if (bracketCount !== 0) {
|
|
5861
|
-
throw new Error(`Invalid redaction path (${
|
|
6062
|
+
throw new Error(`Invalid redaction path (${path38})`);
|
|
5862
6063
|
}
|
|
5863
6064
|
}
|
|
5864
6065
|
function validatePaths(paths) {
|
|
5865
6066
|
if (!Array.isArray(paths)) {
|
|
5866
6067
|
throw new TypeError("paths must be an array");
|
|
5867
6068
|
}
|
|
5868
|
-
for (const
|
|
5869
|
-
validatePath(
|
|
6069
|
+
for (const path38 of paths) {
|
|
6070
|
+
validatePath(path38);
|
|
5870
6071
|
}
|
|
5871
6072
|
}
|
|
5872
6073
|
function slowRedact(options = {}) {
|
|
@@ -6034,8 +6235,8 @@ var require_redaction = __commonJS({
|
|
|
6034
6235
|
if (shape[k2] === null) {
|
|
6035
6236
|
o[k2] = (value) => topCensor(value, [k2]);
|
|
6036
6237
|
} else {
|
|
6037
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
6038
|
-
return censor(value, [k2, ...
|
|
6238
|
+
const wrappedCensor = typeof censor === "function" ? (value, path38) => {
|
|
6239
|
+
return censor(value, [k2, ...path38]);
|
|
6039
6240
|
} : censor;
|
|
6040
6241
|
o[k2] = Redact({
|
|
6041
6242
|
paths: shape[k2],
|
|
@@ -6253,10 +6454,10 @@ var require_atomic_sleep = __commonJS({
|
|
|
6253
6454
|
var require_sonic_boom = __commonJS({
|
|
6254
6455
|
"../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
|
|
6255
6456
|
"use strict";
|
|
6256
|
-
var
|
|
6457
|
+
var fs34 = require("fs");
|
|
6257
6458
|
var EventEmitter2 = require("events");
|
|
6258
6459
|
var inherits = require("util").inherits;
|
|
6259
|
-
var
|
|
6460
|
+
var path38 = require("path");
|
|
6260
6461
|
var sleep = require_atomic_sleep();
|
|
6261
6462
|
var assert = require("assert");
|
|
6262
6463
|
var BUSY_WRITE_TIMEOUT = 100;
|
|
@@ -6310,20 +6511,20 @@ var require_sonic_boom = __commonJS({
|
|
|
6310
6511
|
const mode = sonic.mode;
|
|
6311
6512
|
if (sonic.sync) {
|
|
6312
6513
|
try {
|
|
6313
|
-
if (sonic.mkdir)
|
|
6314
|
-
const fd =
|
|
6514
|
+
if (sonic.mkdir) fs34.mkdirSync(path38.dirname(file), { recursive: true });
|
|
6515
|
+
const fd = fs34.openSync(file, flags, mode);
|
|
6315
6516
|
fileOpened(null, fd);
|
|
6316
6517
|
} catch (err) {
|
|
6317
6518
|
fileOpened(err);
|
|
6318
6519
|
throw err;
|
|
6319
6520
|
}
|
|
6320
6521
|
} else if (sonic.mkdir) {
|
|
6321
|
-
|
|
6522
|
+
fs34.mkdir(path38.dirname(file), { recursive: true }, (err) => {
|
|
6322
6523
|
if (err) return fileOpened(err);
|
|
6323
|
-
|
|
6524
|
+
fs34.open(file, flags, mode, fileOpened);
|
|
6324
6525
|
});
|
|
6325
6526
|
} else {
|
|
6326
|
-
|
|
6527
|
+
fs34.open(file, flags, mode, fileOpened);
|
|
6327
6528
|
}
|
|
6328
6529
|
}
|
|
6329
6530
|
function SonicBoom(opts) {
|
|
@@ -6364,8 +6565,8 @@ var require_sonic_boom = __commonJS({
|
|
|
6364
6565
|
this.flush = flushBuffer;
|
|
6365
6566
|
this.flushSync = flushBufferSync;
|
|
6366
6567
|
this._actualWrite = actualWriteBuffer;
|
|
6367
|
-
fsWriteSync = () =>
|
|
6368
|
-
fsWrite = () =>
|
|
6568
|
+
fsWriteSync = () => fs34.writeSync(this.fd, this._writingBuf);
|
|
6569
|
+
fsWrite = () => fs34.write(this.fd, this._writingBuf, this.release);
|
|
6369
6570
|
} else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
|
|
6370
6571
|
this._writingBuf = "";
|
|
6371
6572
|
this.write = write;
|
|
@@ -6374,15 +6575,15 @@ var require_sonic_boom = __commonJS({
|
|
|
6374
6575
|
this._actualWrite = actualWrite;
|
|
6375
6576
|
fsWriteSync = () => {
|
|
6376
6577
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6377
|
-
return
|
|
6578
|
+
return fs34.writeSync(this.fd, this._writingBuf);
|
|
6378
6579
|
}
|
|
6379
|
-
return
|
|
6580
|
+
return fs34.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6380
6581
|
};
|
|
6381
6582
|
fsWrite = () => {
|
|
6382
6583
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6383
|
-
return
|
|
6584
|
+
return fs34.write(this.fd, this._writingBuf, this.release);
|
|
6384
6585
|
}
|
|
6385
|
-
return
|
|
6586
|
+
return fs34.write(this.fd, this._writingBuf, "utf8", this.release);
|
|
6386
6587
|
};
|
|
6387
6588
|
} else {
|
|
6388
6589
|
throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
|
|
@@ -6439,7 +6640,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6439
6640
|
}
|
|
6440
6641
|
}
|
|
6441
6642
|
if (this._fsync) {
|
|
6442
|
-
|
|
6643
|
+
fs34.fsyncSync(this.fd);
|
|
6443
6644
|
}
|
|
6444
6645
|
const len = this._len;
|
|
6445
6646
|
if (this._reopening) {
|
|
@@ -6553,7 +6754,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6553
6754
|
const onDrain = () => {
|
|
6554
6755
|
if (!this._fsync) {
|
|
6555
6756
|
try {
|
|
6556
|
-
|
|
6757
|
+
fs34.fsync(this.fd, (err) => {
|
|
6557
6758
|
this._flushPending = false;
|
|
6558
6759
|
cb(err);
|
|
6559
6760
|
});
|
|
@@ -6655,7 +6856,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6655
6856
|
const fd = this.fd;
|
|
6656
6857
|
this.once("ready", () => {
|
|
6657
6858
|
if (fd !== this.fd) {
|
|
6658
|
-
|
|
6859
|
+
fs34.close(fd, (err) => {
|
|
6659
6860
|
if (err) {
|
|
6660
6861
|
return this.emit("error", err);
|
|
6661
6862
|
}
|
|
@@ -6704,7 +6905,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6704
6905
|
buf = this._bufs[0];
|
|
6705
6906
|
}
|
|
6706
6907
|
try {
|
|
6707
|
-
const n = Buffer.isBuffer(buf) ?
|
|
6908
|
+
const n = Buffer.isBuffer(buf) ? fs34.writeSync(this.fd, buf) : fs34.writeSync(this.fd, buf, "utf8");
|
|
6708
6909
|
const releasedBufObj = releaseWritingBuf(buf, this._len, n);
|
|
6709
6910
|
buf = releasedBufObj.writingBuf;
|
|
6710
6911
|
this._len = releasedBufObj.len;
|
|
@@ -6720,7 +6921,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6720
6921
|
}
|
|
6721
6922
|
}
|
|
6722
6923
|
try {
|
|
6723
|
-
|
|
6924
|
+
fs34.fsyncSync(this.fd);
|
|
6724
6925
|
} catch {
|
|
6725
6926
|
}
|
|
6726
6927
|
}
|
|
@@ -6741,7 +6942,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6741
6942
|
buf = mergeBuf(this._bufs[0], this._lens[0]);
|
|
6742
6943
|
}
|
|
6743
6944
|
try {
|
|
6744
|
-
const n =
|
|
6945
|
+
const n = fs34.writeSync(this.fd, buf);
|
|
6745
6946
|
buf = buf.subarray(n);
|
|
6746
6947
|
this._len = Math.max(this._len - n, 0);
|
|
6747
6948
|
if (buf.length <= 0) {
|
|
@@ -6769,13 +6970,13 @@ var require_sonic_boom = __commonJS({
|
|
|
6769
6970
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
|
|
6770
6971
|
if (this.sync) {
|
|
6771
6972
|
try {
|
|
6772
|
-
const written = Buffer.isBuffer(this._writingBuf) ?
|
|
6973
|
+
const written = Buffer.isBuffer(this._writingBuf) ? fs34.writeSync(this.fd, this._writingBuf) : fs34.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6773
6974
|
release(null, written);
|
|
6774
6975
|
} catch (err) {
|
|
6775
6976
|
release(err);
|
|
6776
6977
|
}
|
|
6777
6978
|
} else {
|
|
6778
|
-
|
|
6979
|
+
fs34.write(this.fd, this._writingBuf, release);
|
|
6779
6980
|
}
|
|
6780
6981
|
}
|
|
6781
6982
|
function actualWriteBuffer() {
|
|
@@ -6784,7 +6985,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6784
6985
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
|
|
6785
6986
|
if (this.sync) {
|
|
6786
6987
|
try {
|
|
6787
|
-
const written =
|
|
6988
|
+
const written = fs34.writeSync(this.fd, this._writingBuf);
|
|
6788
6989
|
release(null, written);
|
|
6789
6990
|
} catch (err) {
|
|
6790
6991
|
release(err);
|
|
@@ -6793,7 +6994,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6793
6994
|
if (kCopyBuffer) {
|
|
6794
6995
|
this._writingBuf = Buffer.from(this._writingBuf);
|
|
6795
6996
|
}
|
|
6796
|
-
|
|
6997
|
+
fs34.write(this.fd, this._writingBuf, release);
|
|
6797
6998
|
}
|
|
6798
6999
|
}
|
|
6799
7000
|
function actualClose(sonic) {
|
|
@@ -6809,12 +7010,12 @@ var require_sonic_boom = __commonJS({
|
|
|
6809
7010
|
sonic._lens = [];
|
|
6810
7011
|
assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
|
|
6811
7012
|
try {
|
|
6812
|
-
|
|
7013
|
+
fs34.fsync(sonic.fd, closeWrapped);
|
|
6813
7014
|
} catch {
|
|
6814
7015
|
}
|
|
6815
7016
|
function closeWrapped() {
|
|
6816
7017
|
if (sonic.fd !== 1 && sonic.fd !== 2) {
|
|
6817
|
-
|
|
7018
|
+
fs34.close(sonic.fd, done);
|
|
6818
7019
|
} else {
|
|
6819
7020
|
done();
|
|
6820
7021
|
}
|
|
@@ -7071,7 +7272,7 @@ var require_thread_stream = __commonJS({
|
|
|
7071
7272
|
var { version: version2 } = require_package();
|
|
7072
7273
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
7073
7274
|
var { Worker } = require("worker_threads");
|
|
7074
|
-
var { join:
|
|
7275
|
+
var { join: join11 } = require("path");
|
|
7075
7276
|
var { pathToFileURL } = require("url");
|
|
7076
7277
|
var { wait } = require_wait();
|
|
7077
7278
|
var {
|
|
@@ -7107,7 +7308,7 @@ var require_thread_stream = __commonJS({
|
|
|
7107
7308
|
function createWorker(stream, opts) {
|
|
7108
7309
|
const { filename, workerData } = opts;
|
|
7109
7310
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
7110
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
7311
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
|
|
7111
7312
|
const worker = new Worker(toExecute, {
|
|
7112
7313
|
...opts.workerOpts,
|
|
7113
7314
|
trackUnmanagedFds: false,
|
|
@@ -7493,7 +7694,7 @@ var require_transport = __commonJS({
|
|
|
7493
7694
|
"use strict";
|
|
7494
7695
|
var { createRequire } = require("module");
|
|
7495
7696
|
var getCallers = require_caller();
|
|
7496
|
-
var { join:
|
|
7697
|
+
var { join: join11, isAbsolute, sep: sep2 } = require("path");
|
|
7497
7698
|
var sleep = require_atomic_sleep();
|
|
7498
7699
|
var onExit = require_on_exit_leak_free();
|
|
7499
7700
|
var ThreadStream = require_thread_stream();
|
|
@@ -7556,7 +7757,7 @@ var require_transport = __commonJS({
|
|
|
7556
7757
|
throw new Error("only one of target or targets can be specified");
|
|
7557
7758
|
}
|
|
7558
7759
|
if (targets) {
|
|
7559
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
7760
|
+
target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
|
|
7560
7761
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
7561
7762
|
return {
|
|
7562
7763
|
...dest,
|
|
@@ -7574,7 +7775,7 @@ var require_transport = __commonJS({
|
|
|
7574
7775
|
});
|
|
7575
7776
|
});
|
|
7576
7777
|
} else if (pipeline2) {
|
|
7577
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
7778
|
+
target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
|
|
7578
7779
|
options.pipelines = [pipeline2.map((dest) => {
|
|
7579
7780
|
return {
|
|
7580
7781
|
...dest,
|
|
@@ -7596,12 +7797,12 @@ var require_transport = __commonJS({
|
|
|
7596
7797
|
return origin;
|
|
7597
7798
|
}
|
|
7598
7799
|
if (origin === "pino/file") {
|
|
7599
|
-
return
|
|
7800
|
+
return join11(__dirname, "..", "file.js");
|
|
7600
7801
|
}
|
|
7601
7802
|
let fixTarget2;
|
|
7602
7803
|
for (const filePath of callers) {
|
|
7603
7804
|
try {
|
|
7604
|
-
const context = filePath === "node:repl" ? process.cwd() +
|
|
7805
|
+
const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
|
|
7605
7806
|
fixTarget2 = createRequire(context).resolve(origin);
|
|
7606
7807
|
break;
|
|
7607
7808
|
} catch (err) {
|
|
@@ -8586,7 +8787,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
8586
8787
|
return circularValue;
|
|
8587
8788
|
}
|
|
8588
8789
|
let res = "";
|
|
8589
|
-
let
|
|
8790
|
+
let join11 = ",";
|
|
8590
8791
|
const originalIndentation = indentation;
|
|
8591
8792
|
if (Array.isArray(value)) {
|
|
8592
8793
|
if (value.length === 0) {
|
|
@@ -8600,7 +8801,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
8600
8801
|
indentation += spacer;
|
|
8601
8802
|
res += `
|
|
8602
8803
|
${indentation}`;
|
|
8603
|
-
|
|
8804
|
+
join11 = `,
|
|
8604
8805
|
${indentation}`;
|
|
8605
8806
|
}
|
|
8606
8807
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -8608,13 +8809,13 @@ ${indentation}`;
|
|
|
8608
8809
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8609
8810
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
8610
8811
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
8611
|
-
res +=
|
|
8812
|
+
res += join11;
|
|
8612
8813
|
}
|
|
8613
8814
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
8614
8815
|
res += tmp !== void 0 ? tmp : "null";
|
|
8615
8816
|
if (value.length - 1 > maximumBreadth) {
|
|
8616
8817
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8617
|
-
res += `${
|
|
8818
|
+
res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8618
8819
|
}
|
|
8619
8820
|
if (spacer !== "") {
|
|
8620
8821
|
res += `
|
|
@@ -8635,7 +8836,7 @@ ${originalIndentation}`;
|
|
|
8635
8836
|
let separator = "";
|
|
8636
8837
|
if (spacer !== "") {
|
|
8637
8838
|
indentation += spacer;
|
|
8638
|
-
|
|
8839
|
+
join11 = `,
|
|
8639
8840
|
${indentation}`;
|
|
8640
8841
|
whitespace = " ";
|
|
8641
8842
|
}
|
|
@@ -8649,13 +8850,13 @@ ${indentation}`;
|
|
|
8649
8850
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
8650
8851
|
if (tmp !== void 0) {
|
|
8651
8852
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
8652
|
-
separator =
|
|
8853
|
+
separator = join11;
|
|
8653
8854
|
}
|
|
8654
8855
|
}
|
|
8655
8856
|
if (keyLength > maximumBreadth) {
|
|
8656
8857
|
const removedKeys = keyLength - maximumBreadth;
|
|
8657
8858
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
8658
|
-
separator =
|
|
8859
|
+
separator = join11;
|
|
8659
8860
|
}
|
|
8660
8861
|
if (spacer !== "" && separator.length > 1) {
|
|
8661
8862
|
res = `
|
|
@@ -8696,7 +8897,7 @@ ${originalIndentation}`;
|
|
|
8696
8897
|
}
|
|
8697
8898
|
const originalIndentation = indentation;
|
|
8698
8899
|
let res = "";
|
|
8699
|
-
let
|
|
8900
|
+
let join11 = ",";
|
|
8700
8901
|
if (Array.isArray(value)) {
|
|
8701
8902
|
if (value.length === 0) {
|
|
8702
8903
|
return "[]";
|
|
@@ -8709,7 +8910,7 @@ ${originalIndentation}`;
|
|
|
8709
8910
|
indentation += spacer;
|
|
8710
8911
|
res += `
|
|
8711
8912
|
${indentation}`;
|
|
8712
|
-
|
|
8913
|
+
join11 = `,
|
|
8713
8914
|
${indentation}`;
|
|
8714
8915
|
}
|
|
8715
8916
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -8717,13 +8918,13 @@ ${indentation}`;
|
|
|
8717
8918
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8718
8919
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
8719
8920
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
8720
|
-
res +=
|
|
8921
|
+
res += join11;
|
|
8721
8922
|
}
|
|
8722
8923
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
8723
8924
|
res += tmp !== void 0 ? tmp : "null";
|
|
8724
8925
|
if (value.length - 1 > maximumBreadth) {
|
|
8725
8926
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8726
|
-
res += `${
|
|
8927
|
+
res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8727
8928
|
}
|
|
8728
8929
|
if (spacer !== "") {
|
|
8729
8930
|
res += `
|
|
@@ -8736,7 +8937,7 @@ ${originalIndentation}`;
|
|
|
8736
8937
|
let whitespace = "";
|
|
8737
8938
|
if (spacer !== "") {
|
|
8738
8939
|
indentation += spacer;
|
|
8739
|
-
|
|
8940
|
+
join11 = `,
|
|
8740
8941
|
${indentation}`;
|
|
8741
8942
|
whitespace = " ";
|
|
8742
8943
|
}
|
|
@@ -8745,7 +8946,7 @@ ${indentation}`;
|
|
|
8745
8946
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
8746
8947
|
if (tmp !== void 0) {
|
|
8747
8948
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
8748
|
-
separator =
|
|
8949
|
+
separator = join11;
|
|
8749
8950
|
}
|
|
8750
8951
|
}
|
|
8751
8952
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -8803,20 +9004,20 @@ ${originalIndentation}`;
|
|
|
8803
9004
|
indentation += spacer;
|
|
8804
9005
|
let res2 = `
|
|
8805
9006
|
${indentation}`;
|
|
8806
|
-
const
|
|
9007
|
+
const join12 = `,
|
|
8807
9008
|
${indentation}`;
|
|
8808
9009
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
8809
9010
|
let i = 0;
|
|
8810
9011
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8811
9012
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
8812
9013
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
8813
|
-
res2 +=
|
|
9014
|
+
res2 += join12;
|
|
8814
9015
|
}
|
|
8815
9016
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
8816
9017
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
8817
9018
|
if (value.length - 1 > maximumBreadth) {
|
|
8818
9019
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8819
|
-
res2 += `${
|
|
9020
|
+
res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8820
9021
|
}
|
|
8821
9022
|
res2 += `
|
|
8822
9023
|
${originalIndentation}`;
|
|
@@ -8832,16 +9033,16 @@ ${originalIndentation}`;
|
|
|
8832
9033
|
return '"[Object]"';
|
|
8833
9034
|
}
|
|
8834
9035
|
indentation += spacer;
|
|
8835
|
-
const
|
|
9036
|
+
const join11 = `,
|
|
8836
9037
|
${indentation}`;
|
|
8837
9038
|
let res = "";
|
|
8838
9039
|
let separator = "";
|
|
8839
9040
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
8840
9041
|
if (isTypedArrayWithEntries(value)) {
|
|
8841
|
-
res += stringifyTypedArray(value,
|
|
9042
|
+
res += stringifyTypedArray(value, join11, maximumBreadth);
|
|
8842
9043
|
keys = keys.slice(value.length);
|
|
8843
9044
|
maximumPropertiesToStringify -= value.length;
|
|
8844
|
-
separator =
|
|
9045
|
+
separator = join11;
|
|
8845
9046
|
}
|
|
8846
9047
|
if (deterministic) {
|
|
8847
9048
|
keys = sort(keys, comparator);
|
|
@@ -8852,13 +9053,13 @@ ${indentation}`;
|
|
|
8852
9053
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
8853
9054
|
if (tmp !== void 0) {
|
|
8854
9055
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
8855
|
-
separator =
|
|
9056
|
+
separator = join11;
|
|
8856
9057
|
}
|
|
8857
9058
|
}
|
|
8858
9059
|
if (keyLength > maximumBreadth) {
|
|
8859
9060
|
const removedKeys = keyLength - maximumBreadth;
|
|
8860
9061
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
8861
|
-
separator =
|
|
9062
|
+
separator = join11;
|
|
8862
9063
|
}
|
|
8863
9064
|
if (separator !== "") {
|
|
8864
9065
|
res = `
|
|
@@ -9949,11 +10150,11 @@ var init_lib = __esm({
|
|
|
9949
10150
|
}
|
|
9950
10151
|
}
|
|
9951
10152
|
},
|
|
9952
|
-
addToPath: function addToPath(
|
|
9953
|
-
var last =
|
|
10153
|
+
addToPath: function addToPath(path38, added, removed, oldPosInc, options) {
|
|
10154
|
+
var last = path38.lastComponent;
|
|
9954
10155
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
9955
10156
|
return {
|
|
9956
|
-
oldPos:
|
|
10157
|
+
oldPos: path38.oldPos + oldPosInc,
|
|
9957
10158
|
lastComponent: {
|
|
9958
10159
|
count: last.count + 1,
|
|
9959
10160
|
added,
|
|
@@ -9963,7 +10164,7 @@ var init_lib = __esm({
|
|
|
9963
10164
|
};
|
|
9964
10165
|
} else {
|
|
9965
10166
|
return {
|
|
9966
|
-
oldPos:
|
|
10167
|
+
oldPos: path38.oldPos + oldPosInc,
|
|
9967
10168
|
lastComponent: {
|
|
9968
10169
|
count: 1,
|
|
9969
10170
|
added,
|
|
@@ -10021,7 +10222,7 @@ var init_lib = __esm({
|
|
|
10021
10222
|
tokenize: function tokenize(value) {
|
|
10022
10223
|
return Array.from(value);
|
|
10023
10224
|
},
|
|
10024
|
-
join: function
|
|
10225
|
+
join: function join4(chars) {
|
|
10025
10226
|
return chars.join("");
|
|
10026
10227
|
},
|
|
10027
10228
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -10394,10 +10595,10 @@ function attachmentToHistoryMessage(o, ts) {
|
|
|
10394
10595
|
const memories = raw.map((m2) => {
|
|
10395
10596
|
if (!m2 || typeof m2 !== "object") return null;
|
|
10396
10597
|
const rec = m2;
|
|
10397
|
-
const
|
|
10598
|
+
const path38 = typeof rec.path === "string" ? rec.path : null;
|
|
10398
10599
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
10399
|
-
if (!
|
|
10400
|
-
const entry = { path:
|
|
10600
|
+
if (!path38 || content == null) return null;
|
|
10601
|
+
const entry = { path: path38, content };
|
|
10401
10602
|
if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
|
|
10402
10603
|
return entry;
|
|
10403
10604
|
}).filter((m2) => m2 !== null);
|
|
@@ -10857,6 +11058,27 @@ var init_claude_history = __esm({
|
|
|
10857
11058
|
}
|
|
10858
11059
|
});
|
|
10859
11060
|
|
|
11061
|
+
// src/tools/sandbox.ts
|
|
11062
|
+
function shouldSandbox(cwd, personaRoot) {
|
|
11063
|
+
if (!personaRoot) return false;
|
|
11064
|
+
const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
|
|
11065
|
+
return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
|
|
11066
|
+
}
|
|
11067
|
+
function inferSandboxSettingsPath(cwd, personaRoot) {
|
|
11068
|
+
if (!shouldSandbox(cwd, personaRoot)) return null;
|
|
11069
|
+
const rel = path12.relative(personaRoot, cwd);
|
|
11070
|
+
const personaId = rel.split(path12.sep)[0];
|
|
11071
|
+
if (!personaId) return null;
|
|
11072
|
+
return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
|
|
11073
|
+
}
|
|
11074
|
+
var path12;
|
|
11075
|
+
var init_sandbox = __esm({
|
|
11076
|
+
"src/tools/sandbox.ts"() {
|
|
11077
|
+
"use strict";
|
|
11078
|
+
path12 = __toESM(require("path"), 1);
|
|
11079
|
+
}
|
|
11080
|
+
});
|
|
11081
|
+
|
|
10860
11082
|
// src/tools/claude.ts
|
|
10861
11083
|
function macOSDesktopCandidates(home) {
|
|
10862
11084
|
return [
|
|
@@ -10906,7 +11128,7 @@ function buildSpawnArgs(ctx) {
|
|
|
10906
11128
|
];
|
|
10907
11129
|
if (ctx.model) args.push("--model", ctx.model);
|
|
10908
11130
|
switch (ctx.personaMode) {
|
|
10909
|
-
case "
|
|
11131
|
+
case "guest":
|
|
10910
11132
|
args.push("--setting-sources", "project,local");
|
|
10911
11133
|
break;
|
|
10912
11134
|
case "owner":
|
|
@@ -10921,7 +11143,8 @@ function buildSpawnArgs(ctx) {
|
|
|
10921
11143
|
throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
|
|
10922
11144
|
}
|
|
10923
11145
|
}
|
|
10924
|
-
|
|
11146
|
+
const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
|
|
11147
|
+
if (sandboxSettings) args.push("--settings", sandboxSettings);
|
|
10925
11148
|
if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
|
|
10926
11149
|
if (ctx.effort) args.push("--effort", ctx.effort);
|
|
10927
11150
|
if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
|
|
@@ -11201,10 +11424,10 @@ function parseAttachment(obj) {
|
|
|
11201
11424
|
const memories = raw.map((m2) => {
|
|
11202
11425
|
if (!m2 || typeof m2 !== "object") return null;
|
|
11203
11426
|
const rec = m2;
|
|
11204
|
-
const
|
|
11427
|
+
const path38 = typeof rec.path === "string" ? rec.path : null;
|
|
11205
11428
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
11206
|
-
if (!
|
|
11207
|
-
const out = { path:
|
|
11429
|
+
if (!path38 || content == null) return null;
|
|
11430
|
+
const out = { path: path38, content };
|
|
11208
11431
|
if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
|
|
11209
11432
|
return out;
|
|
11210
11433
|
}).filter((m2) => m2 !== null);
|
|
@@ -11320,6 +11543,7 @@ var init_claude = __esm({
|
|
|
11320
11543
|
init_protocol();
|
|
11321
11544
|
init_claude_history();
|
|
11322
11545
|
init_tool_result_extra();
|
|
11546
|
+
init_sandbox();
|
|
11323
11547
|
ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
|
|
11324
11548
|
"hook_additional_context",
|
|
11325
11549
|
"hook_success",
|
|
@@ -18702,7 +18926,7 @@ var require_websocket = __commonJS({
|
|
|
18702
18926
|
var http2 = require("http");
|
|
18703
18927
|
var net = require("net");
|
|
18704
18928
|
var tls = require("tls");
|
|
18705
|
-
var { randomBytes, createHash } = require("crypto");
|
|
18929
|
+
var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
|
|
18706
18930
|
var { Duplex, Readable: Readable3 } = require("stream");
|
|
18707
18931
|
var { URL: URL2 } = require("url");
|
|
18708
18932
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -19232,7 +19456,7 @@ var require_websocket = __commonJS({
|
|
|
19232
19456
|
}
|
|
19233
19457
|
}
|
|
19234
19458
|
const defaultPort = isSecure ? 443 : 80;
|
|
19235
|
-
const key =
|
|
19459
|
+
const key = randomBytes2(16).toString("base64");
|
|
19236
19460
|
const request = isSecure ? https.request : http2.request;
|
|
19237
19461
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
19238
19462
|
let perMessageDeflate;
|
|
@@ -19362,7 +19586,7 @@ var require_websocket = __commonJS({
|
|
|
19362
19586
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
19363
19587
|
return;
|
|
19364
19588
|
}
|
|
19365
|
-
const digest =
|
|
19589
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
19366
19590
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
19367
19591
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
19368
19592
|
return;
|
|
@@ -19729,7 +19953,7 @@ var require_websocket_server = __commonJS({
|
|
|
19729
19953
|
var EventEmitter2 = require("events");
|
|
19730
19954
|
var http2 = require("http");
|
|
19731
19955
|
var { Duplex } = require("stream");
|
|
19732
|
-
var { createHash } = require("crypto");
|
|
19956
|
+
var { createHash: createHash3 } = require("crypto");
|
|
19733
19957
|
var extension2 = require_extension();
|
|
19734
19958
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
19735
19959
|
var subprotocol2 = require_subprotocol();
|
|
@@ -20030,7 +20254,7 @@ var require_websocket_server = __commonJS({
|
|
|
20030
20254
|
);
|
|
20031
20255
|
}
|
|
20032
20256
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
20033
|
-
const digest =
|
|
20257
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
20034
20258
|
const headers = [
|
|
20035
20259
|
"HTTP/1.1 101 Switching Protocols",
|
|
20036
20260
|
"Upgrade: websocket",
|
|
@@ -20118,7 +20342,7 @@ var require_websocket_server = __commonJS({
|
|
|
20118
20342
|
// src/run-case/recorder.ts
|
|
20119
20343
|
function startRunCaseRecorder(opts) {
|
|
20120
20344
|
const now = opts.now ?? Date.now;
|
|
20121
|
-
const dir =
|
|
20345
|
+
const dir = import_node_path27.default.dirname(opts.recordPath);
|
|
20122
20346
|
let stream = null;
|
|
20123
20347
|
let closing = false;
|
|
20124
20348
|
let closedSettled = false;
|
|
@@ -20158,12 +20382,12 @@ function startRunCaseRecorder(opts) {
|
|
|
20158
20382
|
};
|
|
20159
20383
|
return { tap, close, closed };
|
|
20160
20384
|
}
|
|
20161
|
-
var import_node_fs25,
|
|
20385
|
+
var import_node_fs25, import_node_path27;
|
|
20162
20386
|
var init_recorder = __esm({
|
|
20163
20387
|
"src/run-case/recorder.ts"() {
|
|
20164
20388
|
"use strict";
|
|
20165
20389
|
import_node_fs25 = __toESM(require("fs"), 1);
|
|
20166
|
-
|
|
20390
|
+
import_node_path27 = __toESM(require("path"), 1);
|
|
20167
20391
|
}
|
|
20168
20392
|
});
|
|
20169
20393
|
|
|
@@ -20206,7 +20430,7 @@ var init_wire = __esm({
|
|
|
20206
20430
|
// src/run-case/controller.ts
|
|
20207
20431
|
async function runController(opts) {
|
|
20208
20432
|
const now = opts.now ?? Date.now;
|
|
20209
|
-
const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(
|
|
20433
|
+
const cwd = opts.cwd ?? (0, import_node_fs26.mkdtempSync)(import_node_path28.default.join(import_node_os14.default.tmpdir(), "clawd-runcase-"));
|
|
20210
20434
|
const ownsCwd = opts.cwd === void 0;
|
|
20211
20435
|
const recorder = startRunCaseRecorder({ recordPath: opts.record, now });
|
|
20212
20436
|
const spawnCtx = { cwd };
|
|
@@ -20373,13 +20597,13 @@ async function runController(opts) {
|
|
|
20373
20597
|
}
|
|
20374
20598
|
return exitCode ?? 0;
|
|
20375
20599
|
}
|
|
20376
|
-
var import_node_fs26, import_node_os14,
|
|
20600
|
+
var import_node_fs26, import_node_os14, import_node_path28;
|
|
20377
20601
|
var init_controller = __esm({
|
|
20378
20602
|
"src/run-case/controller.ts"() {
|
|
20379
20603
|
"use strict";
|
|
20380
20604
|
import_node_fs26 = require("fs");
|
|
20381
20605
|
import_node_os14 = __toESM(require("os"), 1);
|
|
20382
|
-
|
|
20606
|
+
import_node_path28 = __toESM(require("path"), 1);
|
|
20383
20607
|
init_claude();
|
|
20384
20608
|
init_stdout_splitter();
|
|
20385
20609
|
init_permission_stdio();
|
|
@@ -20611,7 +20835,7 @@ Env (advanced):
|
|
|
20611
20835
|
`;
|
|
20612
20836
|
|
|
20613
20837
|
// src/index.ts
|
|
20614
|
-
var
|
|
20838
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
20615
20839
|
var import_node_fs24 = __toESM(require("fs"), 1);
|
|
20616
20840
|
|
|
20617
20841
|
// src/logger.ts
|
|
@@ -20649,6 +20873,9 @@ function createLogger(opts = {}) {
|
|
|
20649
20873
|
return wrap(base);
|
|
20650
20874
|
}
|
|
20651
20875
|
|
|
20876
|
+
// src/session/store-factory.ts
|
|
20877
|
+
var path5 = __toESM(require("path"), 1);
|
|
20878
|
+
|
|
20652
20879
|
// src/session/store.ts
|
|
20653
20880
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
20654
20881
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -20657,10 +20884,14 @@ init_protocol();
|
|
|
20657
20884
|
// src/session/scope.ts
|
|
20658
20885
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
20659
20886
|
function scopeKey(scope) {
|
|
20660
|
-
|
|
20887
|
+
if (scope.kind === "default") return "default";
|
|
20888
|
+
if (scope.mode === "guest") return `persona:${scope.personaId}:guest:${scope.capId}`;
|
|
20889
|
+
return `persona:${scope.personaId}:${scope.mode}`;
|
|
20661
20890
|
}
|
|
20662
20891
|
function scopeSubPath(scope) {
|
|
20663
|
-
|
|
20892
|
+
if (scope.kind === "default") return ["default"];
|
|
20893
|
+
if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
|
|
20894
|
+
return [scope.personaId, scope.mode];
|
|
20664
20895
|
}
|
|
20665
20896
|
function metaFromScope(scope, personaRoot) {
|
|
20666
20897
|
if (scope.kind === "default") return void 0;
|
|
@@ -20669,10 +20900,18 @@ function metaFromScope(scope, personaRoot) {
|
|
|
20669
20900
|
}
|
|
20670
20901
|
return {
|
|
20671
20902
|
idleKillEnabled: true,
|
|
20672
|
-
personaMode: "
|
|
20903
|
+
personaMode: "guest",
|
|
20673
20904
|
extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json")
|
|
20674
20905
|
};
|
|
20675
20906
|
}
|
|
20907
|
+
function scopeForFile(file, ownerPrincipalId) {
|
|
20908
|
+
if (!file.ownerPersonaId) return { kind: "default" };
|
|
20909
|
+
const creator = file.creatorPrincipalId;
|
|
20910
|
+
if (ownerPrincipalId && creator && creator !== ownerPrincipalId) {
|
|
20911
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "guest", capId: creator };
|
|
20912
|
+
}
|
|
20913
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
|
|
20914
|
+
}
|
|
20676
20915
|
|
|
20677
20916
|
// src/session/store.ts
|
|
20678
20917
|
function safeFileName(sessionId) {
|
|
@@ -20686,12 +20925,16 @@ function safeFileName(sessionId) {
|
|
|
20686
20925
|
var SessionStore = class {
|
|
20687
20926
|
root;
|
|
20688
20927
|
constructor(opts) {
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20692
|
-
"
|
|
20693
|
-
|
|
20694
|
-
|
|
20928
|
+
if ("root" in opts) {
|
|
20929
|
+
this.root = opts.root;
|
|
20930
|
+
} else {
|
|
20931
|
+
const scope = opts.scope ?? { kind: "default" };
|
|
20932
|
+
this.root = import_node_path4.default.join(
|
|
20933
|
+
opts.dataDir,
|
|
20934
|
+
"sessions",
|
|
20935
|
+
...scopeSubPath(scope).map(safeFileName)
|
|
20936
|
+
);
|
|
20937
|
+
}
|
|
20695
20938
|
}
|
|
20696
20939
|
filePath(sessionId) {
|
|
20697
20940
|
return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
|
|
@@ -20756,6 +20999,66 @@ var SessionStore = class {
|
|
|
20756
20999
|
}
|
|
20757
21000
|
};
|
|
20758
21001
|
|
|
21002
|
+
// src/session/store-factory.ts
|
|
21003
|
+
var SessionStoreFactory = class {
|
|
21004
|
+
dataDir;
|
|
21005
|
+
bareStore = null;
|
|
21006
|
+
vmOwnerStores = /* @__PURE__ */ new Map();
|
|
21007
|
+
// vmGuest 索引 key = `${pid}::${capId}`(按 cap.id 隔离,一张 cap 一个目录)
|
|
21008
|
+
vmGuestStores = /* @__PURE__ */ new Map();
|
|
21009
|
+
constructor(opts) {
|
|
21010
|
+
this.dataDir = opts.dataDir;
|
|
21011
|
+
}
|
|
21012
|
+
// ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
|
|
21013
|
+
bareRoot() {
|
|
21014
|
+
return path5.join(this.dataDir, "sessions");
|
|
21015
|
+
}
|
|
21016
|
+
vmOwnerRoot(personaId) {
|
|
21017
|
+
return path5.join(
|
|
21018
|
+
this.dataDir,
|
|
21019
|
+
"personas",
|
|
21020
|
+
safeFileName(personaId),
|
|
21021
|
+
".clawd",
|
|
21022
|
+
"sessions",
|
|
21023
|
+
"owner"
|
|
21024
|
+
);
|
|
21025
|
+
}
|
|
21026
|
+
vmGuestRoot(personaId, capId) {
|
|
21027
|
+
return path5.join(
|
|
21028
|
+
this.dataDir,
|
|
21029
|
+
"personas",
|
|
21030
|
+
safeFileName(personaId),
|
|
21031
|
+
".clawd",
|
|
21032
|
+
"sessions",
|
|
21033
|
+
"guests",
|
|
21034
|
+
safeFileName(capId)
|
|
21035
|
+
);
|
|
21036
|
+
}
|
|
21037
|
+
// ---- SessionStore 工厂(缓存) ----
|
|
21038
|
+
forBare() {
|
|
21039
|
+
if (!this.bareStore) {
|
|
21040
|
+
this.bareStore = new SessionStore({ root: this.bareRoot() });
|
|
21041
|
+
}
|
|
21042
|
+
return this.bareStore;
|
|
21043
|
+
}
|
|
21044
|
+
forVmOwner(personaId) {
|
|
21045
|
+
const key = personaId;
|
|
21046
|
+
const cached = this.vmOwnerStores.get(key);
|
|
21047
|
+
if (cached) return cached;
|
|
21048
|
+
const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
|
|
21049
|
+
this.vmOwnerStores.set(key, st);
|
|
21050
|
+
return st;
|
|
21051
|
+
}
|
|
21052
|
+
forVmGuest(personaId, capId) {
|
|
21053
|
+
const key = `${personaId}::${capId}`;
|
|
21054
|
+
const cached = this.vmGuestStores.get(key);
|
|
21055
|
+
if (cached) return cached;
|
|
21056
|
+
const st = new SessionStore({ root: this.vmGuestRoot(personaId, capId) });
|
|
21057
|
+
this.vmGuestStores.set(key, st);
|
|
21058
|
+
return st;
|
|
21059
|
+
}
|
|
21060
|
+
};
|
|
21061
|
+
|
|
20759
21062
|
// src/session/manager.ts
|
|
20760
21063
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
20761
21064
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
@@ -20875,7 +21178,7 @@ function cloneState(s) {
|
|
|
20875
21178
|
subSessionMeta: s.subSessionMeta
|
|
20876
21179
|
};
|
|
20877
21180
|
}
|
|
20878
|
-
var IDLE_KILL_DELAY_MS =
|
|
21181
|
+
var IDLE_KILL_DELAY_MS = 6e4;
|
|
20879
21182
|
var SEEN_UUID_CAP = 2e3;
|
|
20880
21183
|
function applyEventBatch(state, events, deps) {
|
|
20881
21184
|
if (events.length === 0) return { state, effects: [] };
|
|
@@ -20915,7 +21218,10 @@ function buildSpawnContext(state, deps) {
|
|
|
20915
21218
|
toolSessionId: file.toolSessionId,
|
|
20916
21219
|
model: file.model,
|
|
20917
21220
|
permissionMode: file.permissionMode,
|
|
20918
|
-
effort: file.effort
|
|
21221
|
+
effort: file.effort,
|
|
21222
|
+
// Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
|
|
21223
|
+
// 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
|
|
21224
|
+
personaRoot: deps.personaRoot
|
|
20919
21225
|
};
|
|
20920
21226
|
const meta = state.subSessionMeta;
|
|
20921
21227
|
if (meta?.personaMode) {
|
|
@@ -21642,7 +21948,8 @@ var SessionRunner = class {
|
|
|
21642
21948
|
now: this.hooks.now ?? Date.now,
|
|
21643
21949
|
resolveContextWindow: this.hooks.resolveContextWindow,
|
|
21644
21950
|
genUuid: this.hooks.genUuid ?? v4_default,
|
|
21645
|
-
ownerDisplayName: this.hooks.ownerDisplayName
|
|
21951
|
+
ownerDisplayName: this.hooks.ownerDisplayName,
|
|
21952
|
+
personaRoot: this.hooks.personaRoot
|
|
21646
21953
|
};
|
|
21647
21954
|
const { state, effects } = reduceSession(this.state, inputMsg, deps);
|
|
21648
21955
|
this.state = state;
|
|
@@ -22066,9 +22373,18 @@ var SessionManager = class {
|
|
|
22066
22373
|
attachObserver(observer) {
|
|
22067
22374
|
this.attachedObserver = observer;
|
|
22068
22375
|
}
|
|
22069
|
-
// 按 scope 拿对应的 SessionStore
|
|
22070
|
-
//
|
|
22376
|
+
// 按 scope 拿对应的 SessionStore.
|
|
22377
|
+
// Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
|
|
22378
|
+
// default → factory.forBare() <dataDir>/sessions/
|
|
22379
|
+
// persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
|
|
22380
|
+
// persona/<pid>/guest → factory.forVmGuest(pid, capId) <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
|
|
22381
|
+
// 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
|
|
22071
22382
|
storeFor(scope) {
|
|
22383
|
+
if (this.deps.storeFactory) {
|
|
22384
|
+
if (scope.kind === "default") return this.deps.storeFactory.forBare();
|
|
22385
|
+
if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
|
|
22386
|
+
return this.deps.storeFactory.forVmGuest(scope.personaId, scope.capId);
|
|
22387
|
+
}
|
|
22072
22388
|
if (scope.kind === "default") return this.deps.store;
|
|
22073
22389
|
const key = scopeKey(scope);
|
|
22074
22390
|
const cached = this.storesByScope.get(key);
|
|
@@ -22088,19 +22404,18 @@ var SessionManager = class {
|
|
|
22088
22404
|
this.capabilitiesCache.set(tool, caps);
|
|
22089
22405
|
return caps;
|
|
22090
22406
|
}
|
|
22091
|
-
// 由 SessionFile
|
|
22092
|
-
//
|
|
22093
|
-
// session 出身(不可变);此函数只用于 SessionStore 的写入路由,不参与运行时行为
|
|
22094
|
-
// 决策(personaMode='owner' 等由路径 → scope → metaFromScope 派生)。
|
|
22095
|
-
// 仅覆盖 owner + default 两类——listener sub-session 不会从 default RPC 路径进来。
|
|
22407
|
+
// 由 SessionFile 反推 scope 的 thin wrapper —— 决策矩阵收敛到 scope.scopeForFile
|
|
22408
|
+
// free function(scope.spec.ts transition 表驱动测试),manager 只负责注入 deps.
|
|
22096
22409
|
scopeForFile(file) {
|
|
22097
|
-
return file
|
|
22410
|
+
return scopeForFile(file, this.deps.ownerPrincipalId);
|
|
22098
22411
|
}
|
|
22099
|
-
//
|
|
22100
|
-
//
|
|
22412
|
+
// 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned 跨 scope 查询.
|
|
22413
|
+
// Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
|
|
22414
|
+
// 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
|
|
22415
|
+
// <dataDir>/sessions/ 列子目录 (排除 'default').
|
|
22101
22416
|
listPersonaIdsOnDisk() {
|
|
22102
22417
|
if (!this.deps.dataDir) return [];
|
|
22103
|
-
const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
|
|
22418
|
+
const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
|
|
22104
22419
|
let entries;
|
|
22105
22420
|
try {
|
|
22106
22421
|
entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
|
|
@@ -22111,60 +22426,79 @@ var SessionManager = class {
|
|
|
22111
22426
|
}
|
|
22112
22427
|
return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
|
|
22113
22428
|
}
|
|
22114
|
-
//
|
|
22115
|
-
//
|
|
22116
|
-
//
|
|
22117
|
-
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
|
|
22121
|
-
|
|
22122
|
-
|
|
22123
|
-
|
|
22124
|
-
|
|
22429
|
+
// 扫某 persona 下 guests/ 子目录, 拿所有 capId.
|
|
22430
|
+
// 路径: <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
|
|
22431
|
+
// 只在 storeFactory 注入 (新布局) 下生效, 老布局无 guest 目录.
|
|
22432
|
+
listGuestCapIdsForPersona(personaId) {
|
|
22433
|
+
if (!this.deps.dataDir || !this.deps.storeFactory) return [];
|
|
22434
|
+
const root = import_node_path6.default.join(
|
|
22435
|
+
this.deps.dataDir,
|
|
22436
|
+
"personas",
|
|
22437
|
+
personaId,
|
|
22438
|
+
".clawd",
|
|
22439
|
+
"sessions",
|
|
22440
|
+
"guests"
|
|
22441
|
+
);
|
|
22442
|
+
let entries;
|
|
22443
|
+
try {
|
|
22444
|
+
entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
|
|
22445
|
+
} catch (err) {
|
|
22446
|
+
const code = err?.code;
|
|
22447
|
+
if (code === "ENOENT") return [];
|
|
22448
|
+
throw err;
|
|
22449
|
+
}
|
|
22450
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
22451
|
+
}
|
|
22452
|
+
// owned 类目下按 sessionId 找文件——前端只传 sessionId 不带 scope.
|
|
22453
|
+
// 扫盘顺序: default → 每个 persona 的 owner/ → 每个 persona 的 guests/<capId>/.
|
|
22125
22454
|
findOwnedSession(sessionId) {
|
|
22126
|
-
const runner = this.runners.get(sessionId);
|
|
22127
|
-
if (runner) return runner.getState().file;
|
|
22128
22455
|
const dflt = this.deps.store.read(sessionId);
|
|
22129
22456
|
if (dflt) return dflt;
|
|
22130
22457
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22131
22458
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22132
|
-
const
|
|
22133
|
-
if (
|
|
22459
|
+
const found = ownerStore.read(sessionId);
|
|
22460
|
+
if (found) return found;
|
|
22461
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22462
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22463
|
+
const g2 = guestStore.read(sessionId);
|
|
22464
|
+
if (g2) return g2;
|
|
22465
|
+
}
|
|
22134
22466
|
}
|
|
22135
22467
|
return null;
|
|
22136
22468
|
}
|
|
22137
|
-
//
|
|
22138
|
-
//
|
|
22139
|
-
//
|
|
22140
|
-
findOwnedSessionScope(sessionId) {
|
|
22141
|
-
const file = this.findOwnedSession(sessionId);
|
|
22142
|
-
return file ? this.scopeForFile(file) : null;
|
|
22143
|
-
}
|
|
22144
|
-
// 合并 default + 所有 persona owner 的 SessionFile —— 桌面 App 主 session 列表入口。
|
|
22145
|
-
// 同样不含 listener sub-session(listener 走 persona:listSubSessions RPC 单独入口)。
|
|
22469
|
+
// 合并 default + 每个 persona 的 owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
|
|
22470
|
+
// guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
|
|
22471
|
+
// (creatorPrincipalId === capId), 这里仅负责数据源汇总.
|
|
22146
22472
|
listAllOwned() {
|
|
22147
22473
|
const out = [];
|
|
22148
22474
|
out.push(...this.deps.store.list());
|
|
22149
22475
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22150
22476
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22151
22477
|
out.push(...ownerStore.list());
|
|
22478
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22479
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22480
|
+
out.push(...guestStore.list());
|
|
22481
|
+
}
|
|
22152
22482
|
}
|
|
22153
22483
|
return out.sort(
|
|
22154
22484
|
(a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
|
|
22155
22485
|
);
|
|
22156
22486
|
}
|
|
22157
|
-
// 写回 SessionFile,自动根据 ownerPersonaId 派生写入 scope
|
|
22158
|
-
// 调用方原先都是 `deps.store.write(updated)
|
|
22487
|
+
// 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
|
|
22488
|
+
// 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
|
|
22159
22489
|
writeOwned(file) {
|
|
22160
22490
|
return this.storeFor(this.scopeForFile(file)).write(file);
|
|
22161
22491
|
}
|
|
22162
|
-
// 删除 owned SessionFile(default
|
|
22492
|
+
// 删除 owned SessionFile(default / persona owner / persona guest),与 findOwnedSession 对称扫盘.
|
|
22163
22493
|
deleteOwned(sessionId) {
|
|
22164
22494
|
if (this.deps.store.delete(sessionId)) return true;
|
|
22165
22495
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22166
22496
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22167
22497
|
if (ownerStore.delete(sessionId)) return true;
|
|
22498
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22499
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22500
|
+
if (guestStore.delete(sessionId)) return true;
|
|
22501
|
+
}
|
|
22168
22502
|
}
|
|
22169
22503
|
return false;
|
|
22170
22504
|
}
|
|
@@ -22172,8 +22506,8 @@ var SessionManager = class {
|
|
|
22172
22506
|
// 经过 compressFrameForWire 后决定是 push collector 还是走 deps.broadcastFrame。
|
|
22173
22507
|
// scope 决定两件事:SessionStore 路由 + SubSessionMeta 派生(metaFromScope)。
|
|
22174
22508
|
// - default → 复用 deps.store,无 subSessionMeta(普通 session)
|
|
22175
|
-
// - persona/owner →
|
|
22176
|
-
// - persona/
|
|
22509
|
+
// - persona/owner → personas/<pid>/.clawd/sessions/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
|
|
22510
|
+
// - persona/guest → personas/<pid>/.clawd/sessions/guests/<capId>/,meta={idleKillEnabled:true, personaMode:'guest', extraSettings:<sandbox>}
|
|
22177
22511
|
newRunner(file, scope) {
|
|
22178
22512
|
const adapter = this.deps.getAdapter(file.tool ?? "claude");
|
|
22179
22513
|
const store = this.storeFor(scope);
|
|
@@ -22192,6 +22526,9 @@ var SessionManager = class {
|
|
|
22192
22526
|
dataDir: this.deps.dataDir,
|
|
22193
22527
|
personaStore: this.deps.personaStore,
|
|
22194
22528
|
ownerDisplayName: this.deps.ownerDisplayName,
|
|
22529
|
+
// Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
|
|
22530
|
+
// cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
|
|
22531
|
+
personaRoot: this.deps.personaRoot,
|
|
22195
22532
|
// file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
|
|
22196
22533
|
onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
|
|
22197
22534
|
scope,
|
|
@@ -22286,7 +22623,7 @@ var SessionManager = class {
|
|
|
22286
22623
|
}
|
|
22287
22624
|
}
|
|
22288
22625
|
// ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
|
|
22289
|
-
create(args) {
|
|
22626
|
+
create(args, creatorPrincipalId) {
|
|
22290
22627
|
let cwd;
|
|
22291
22628
|
if (args.ownerPersonaId) {
|
|
22292
22629
|
cwd = derivePersonaSpawnCwd(
|
|
@@ -22327,10 +22664,11 @@ var SessionManager = class {
|
|
|
22327
22664
|
iconKey: args.iconKey,
|
|
22328
22665
|
forkedFromSessionId: args.forkedFromSessionId,
|
|
22329
22666
|
ownerPersonaId: args.ownerPersonaId,
|
|
22667
|
+
creatorPrincipalId,
|
|
22330
22668
|
createdAt: iso,
|
|
22331
22669
|
updatedAt: iso
|
|
22332
22670
|
};
|
|
22333
|
-
const scope =
|
|
22671
|
+
const scope = this.scopeForFile(file);
|
|
22334
22672
|
const written = this.storeFor(scope).write(file);
|
|
22335
22673
|
return { response: written, broadcast: [] };
|
|
22336
22674
|
}
|
|
@@ -22755,28 +23093,18 @@ var SessionManager = class {
|
|
|
22755
23093
|
this.runners.set(file.sessionId, r);
|
|
22756
23094
|
return r;
|
|
22757
23095
|
}
|
|
22758
|
-
// ----------------
|
|
23096
|
+
// ---------------- scope-explicit sub-session API ----------------
|
|
22759
23097
|
//
|
|
22760
|
-
//
|
|
22761
|
-
//
|
|
22762
|
-
// -
|
|
22763
|
-
// -
|
|
22764
|
-
// - sessionId 由 PersonaManager 派生(personaId-<tokenHash>),不走自动 newSessionId
|
|
23098
|
+
// 区别于 owner / default 单 sessionId 入口,这些 API 接 SessionScope 参数 ——
|
|
23099
|
+
// 调用方(PersonaManager / observer 等)明确传 scope,由 storeFor 路由到正确目录:
|
|
23100
|
+
// - {kind:'persona', personaId, mode:'guest', capId} → guests/<capId>/,meta={idleKill:true, sandbox}
|
|
23101
|
+
// - {kind:'persona', personaId, mode:'owner'} → owner/,meta={idleKill:false}
|
|
22765
23102
|
//
|
|
22766
|
-
//
|
|
22767
|
-
// {kind:'persona', personaId, mode:'listener'},与 owner / default 分类隔离。
|
|
23103
|
+
// SubSessionMeta 由 scope → metaFromScope 派生,不进 SessionFile schema。
|
|
22768
23104
|
/** 按 scope 读取 SessionFile;不存在返回 null */
|
|
22769
23105
|
readForScope(sessionId, scope) {
|
|
22770
23106
|
return this.storeFor(scope).read(sessionId);
|
|
22771
23107
|
}
|
|
22772
|
-
/**
|
|
22773
|
-
* 列出指定 persona 在指定 mode 下的所有 SessionFile。
|
|
22774
|
-
* persona:listSubSessions RPC 入口(mode='listener');
|
|
22775
|
-
* 路径下还未写过任何 session 时 list() 返回空数组(SessionStore.list 内部 ENOENT 处理)
|
|
22776
|
-
*/
|
|
22777
|
-
listPersonaSessions(personaId, mode) {
|
|
22778
|
-
return this.storeFor({ kind: "persona", personaId, mode }).list();
|
|
22779
|
-
}
|
|
22780
23108
|
/**
|
|
22781
23109
|
* 创建 sub-session 的 SessionFile + 准备 runner(不 spawn)。
|
|
22782
23110
|
* SubSessionMeta 不进 SessionFile schema,运行时由 scope → metaFromScope 派生。
|
|
@@ -22921,7 +23249,7 @@ var SessionManager = class {
|
|
|
22921
23249
|
return { response: { ok: true }, broadcast };
|
|
22922
23250
|
}
|
|
22923
23251
|
/**
|
|
22924
|
-
* 改 SessionFile.label
|
|
23252
|
+
* 改 SessionFile.label(scope-explicit sub-session 重命名入口)。
|
|
22925
23253
|
* 不存在抛 SESSION_NOT_FOUND。
|
|
22926
23254
|
*/
|
|
22927
23255
|
renameForScope(args) {
|
|
@@ -22936,7 +23264,7 @@ var SessionManager = class {
|
|
|
22936
23264
|
store.write({ ...file, label: args.label });
|
|
22937
23265
|
}
|
|
22938
23266
|
/**
|
|
22939
|
-
* 删除
|
|
23267
|
+
* 删除 scope-explicit sub-session。
|
|
22940
23268
|
* - 若 runner 存在:走 reducer 'delete' command 收尾(SIGKILL + 文件 unlink + broadcast)
|
|
22941
23269
|
* - 若 runner 不存在:直接删 scope store 文件 + 广播 session:deleted
|
|
22942
23270
|
* 不存在抛 SESSION_NOT_FOUND。
|
|
@@ -23204,7 +23532,7 @@ var SessionManager = class {
|
|
|
23204
23532
|
|
|
23205
23533
|
// src/persona/store.ts
|
|
23206
23534
|
var fs6 = __toESM(require("fs"), 1);
|
|
23207
|
-
var
|
|
23535
|
+
var path8 = __toESM(require("path"), 1);
|
|
23208
23536
|
init_protocol();
|
|
23209
23537
|
var DEFAULT_SETTINGS = {
|
|
23210
23538
|
permissions: {
|
|
@@ -23233,13 +23561,13 @@ var PersonaStore = class {
|
|
|
23233
23561
|
}
|
|
23234
23562
|
root;
|
|
23235
23563
|
personaDir(personaId) {
|
|
23236
|
-
return
|
|
23564
|
+
return path8.join(this.root, safeFileName(personaId));
|
|
23237
23565
|
}
|
|
23238
23566
|
metaPath(personaId) {
|
|
23239
|
-
return
|
|
23567
|
+
return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
|
|
23240
23568
|
}
|
|
23241
23569
|
claudeMdPath(personaId) {
|
|
23242
|
-
return
|
|
23570
|
+
return path8.join(this.personaDir(personaId), "CLAUDE.md");
|
|
23243
23571
|
}
|
|
23244
23572
|
/**
|
|
23245
23573
|
* Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
|
|
@@ -23249,11 +23577,11 @@ var PersonaStore = class {
|
|
|
23249
23577
|
* 加载 persona 人格,只有 listener 多一层 OS sandbox。
|
|
23250
23578
|
*/
|
|
23251
23579
|
sandboxSettingsPath(personaId) {
|
|
23252
|
-
return
|
|
23580
|
+
return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
|
|
23253
23581
|
}
|
|
23254
23582
|
write(persona, personality) {
|
|
23255
23583
|
const dir = this.personaDir(persona.personaId);
|
|
23256
|
-
fs6.mkdirSync(
|
|
23584
|
+
fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
|
|
23257
23585
|
this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
|
|
23258
23586
|
this.atomicWrite(
|
|
23259
23587
|
this.sandboxSettingsPath(persona.personaId),
|
|
@@ -23271,6 +23599,10 @@ var PersonaStore = class {
|
|
|
23271
23599
|
const p2 = this.metaPath(personaId);
|
|
23272
23600
|
if (!fs6.existsSync(p2)) return null;
|
|
23273
23601
|
const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
|
|
23602
|
+
if (raw && typeof raw === "object" && "tokenMap" in raw) {
|
|
23603
|
+
delete raw.tokenMap;
|
|
23604
|
+
this.atomicWrite(p2, JSON.stringify(raw, null, 2));
|
|
23605
|
+
}
|
|
23274
23606
|
return PersonaFileSchema.parse(raw);
|
|
23275
23607
|
}
|
|
23276
23608
|
has(personaId) {
|
|
@@ -23296,48 +23628,12 @@ var PersonaStore = class {
|
|
|
23296
23628
|
}
|
|
23297
23629
|
/** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
|
|
23298
23630
|
skillsDir(personaId) {
|
|
23299
|
-
return
|
|
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;
|
|
23631
|
+
return path8.join(this.personaDir(personaId), ".claude", "skills");
|
|
23336
23632
|
}
|
|
23337
23633
|
list() {
|
|
23338
23634
|
if (!fs6.existsSync(this.root)) return [];
|
|
23339
23635
|
return fs6.readdirSync(this.root).filter((name) => {
|
|
23340
|
-
return fs6.existsSync(
|
|
23636
|
+
return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
|
|
23341
23637
|
});
|
|
23342
23638
|
}
|
|
23343
23639
|
remove(personaId) {
|
|
@@ -23362,12 +23658,19 @@ var PersonaRegistry = class {
|
|
|
23362
23658
|
}
|
|
23363
23659
|
store;
|
|
23364
23660
|
cache = /* @__PURE__ */ new Map();
|
|
23365
|
-
/**
|
|
23661
|
+
/**
|
|
23662
|
+
* 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
|
|
23663
|
+
* 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
|
|
23664
|
+
* try/catch 隔离,让其它 persona 仍能加载。
|
|
23665
|
+
*/
|
|
23366
23666
|
reload() {
|
|
23367
23667
|
this.cache.clear();
|
|
23368
23668
|
for (const id of this.store.list()) {
|
|
23369
|
-
|
|
23370
|
-
|
|
23669
|
+
try {
|
|
23670
|
+
const p2 = this.store.read(id);
|
|
23671
|
+
if (p2) this.cache.set(p2.personaId, p2);
|
|
23672
|
+
} catch {
|
|
23673
|
+
}
|
|
23371
23674
|
}
|
|
23372
23675
|
}
|
|
23373
23676
|
get(personaId) {
|
|
@@ -23383,21 +23686,6 @@ var PersonaRegistry = class {
|
|
|
23383
23686
|
list() {
|
|
23384
23687
|
return Array.from(this.cache.values());
|
|
23385
23688
|
}
|
|
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
|
-
}
|
|
23401
23689
|
};
|
|
23402
23690
|
|
|
23403
23691
|
// src/persona/manager.ts
|
|
@@ -23691,9 +23979,6 @@ var PersonaManager = class {
|
|
|
23691
23979
|
model: args.model,
|
|
23692
23980
|
public: args.public ?? false,
|
|
23693
23981
|
iconKey: args.iconKey,
|
|
23694
|
-
// 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
|
|
23695
|
-
// file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
|
|
23696
|
-
tokenMap: {},
|
|
23697
23982
|
createdAt: now,
|
|
23698
23983
|
updatedAt: now
|
|
23699
23984
|
};
|
|
@@ -23730,65 +24015,28 @@ var PersonaManager = class {
|
|
|
23730
24015
|
listSkills(personaId) {
|
|
23731
24016
|
return listSkillsForDir(this.deps.store.skillsDir(personaId), "project");
|
|
23732
24017
|
}
|
|
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
|
-
}
|
|
23740
24018
|
/** 读 sandbox-settings.json;不存在 / 损坏返回 null。供 persona:get handler 拼响应。 */
|
|
23741
24019
|
readSandboxSettings(personaId) {
|
|
23742
24020
|
return this.deps.store.readSandboxSettings(personaId);
|
|
23743
24021
|
}
|
|
23744
24022
|
/**
|
|
23745
24023
|
* 删除 persona。
|
|
23746
|
-
* PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/
|
|
24024
|
+
* PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
|
|
24025
|
+
* Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
|
|
24026
|
+
* .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
|
|
24027
|
+
*
|
|
24028
|
+
* ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
|
|
24029
|
+
* ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
|
|
24030
|
+
* 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
|
|
24031
|
+
* 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
|
|
24032
|
+
* 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
|
|
24033
|
+
* force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
|
|
24034
|
+
* "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
|
|
23747
24035
|
*/
|
|
23748
24036
|
delete(personaId) {
|
|
23749
24037
|
this.deps.store.remove(personaId);
|
|
23750
24038
|
this.deps.registry.remove(personaId);
|
|
23751
24039
|
}
|
|
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
|
-
}
|
|
23792
24040
|
/**
|
|
23793
24041
|
* label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
|
|
23794
24042
|
* base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
|
|
@@ -23808,7 +24056,7 @@ var PersonaManager = class {
|
|
|
23808
24056
|
|
|
23809
24057
|
// src/persona/seed.ts
|
|
23810
24058
|
var fs8 = __toESM(require("fs"), 1);
|
|
23811
|
-
var
|
|
24059
|
+
var path10 = __toESM(require("path"), 1);
|
|
23812
24060
|
var import_node_url = require("url");
|
|
23813
24061
|
var import_meta = {};
|
|
23814
24062
|
var DEFAULT_PERSONAS = [
|
|
@@ -23844,14 +24092,14 @@ var DEFAULT_PERSONAS = [
|
|
|
23844
24092
|
function findDefaultsRoot() {
|
|
23845
24093
|
const candidates = [];
|
|
23846
24094
|
try {
|
|
23847
|
-
const here =
|
|
23848
|
-
candidates.push(
|
|
23849
|
-
candidates.push(
|
|
24095
|
+
const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
24096
|
+
candidates.push(path10.resolve(here, "defaults"));
|
|
24097
|
+
candidates.push(path10.resolve(here, "persona-defaults"));
|
|
23850
24098
|
} catch {
|
|
23851
24099
|
}
|
|
23852
24100
|
if (process.argv[1]) {
|
|
23853
|
-
const argvDir =
|
|
23854
|
-
candidates.push(
|
|
24101
|
+
const argvDir = path10.dirname(process.argv[1]);
|
|
24102
|
+
candidates.push(path10.resolve(argvDir, "persona-defaults"));
|
|
23855
24103
|
}
|
|
23856
24104
|
for (const c of candidates) {
|
|
23857
24105
|
try {
|
|
@@ -23868,7 +24116,7 @@ function seedDefaultPersonas(args) {
|
|
|
23868
24116
|
args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
|
|
23869
24117
|
continue;
|
|
23870
24118
|
}
|
|
23871
|
-
const bundleDir =
|
|
24119
|
+
const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
|
|
23872
24120
|
if (!fs8.existsSync(bundleDir)) {
|
|
23873
24121
|
args.logger.warn("persona.seed.skip", {
|
|
23874
24122
|
personaId: entry.personaId,
|
|
@@ -23877,7 +24125,7 @@ function seedDefaultPersonas(args) {
|
|
|
23877
24125
|
});
|
|
23878
24126
|
continue;
|
|
23879
24127
|
}
|
|
23880
|
-
const claudeMdPath =
|
|
24128
|
+
const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
|
|
23881
24129
|
if (!fs8.existsSync(claudeMdPath)) {
|
|
23882
24130
|
args.logger.warn("persona.seed.skip", {
|
|
23883
24131
|
personaId: entry.personaId,
|
|
@@ -23894,7 +24142,6 @@ function seedDefaultPersonas(args) {
|
|
|
23894
24142
|
model: entry.model,
|
|
23895
24143
|
public: entry.public,
|
|
23896
24144
|
iconKey: entry.iconKey,
|
|
23897
|
-
tokenMap: {},
|
|
23898
24145
|
createdAt: now,
|
|
23899
24146
|
updatedAt: now
|
|
23900
24147
|
};
|
|
@@ -23906,8 +24153,8 @@ function seedDefaultPersonas(args) {
|
|
|
23906
24153
|
function copyBundleExtras(srcDir, dstDir) {
|
|
23907
24154
|
for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
|
|
23908
24155
|
if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
|
|
23909
|
-
const srcPath =
|
|
23910
|
-
const dstPath =
|
|
24156
|
+
const srcPath = path10.join(srcDir, entry.name);
|
|
24157
|
+
const dstPath = path10.join(dstDir, entry.name);
|
|
23911
24158
|
if (entry.isDirectory()) {
|
|
23912
24159
|
fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
|
|
23913
24160
|
} else if (entry.isFile()) {
|
|
@@ -25275,6 +25522,7 @@ var import_sender = __toESM(require_sender(), 1);
|
|
|
25275
25522
|
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
25276
25523
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25277
25524
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25525
|
+
var wrapper_default = import_websocket.default;
|
|
25278
25526
|
|
|
25279
25527
|
// src/transport/local-ws-server.ts
|
|
25280
25528
|
var import_node_http = __toESM(require("http"), 1);
|
|
@@ -25289,6 +25537,9 @@ var LocalWsServer = class {
|
|
|
25289
25537
|
httpServer = null;
|
|
25290
25538
|
frameHandler = null;
|
|
25291
25539
|
clients = /* @__PURE__ */ new Map();
|
|
25540
|
+
// Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
|
|
25541
|
+
// 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
|
|
25542
|
+
capabilityIdToClients = /* @__PURE__ */ new Map();
|
|
25292
25543
|
logger;
|
|
25293
25544
|
pingIntervalMs;
|
|
25294
25545
|
async start() {
|
|
@@ -25379,6 +25630,49 @@ var LocalWsServer = class {
|
|
|
25379
25630
|
this.safeSend(c.ws, frame);
|
|
25380
25631
|
}
|
|
25381
25632
|
}
|
|
25633
|
+
// Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
|
|
25634
|
+
// 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
|
|
25635
|
+
// guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
|
|
25636
|
+
broadcastToOwners(frame) {
|
|
25637
|
+
const gate = this.opts.authGate;
|
|
25638
|
+
for (const c of this.clients.values()) {
|
|
25639
|
+
if (gate && !gate.isAuthed(c.handle.id)) continue;
|
|
25640
|
+
if (c.ctx && c.ctx.principal.kind !== "owner") continue;
|
|
25641
|
+
this.safeSend(c.ws, frame);
|
|
25642
|
+
}
|
|
25643
|
+
}
|
|
25644
|
+
// capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
|
|
25645
|
+
// - 所有 owner ws (owner 看自家所有 channel)
|
|
25646
|
+
// - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
|
|
25647
|
+
// 一次调用同时推两端, inbox:event push 帧由此走.
|
|
25648
|
+
broadcastToCapabilityChannel(capabilityId, frame) {
|
|
25649
|
+
this.broadcastToOwners(frame);
|
|
25650
|
+
const set = this.capabilityIdToClients.get(capabilityId);
|
|
25651
|
+
if (!set) return;
|
|
25652
|
+
for (const id of set) {
|
|
25653
|
+
const c = this.clients.get(id);
|
|
25654
|
+
if (!c) continue;
|
|
25655
|
+
this.safeSend(c.ws, frame);
|
|
25656
|
+
}
|
|
25657
|
+
}
|
|
25658
|
+
// Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
|
|
25659
|
+
// principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
|
|
25660
|
+
// principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
|
|
25661
|
+
// principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
|
|
25662
|
+
// 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
|
|
25663
|
+
broadcastToPrincipal(principalId, frame) {
|
|
25664
|
+
if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
|
|
25665
|
+
this.broadcastToOwners(frame);
|
|
25666
|
+
return;
|
|
25667
|
+
}
|
|
25668
|
+
const set = this.capabilityIdToClients.get(principalId);
|
|
25669
|
+
if (!set) return;
|
|
25670
|
+
for (const id of set) {
|
|
25671
|
+
const c = this.clients.get(id);
|
|
25672
|
+
if (!c) continue;
|
|
25673
|
+
this.safeSend(c.ws, frame);
|
|
25674
|
+
}
|
|
25675
|
+
}
|
|
25382
25676
|
firstSubscriber(sessionId) {
|
|
25383
25677
|
for (const c of this.clients.values()) {
|
|
25384
25678
|
if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
|
|
@@ -25400,6 +25694,40 @@ var LocalWsServer = class {
|
|
|
25400
25694
|
if (!c) return;
|
|
25401
25695
|
this.safeSend(c.ws, frame);
|
|
25402
25696
|
}
|
|
25697
|
+
// Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
|
|
25698
|
+
// 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
|
|
25699
|
+
attachClientContext(clientId, ctx) {
|
|
25700
|
+
const c = this.clients.get(clientId);
|
|
25701
|
+
if (!c) return;
|
|
25702
|
+
c.ctx = ctx;
|
|
25703
|
+
if (ctx.capabilityId) {
|
|
25704
|
+
let set = this.capabilityIdToClients.get(ctx.capabilityId);
|
|
25705
|
+
if (!set) {
|
|
25706
|
+
set = /* @__PURE__ */ new Set();
|
|
25707
|
+
this.capabilityIdToClients.set(ctx.capabilityId, set);
|
|
25708
|
+
}
|
|
25709
|
+
set.add(clientId);
|
|
25710
|
+
}
|
|
25711
|
+
}
|
|
25712
|
+
// 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
|
|
25713
|
+
getClientContext(clientId) {
|
|
25714
|
+
return this.clients.get(clientId)?.ctx ?? null;
|
|
25715
|
+
}
|
|
25716
|
+
// Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
|
|
25717
|
+
// 让客户端识别 'capability revoked' 而非普通断连。
|
|
25718
|
+
closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
|
|
25719
|
+
const set = this.capabilityIdToClients.get(capabilityId);
|
|
25720
|
+
if (!set) return;
|
|
25721
|
+
const ids = [...set];
|
|
25722
|
+
for (const id of ids) {
|
|
25723
|
+
const c = this.clients.get(id);
|
|
25724
|
+
if (!c) continue;
|
|
25725
|
+
try {
|
|
25726
|
+
c.ws.close(code, reason);
|
|
25727
|
+
} catch {
|
|
25728
|
+
}
|
|
25729
|
+
}
|
|
25730
|
+
}
|
|
25403
25731
|
// URL path 路由:'/' → 主连接路径;其他 → close 4404
|
|
25404
25732
|
routeConnection(socket, req) {
|
|
25405
25733
|
const remoteAddress = req?.socket?.remoteAddress;
|
|
@@ -25434,7 +25762,7 @@ var LocalWsServer = class {
|
|
|
25434
25762
|
} catch {
|
|
25435
25763
|
}
|
|
25436
25764
|
}, this.pingIntervalMs);
|
|
25437
|
-
this.clients.set(id, { handle, ws: socket, pingTimer });
|
|
25765
|
+
this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
|
|
25438
25766
|
this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
|
|
25439
25767
|
const authGate = this.opts.authGate;
|
|
25440
25768
|
let authed = true;
|
|
@@ -25458,6 +25786,12 @@ var LocalWsServer = class {
|
|
|
25458
25786
|
socket.on("close", () => {
|
|
25459
25787
|
const c = this.clients.get(id);
|
|
25460
25788
|
if (c?.pingTimer) clearInterval(c.pingTimer);
|
|
25789
|
+
const capId = c?.ctx?.capabilityId;
|
|
25790
|
+
if (capId) {
|
|
25791
|
+
const set = this.capabilityIdToClients.get(capId);
|
|
25792
|
+
set?.delete(id);
|
|
25793
|
+
if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
|
|
25794
|
+
}
|
|
25461
25795
|
this.clients.delete(id);
|
|
25462
25796
|
authGate?.unregister(id);
|
|
25463
25797
|
this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
|
|
@@ -25490,6 +25824,17 @@ var LocalWsServer = class {
|
|
|
25490
25824
|
return;
|
|
25491
25825
|
}
|
|
25492
25826
|
} else if (frame.type === "auth") {
|
|
25827
|
+
const token = typeof frame.token === "string" ? frame.token ?? "" : "";
|
|
25828
|
+
const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
|
|
25829
|
+
const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
|
|
25830
|
+
if (token && this.opts.tryVerifyCapabilityToken) {
|
|
25831
|
+
const guestCtx = this.opts.tryVerifyCapabilityToken(
|
|
25832
|
+
token,
|
|
25833
|
+
selfPrincipalId || void 0,
|
|
25834
|
+
selfDisplayName || void 0
|
|
25835
|
+
);
|
|
25836
|
+
if (guestCtx) this.attachClientContext(client.id, guestCtx);
|
|
25837
|
+
}
|
|
25493
25838
|
this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
|
|
25494
25839
|
return;
|
|
25495
25840
|
}
|
|
@@ -25606,15 +25951,31 @@ var AuthGate = class {
|
|
|
25606
25951
|
this.markFailed(handle.id);
|
|
25607
25952
|
return frame.type === "auth" ? "consumed" : "reject";
|
|
25608
25953
|
}
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
this.
|
|
25612
|
-
|
|
25613
|
-
|
|
25614
|
-
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25954
|
+
let ctx = null;
|
|
25955
|
+
if (this.opts.authenticate) {
|
|
25956
|
+
const r = this.opts.authenticate(
|
|
25957
|
+
parsed.data.token,
|
|
25958
|
+
parsed.data.selfPrincipalId,
|
|
25959
|
+
parsed.data.selfDisplayName
|
|
25960
|
+
);
|
|
25961
|
+
if (!r.ok) {
|
|
25962
|
+
this.opts.closeConnection(handle, 4401, r.code);
|
|
25963
|
+
this.markFailed(handle.id);
|
|
25964
|
+
return "consumed";
|
|
25965
|
+
}
|
|
25966
|
+
ctx = r.context;
|
|
25967
|
+
} else {
|
|
25968
|
+
if (!this.opts.expectedToken) {
|
|
25969
|
+
this.opts.closeConnection(handle, 1008, "auth not configured");
|
|
25970
|
+
this.markFailed(handle.id);
|
|
25971
|
+
return "consumed";
|
|
25972
|
+
}
|
|
25973
|
+
if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
|
|
25974
|
+
this.opts.closeConnection(handle, 1008, "auth failed");
|
|
25975
|
+
this.markFailed(handle.id);
|
|
25976
|
+
return "consumed";
|
|
25977
|
+
}
|
|
25978
|
+
ctx = this.opts.buildOwnerContext?.() ?? null;
|
|
25618
25979
|
}
|
|
25619
25980
|
if (st.timer != null) {
|
|
25620
25981
|
const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
@@ -25622,6 +25983,7 @@ var AuthGate = class {
|
|
|
25622
25983
|
}
|
|
25623
25984
|
st.authed = true;
|
|
25624
25985
|
st.timer = null;
|
|
25986
|
+
if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
|
|
25625
25987
|
this.opts.sendOk(handle, { type: "auth:ok" });
|
|
25626
25988
|
return "consumed";
|
|
25627
25989
|
}
|
|
@@ -25657,7 +26019,7 @@ var AuthContextResolver = class {
|
|
|
25657
26019
|
}
|
|
25658
26020
|
opts;
|
|
25659
26021
|
/**
|
|
25660
|
-
* 从 `Authorization` 头解析。null = 无凭证 /
|
|
26022
|
+
* 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
|
|
25661
26023
|
* remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
|
|
25662
26024
|
*/
|
|
25663
26025
|
resolveFromHeader(authHeader, remoteAddress) {
|
|
@@ -25667,15 +26029,6 @@ var AuthContextResolver = class {
|
|
|
25667
26029
|
if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
|
|
25668
26030
|
return { role: "owner", isLoopback };
|
|
25669
26031
|
}
|
|
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
|
-
}
|
|
25679
26032
|
return null;
|
|
25680
26033
|
}
|
|
25681
26034
|
};
|
|
@@ -25697,206 +26050,753 @@ function constantTimeEqual2(a, b2) {
|
|
|
25697
26050
|
return diff2 === 0;
|
|
25698
26051
|
}
|
|
25699
26052
|
|
|
25700
|
-
// src/
|
|
25701
|
-
|
|
25702
|
-
var import_node_path16 = __toESM(require("path"), 1);
|
|
26053
|
+
// ../protocol/src/index.ts
|
|
26054
|
+
init_runtime();
|
|
25703
26055
|
|
|
25704
|
-
// src/
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
25710
|
-
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
25715
|
-
|
|
25716
|
-
}
|
|
25717
|
-
|
|
25718
|
-
|
|
26056
|
+
// src/transport/connection-context.ts
|
|
26057
|
+
function ownerContext(ownerPrincipalId, displayName) {
|
|
26058
|
+
return {
|
|
26059
|
+
principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
|
|
26060
|
+
grants: [{ resource: { type: "*" }, actions: ["admin"] }]
|
|
26061
|
+
};
|
|
26062
|
+
}
|
|
26063
|
+
function guestContext(cap) {
|
|
26064
|
+
return {
|
|
26065
|
+
principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
|
|
26066
|
+
grants: cap.grants,
|
|
26067
|
+
capabilityId: cap.id
|
|
26068
|
+
};
|
|
26069
|
+
}
|
|
26070
|
+
function authenticate(token, deps) {
|
|
26071
|
+
if (!token) return { ok: false, code: "NO_TOKEN" };
|
|
26072
|
+
if (deps.isOwnerToken(token)) {
|
|
26073
|
+
return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
|
|
26074
|
+
}
|
|
26075
|
+
if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
|
|
26076
|
+
const v2 = deps.capabilityRegistry.verifyToken(token);
|
|
26077
|
+
if (!v2.ok) {
|
|
26078
|
+
if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
|
|
26079
|
+
return { ok: false, code: v2.code };
|
|
26080
|
+
}
|
|
26081
|
+
if (deps.selfPrincipalId && deps.selfDisplayName && deps.onGuestHello) {
|
|
26082
|
+
try {
|
|
26083
|
+
deps.onGuestHello(v2.capability.id, deps.selfPrincipalId, deps.selfDisplayName);
|
|
26084
|
+
} catch (e) {
|
|
26085
|
+
if (e.message === "TOKEN_SHARED_LEAK") {
|
|
26086
|
+
return { ok: false, code: "TOKEN_REVOKED" };
|
|
26087
|
+
}
|
|
26088
|
+
throw e;
|
|
26089
|
+
}
|
|
25719
26090
|
}
|
|
25720
|
-
|
|
25721
|
-
|
|
25722
|
-
|
|
26091
|
+
return { ok: true, context: guestContext(v2.capability) };
|
|
26092
|
+
}
|
|
26093
|
+
|
|
26094
|
+
// src/permission/capability-store.ts
|
|
26095
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
26096
|
+
var path18 = __toESM(require("path"), 1);
|
|
26097
|
+
var CAPABILITIES_FILE_NAME = "capabilities.json";
|
|
26098
|
+
var FILE_VERSION = 1;
|
|
26099
|
+
var CapabilityStore = class {
|
|
26100
|
+
constructor(dataDir) {
|
|
26101
|
+
this.dataDir = dataDir;
|
|
26102
|
+
fs15.mkdirSync(dataDir, { recursive: true });
|
|
26103
|
+
this.cache = this.readFromDisk();
|
|
25723
26104
|
}
|
|
25724
|
-
|
|
25725
|
-
|
|
26105
|
+
dataDir;
|
|
26106
|
+
cache;
|
|
26107
|
+
list() {
|
|
26108
|
+
return [...this.cache];
|
|
25726
26109
|
}
|
|
25727
|
-
|
|
25728
|
-
|
|
25729
|
-
|
|
26110
|
+
upsert(cap) {
|
|
26111
|
+
const idx = this.cache.findIndex((c) => c.id === cap.id);
|
|
26112
|
+
if (idx >= 0) {
|
|
26113
|
+
this.cache[idx] = cap;
|
|
26114
|
+
} else {
|
|
26115
|
+
this.cache.push(cap);
|
|
26116
|
+
}
|
|
26117
|
+
this.flush();
|
|
26118
|
+
}
|
|
26119
|
+
remove(id) {
|
|
26120
|
+
const next = this.cache.filter((c) => c.id !== id);
|
|
26121
|
+
if (next.length === this.cache.length) return;
|
|
26122
|
+
this.cache = next;
|
|
26123
|
+
this.flush();
|
|
26124
|
+
}
|
|
26125
|
+
filePath() {
|
|
26126
|
+
return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
|
|
26127
|
+
}
|
|
26128
|
+
readFromDisk() {
|
|
26129
|
+
const file = this.filePath();
|
|
26130
|
+
let raw;
|
|
25730
26131
|
try {
|
|
25731
|
-
|
|
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;
|
|
26132
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
25745
26133
|
} catch (err) {
|
|
25746
|
-
|
|
25747
|
-
|
|
25748
|
-
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
|
|
26134
|
+
if (err?.code === "ENOENT") return [];
|
|
26135
|
+
return [];
|
|
26136
|
+
}
|
|
26137
|
+
if (!raw.trim()) return [];
|
|
26138
|
+
let parsed;
|
|
26139
|
+
try {
|
|
26140
|
+
parsed = JSON.parse(raw);
|
|
26141
|
+
} catch {
|
|
25752
26142
|
return [];
|
|
25753
26143
|
}
|
|
26144
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
26145
|
+
const arr = parsed.capabilities;
|
|
26146
|
+
if (!Array.isArray(arr)) return [];
|
|
26147
|
+
const out = [];
|
|
26148
|
+
for (const item of arr) {
|
|
26149
|
+
const r = CapabilitySchema.safeParse(item);
|
|
26150
|
+
if (r.success) out.push(r.data);
|
|
26151
|
+
}
|
|
26152
|
+
return out;
|
|
25754
26153
|
}
|
|
25755
|
-
|
|
25756
|
-
const
|
|
25757
|
-
|
|
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);
|
|
26154
|
+
flush() {
|
|
26155
|
+
const content = { version: FILE_VERSION, capabilities: this.cache };
|
|
26156
|
+
this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
|
|
25761
26157
|
}
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
|
|
25765
|
-
|
|
25766
|
-
|
|
25767
|
-
|
|
25768
|
-
|
|
25769
|
-
|
|
26158
|
+
atomicWrite(file, content) {
|
|
26159
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
26160
|
+
fs15.writeFileSync(tmp, content, { mode: 384 });
|
|
26161
|
+
fs15.renameSync(tmp, file);
|
|
26162
|
+
try {
|
|
26163
|
+
fs15.chmodSync(file, 384);
|
|
26164
|
+
} catch {
|
|
26165
|
+
}
|
|
26166
|
+
}
|
|
26167
|
+
};
|
|
26168
|
+
|
|
26169
|
+
// src/permission/capability-registry.ts
|
|
26170
|
+
var crypto4 = __toESM(require("crypto"), 1);
|
|
26171
|
+
var CapabilityRegistry = class {
|
|
26172
|
+
constructor(store, now = () => Date.now()) {
|
|
26173
|
+
this.store = store;
|
|
26174
|
+
this.now = now;
|
|
26175
|
+
for (const cap of store.list()) {
|
|
26176
|
+
this.bySecretHash.set(cap.secretHash, cap);
|
|
26177
|
+
}
|
|
26178
|
+
}
|
|
26179
|
+
store;
|
|
26180
|
+
now;
|
|
26181
|
+
// sha256(token) → Capability
|
|
26182
|
+
bySecretHash = /* @__PURE__ */ new Map();
|
|
26183
|
+
list() {
|
|
26184
|
+
return this.store.list();
|
|
26185
|
+
}
|
|
26186
|
+
verifyToken(token) {
|
|
26187
|
+
if (!token) return { ok: false, code: "TOKEN_INVALID" };
|
|
26188
|
+
const hash = sha256Hex(token);
|
|
26189
|
+
const cap = this.bySecretHash.get(hash);
|
|
26190
|
+
if (!cap) return { ok: false, code: "TOKEN_INVALID" };
|
|
26191
|
+
if (cap.expiresAt && this.now() >= cap.expiresAt) {
|
|
26192
|
+
return { ok: false, code: "TOKEN_EXPIRED" };
|
|
26193
|
+
}
|
|
26194
|
+
if (cap.maxUses && cap.usedCount >= cap.maxUses) {
|
|
26195
|
+
return { ok: false, code: "TOKEN_EXHAUSTED" };
|
|
26196
|
+
}
|
|
26197
|
+
const updated = { ...cap, usedCount: cap.usedCount + 1 };
|
|
26198
|
+
this.bySecretHash.set(hash, updated);
|
|
26199
|
+
this.store.upsert(updated);
|
|
26200
|
+
return { ok: true, capability: updated };
|
|
26201
|
+
}
|
|
26202
|
+
upsertCapability(cap) {
|
|
26203
|
+
this.bySecretHash.set(cap.secretHash, cap);
|
|
26204
|
+
this.store.upsert(cap);
|
|
25770
26205
|
}
|
|
25771
26206
|
/**
|
|
25772
|
-
*
|
|
25773
|
-
*
|
|
25774
|
-
*
|
|
25775
|
-
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
25776
|
-
*
|
|
25777
|
-
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
26207
|
+
* Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
|
|
26208
|
+
* 后续 verifyToken 该 token 走 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
|
|
26209
|
+
* idempotent:不存在 → null。
|
|
25778
26210
|
*/
|
|
25779
|
-
|
|
25780
|
-
const
|
|
25781
|
-
|
|
25782
|
-
|
|
25783
|
-
|
|
25784
|
-
|
|
25785
|
-
|
|
25786
|
-
|
|
25787
|
-
|
|
25788
|
-
|
|
25789
|
-
|
|
25790
|
-
|
|
25791
|
-
|
|
25792
|
-
|
|
25793
|
-
|
|
25794
|
-
|
|
25795
|
-
|
|
25796
|
-
|
|
25797
|
-
|
|
25798
|
-
|
|
25799
|
-
|
|
25800
|
-
|
|
25801
|
-
|
|
25802
|
-
|
|
25803
|
-
|
|
25804
|
-
|
|
25805
|
-
|
|
26211
|
+
delete(id) {
|
|
26212
|
+
const current = this.store.list().find((c) => c.id === id);
|
|
26213
|
+
if (!current) return null;
|
|
26214
|
+
this.bySecretHash.delete(current.secretHash);
|
|
26215
|
+
this.store.remove(id);
|
|
26216
|
+
return current;
|
|
26217
|
+
}
|
|
26218
|
+
findById(id) {
|
|
26219
|
+
return this.store.list().find((c) => c.id === id) ?? null;
|
|
26220
|
+
}
|
|
26221
|
+
};
|
|
26222
|
+
function sha256Hex(s) {
|
|
26223
|
+
return crypto4.createHash("sha256").update(s).digest("hex");
|
|
26224
|
+
}
|
|
26225
|
+
|
|
26226
|
+
// src/permission/capability-manager.ts
|
|
26227
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
26228
|
+
var CapabilityManager = class {
|
|
26229
|
+
constructor(registry2, hooks = {}) {
|
|
26230
|
+
this.registry = registry2;
|
|
26231
|
+
this.hooks = hooks;
|
|
26232
|
+
}
|
|
26233
|
+
registry;
|
|
26234
|
+
hooks;
|
|
26235
|
+
list() {
|
|
26236
|
+
return this.registry.list();
|
|
26237
|
+
}
|
|
26238
|
+
// whoami handler 用:根据 capabilityId 反查 caller 的 capability 实体。
|
|
26239
|
+
// 返回 Capability 持久化形态;handler 调 stripSecretHash 后再上 wire。
|
|
26240
|
+
findById(id) {
|
|
26241
|
+
return this.registry.findById(id);
|
|
26242
|
+
}
|
|
26243
|
+
issue(args) {
|
|
26244
|
+
if (!args.displayName.trim()) {
|
|
26245
|
+
throw new Error("CapabilityManager.issue: displayName must be non-empty");
|
|
25806
26246
|
}
|
|
25807
|
-
this.
|
|
25808
|
-
this.
|
|
25809
|
-
|
|
26247
|
+
const token = (this.hooks.generateToken ?? defaultGenerateToken)();
|
|
26248
|
+
const id = (this.hooks.generateId ?? defaultGenerateId)();
|
|
26249
|
+
const now = (this.hooks.now ?? Date.now)();
|
|
26250
|
+
const cap = {
|
|
26251
|
+
id,
|
|
26252
|
+
secretHash: sha256Hex2(token),
|
|
26253
|
+
displayName: args.displayName,
|
|
26254
|
+
grants: args.grants,
|
|
26255
|
+
issuedAt: now,
|
|
26256
|
+
usedCount: 0,
|
|
26257
|
+
...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
|
|
26258
|
+
...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
|
|
26259
|
+
};
|
|
26260
|
+
this.registry.upsertCapability(cap);
|
|
26261
|
+
this.hooks.onIssued?.(cap, token);
|
|
26262
|
+
return { token, capability: cap };
|
|
25810
26263
|
}
|
|
25811
26264
|
/**
|
|
25812
|
-
*
|
|
25813
|
-
*
|
|
25814
|
-
*
|
|
25815
|
-
*
|
|
25816
|
-
*
|
|
26265
|
+
* guest 端 auth 帧捎带 selfPrincipalId/selfDisplayName 时,AuthGate.authenticate
|
|
26266
|
+
* 校验 cap token 通过后调本方法把 peer 字段写回 cap:
|
|
26267
|
+
* - 首次(cap.firstUsedByPeerAt 未设)→ 写盘 + 触发 onIssued 复用 push 频道
|
|
26268
|
+
* 让 owner UI 通过 capability:updated 实时拿到 peer 字段
|
|
26269
|
+
* - 重复(同 peerOwnerId)→ noop
|
|
26270
|
+
* - 冲突(不同 peerOwnerId)→ throw TOKEN_SHARED_LEAK,AuthGate 转 close 4401
|
|
25817
26271
|
*/
|
|
25818
|
-
|
|
25819
|
-
const
|
|
25820
|
-
|
|
25821
|
-
if (
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
26272
|
+
recordPeerHello(capId, args) {
|
|
26273
|
+
const cap = this.registry.findById(capId);
|
|
26274
|
+
if (!cap) return { changed: false };
|
|
26275
|
+
if (cap.firstUsedByPeerAt != null) {
|
|
26276
|
+
if (cap.peerOwnerId !== args.ownerPrincipalId) {
|
|
26277
|
+
throw new Error("TOKEN_SHARED_LEAK");
|
|
26278
|
+
}
|
|
26279
|
+
return { changed: false };
|
|
26280
|
+
}
|
|
26281
|
+
const updated = {
|
|
26282
|
+
...cap,
|
|
26283
|
+
peerOwnerId: args.ownerPrincipalId,
|
|
26284
|
+
firstUsedByPeerAt: args.now,
|
|
26285
|
+
peerDisplayName: args.displayName
|
|
26286
|
+
};
|
|
26287
|
+
this.registry.upsertCapability(updated);
|
|
26288
|
+
this.hooks.onIssued?.(updated, "__peer_hello__");
|
|
26289
|
+
return { changed: true };
|
|
25826
26290
|
}
|
|
25827
26291
|
/**
|
|
25828
|
-
*
|
|
25829
|
-
*
|
|
25830
|
-
*
|
|
25831
|
-
*
|
|
25832
|
-
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
26292
|
+
* Hard delete:从 registry / store 物理移除 capability。
|
|
26293
|
+
* idempotent:不存在 → null(handler 翻译成 VALIDATION_ERROR)
|
|
26294
|
+
* onDeleted hook 在 daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
|
|
25833
26295
|
*/
|
|
25834
|
-
|
|
25835
|
-
const
|
|
25836
|
-
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
25840
|
-
|
|
25841
|
-
|
|
26296
|
+
delete(id) {
|
|
26297
|
+
const removed = this.registry.delete(id);
|
|
26298
|
+
if (!removed) return null;
|
|
26299
|
+
this.hooks.onDeleted?.(removed);
|
|
26300
|
+
return { capability: removed };
|
|
26301
|
+
}
|
|
26302
|
+
};
|
|
26303
|
+
function defaultGenerateToken() {
|
|
26304
|
+
return crypto5.randomBytes(24).toString("base64url");
|
|
26305
|
+
}
|
|
26306
|
+
function defaultGenerateId() {
|
|
26307
|
+
return "cap_" + crypto5.randomBytes(6).toString("base64url");
|
|
26308
|
+
}
|
|
26309
|
+
function sha256Hex2(s) {
|
|
26310
|
+
return crypto5.createHash("sha256").update(s).digest("hex");
|
|
26311
|
+
}
|
|
26312
|
+
|
|
26313
|
+
// src/permission/cleanup.ts
|
|
26314
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
26315
|
+
function cleanupGuestSessionsForCapability(cap, factory) {
|
|
26316
|
+
const removed = [];
|
|
26317
|
+
for (const g2 of cap.grants) {
|
|
26318
|
+
if (g2.resource.type !== "persona") continue;
|
|
26319
|
+
const dir = factory.vmGuestRoot(g2.resource.id, cap.id);
|
|
26320
|
+
try {
|
|
26321
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
26322
|
+
removed.push(dir);
|
|
26323
|
+
} catch {
|
|
26324
|
+
}
|
|
26325
|
+
}
|
|
26326
|
+
return { removed };
|
|
26327
|
+
}
|
|
26328
|
+
|
|
26329
|
+
// src/inbox/inbox-store.ts
|
|
26330
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
26331
|
+
var path19 = __toESM(require("path"), 1);
|
|
26332
|
+
var INBOX_SUBDIR = "inbox";
|
|
26333
|
+
var InboxStore = class {
|
|
26334
|
+
constructor(dataDir) {
|
|
26335
|
+
this.dataDir = dataDir;
|
|
26336
|
+
fs17.mkdirSync(this.dirPath(), { recursive: true });
|
|
25842
26337
|
}
|
|
26338
|
+
dataDir;
|
|
25843
26339
|
/**
|
|
25844
|
-
*
|
|
25845
|
-
*
|
|
25846
|
-
*
|
|
25847
|
-
|
|
26340
|
+
* 列出某 peer 的消息, 按 createdAt 升序.
|
|
26341
|
+
* sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
|
|
26342
|
+
* 文件不存在时返回 [] (不抛, 不创建).
|
|
26343
|
+
*/
|
|
26344
|
+
list(peerOwnerId, sinceCreatedAt) {
|
|
26345
|
+
const file = this.filePath(peerOwnerId);
|
|
26346
|
+
let raw;
|
|
26347
|
+
try {
|
|
26348
|
+
raw = fs17.readFileSync(file, "utf8");
|
|
26349
|
+
} catch (err) {
|
|
26350
|
+
if (err?.code === "ENOENT") return [];
|
|
26351
|
+
return [];
|
|
26352
|
+
}
|
|
26353
|
+
const all = parseAllLines(raw);
|
|
26354
|
+
if (sinceCreatedAt === void 0) return all;
|
|
26355
|
+
return all.filter((m2) => m2.createdAt > sinceCreatedAt);
|
|
26356
|
+
}
|
|
26357
|
+
/**
|
|
26358
|
+
* 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
|
|
26359
|
+
*/
|
|
26360
|
+
listAllPeerOwnerIds() {
|
|
26361
|
+
const dir = this.dirPath();
|
|
26362
|
+
let entries;
|
|
26363
|
+
try {
|
|
26364
|
+
entries = fs17.readdirSync(dir);
|
|
26365
|
+
} catch (err) {
|
|
26366
|
+
if (err?.code === "ENOENT") return [];
|
|
26367
|
+
return [];
|
|
26368
|
+
}
|
|
26369
|
+
return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
|
|
26370
|
+
}
|
|
26371
|
+
/**
|
|
26372
|
+
* 追加一条消息,按 message.id 幂等去重。
|
|
26373
|
+
* 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
|
|
26374
|
+
*/
|
|
26375
|
+
append(message) {
|
|
26376
|
+
const existing = this.list(message.peerOwnerId);
|
|
26377
|
+
if (existing.some((m2) => m2.id === message.id)) return;
|
|
26378
|
+
const file = this.filePath(message.peerOwnerId);
|
|
26379
|
+
const line = JSON.stringify(message) + "\n";
|
|
26380
|
+
fs17.appendFileSync(file, line, { mode: 384 });
|
|
26381
|
+
try {
|
|
26382
|
+
fs17.chmodSync(file, 384);
|
|
26383
|
+
} catch {
|
|
26384
|
+
}
|
|
26385
|
+
}
|
|
26386
|
+
/**
|
|
26387
|
+
* 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
|
|
26388
|
+
* readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
|
|
26389
|
+
* 返写入了几条.
|
|
26390
|
+
*/
|
|
26391
|
+
markRead(peerOwnerId, principalId, upToCreatedAt) {
|
|
26392
|
+
const messages = this.list(peerOwnerId);
|
|
26393
|
+
let changed = 0;
|
|
26394
|
+
const next = messages.map((m2) => {
|
|
26395
|
+
if (m2.senderPrincipalId === principalId) return m2;
|
|
26396
|
+
if (m2.createdAt > upToCreatedAt) return m2;
|
|
26397
|
+
const cur = m2.readBy[principalId];
|
|
26398
|
+
if (cur !== void 0 && cur >= upToCreatedAt) return m2;
|
|
26399
|
+
changed++;
|
|
26400
|
+
return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
|
|
26401
|
+
});
|
|
26402
|
+
if (changed === 0) return 0;
|
|
26403
|
+
this.rewriteFile(peerOwnerId, next);
|
|
26404
|
+
return changed;
|
|
26405
|
+
}
|
|
26406
|
+
/**
|
|
26407
|
+
* 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
|
|
26408
|
+
*/
|
|
26409
|
+
removeByPeerOwnerId(peerOwnerId) {
|
|
26410
|
+
const file = this.filePath(peerOwnerId);
|
|
26411
|
+
try {
|
|
26412
|
+
fs17.unlinkSync(file);
|
|
26413
|
+
} catch (err) {
|
|
26414
|
+
if (err?.code === "ENOENT") return;
|
|
26415
|
+
}
|
|
26416
|
+
}
|
|
26417
|
+
rewriteFile(peerOwnerId, messages) {
|
|
26418
|
+
const file = this.filePath(peerOwnerId);
|
|
26419
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
26420
|
+
const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
|
|
26421
|
+
fs17.writeFileSync(tmp, content, { mode: 384 });
|
|
26422
|
+
fs17.renameSync(tmp, file);
|
|
26423
|
+
try {
|
|
26424
|
+
fs17.chmodSync(file, 384);
|
|
26425
|
+
} catch {
|
|
26426
|
+
}
|
|
26427
|
+
}
|
|
26428
|
+
dirPath() {
|
|
26429
|
+
return path19.join(this.dataDir, INBOX_SUBDIR);
|
|
26430
|
+
}
|
|
26431
|
+
filePath(peerOwnerId) {
|
|
26432
|
+
return path19.join(this.dirPath(), `${peerOwnerId}.jsonl`);
|
|
26433
|
+
}
|
|
26434
|
+
};
|
|
26435
|
+
function parseAllLines(raw) {
|
|
26436
|
+
const out = [];
|
|
26437
|
+
for (const line of raw.split("\n")) {
|
|
26438
|
+
const trimmed = line.trim();
|
|
26439
|
+
if (!trimmed) continue;
|
|
26440
|
+
let parsed;
|
|
26441
|
+
try {
|
|
26442
|
+
parsed = JSON.parse(trimmed);
|
|
26443
|
+
} catch {
|
|
26444
|
+
continue;
|
|
26445
|
+
}
|
|
26446
|
+
const r = InboxMessageSchema.safeParse(parsed);
|
|
26447
|
+
if (r.success) out.push(r.data);
|
|
26448
|
+
}
|
|
26449
|
+
return out;
|
|
26450
|
+
}
|
|
26451
|
+
|
|
26452
|
+
// src/inbox/inbox-manager.ts
|
|
26453
|
+
var InboxManager = class {
|
|
26454
|
+
constructor(store, broadcast) {
|
|
26455
|
+
this.store = store;
|
|
26456
|
+
this.broadcast = broadcast;
|
|
26457
|
+
}
|
|
26458
|
+
store;
|
|
26459
|
+
broadcast;
|
|
26460
|
+
/**
|
|
26461
|
+
* 写一条消息到 peer.jsonl + broadcast 给本机所有 owner ws。
|
|
26462
|
+
* 按 id 幂等:同 id 已落盘则 no-op(不广播、不重写)。
|
|
25848
26463
|
*
|
|
25849
|
-
*
|
|
26464
|
+
* caller 必须已通过 handler 鉴权
|
|
26465
|
+
* (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
|
|
25850
26466
|
*/
|
|
25851
|
-
|
|
25852
|
-
const
|
|
25853
|
-
|
|
25854
|
-
|
|
25855
|
-
|
|
25856
|
-
|
|
26467
|
+
postMessage(args) {
|
|
26468
|
+
const beforeLen = this.store.list(args.peerOwnerId).length;
|
|
26469
|
+
const msg = {
|
|
26470
|
+
id: args.id,
|
|
26471
|
+
peerOwnerId: args.peerOwnerId,
|
|
26472
|
+
senderPrincipalId: args.senderPrincipalId,
|
|
26473
|
+
text: args.text,
|
|
26474
|
+
createdAt: args.createdAt,
|
|
26475
|
+
readBy: {}
|
|
26476
|
+
};
|
|
26477
|
+
this.store.append(msg);
|
|
26478
|
+
const afterLen = this.store.list(args.peerOwnerId).length;
|
|
26479
|
+
if (afterLen === beforeLen) return msg;
|
|
26480
|
+
this.broadcast(args.peerOwnerId, {
|
|
26481
|
+
type: "inbox:event",
|
|
26482
|
+
peerOwnerId: args.peerOwnerId,
|
|
26483
|
+
message: msg
|
|
26484
|
+
});
|
|
26485
|
+
return msg;
|
|
26486
|
+
}
|
|
26487
|
+
list(peerOwnerId, sinceCreatedAt) {
|
|
26488
|
+
return this.store.list(peerOwnerId, sinceCreatedAt);
|
|
26489
|
+
}
|
|
26490
|
+
/**
|
|
26491
|
+
* 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
|
|
26492
|
+
* broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
|
|
26493
|
+
*/
|
|
26494
|
+
markRead(args) {
|
|
26495
|
+
const before = this.store.list(args.peerOwnerId);
|
|
26496
|
+
const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
|
|
26497
|
+
if (changed === 0) return 0;
|
|
26498
|
+
const after = this.store.list(args.peerOwnerId);
|
|
26499
|
+
const beforeById = new Map(before.map((m2) => [m2.id, m2]));
|
|
26500
|
+
for (const m2 of after) {
|
|
26501
|
+
const prev = beforeById.get(m2.id);
|
|
26502
|
+
if (!prev) continue;
|
|
26503
|
+
if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
|
|
26504
|
+
this.broadcast(args.peerOwnerId, {
|
|
26505
|
+
type: "inbox:event",
|
|
26506
|
+
peerOwnerId: args.peerOwnerId,
|
|
26507
|
+
message: m2
|
|
26508
|
+
});
|
|
26509
|
+
}
|
|
26510
|
+
return changed;
|
|
26511
|
+
}
|
|
26512
|
+
/** peer:remove cascade. 删整个 peer 的 jsonl. */
|
|
26513
|
+
removeChannel(peerOwnerId) {
|
|
26514
|
+
this.store.removeByPeerOwnerId(peerOwnerId);
|
|
26515
|
+
}
|
|
26516
|
+
};
|
|
26517
|
+
|
|
26518
|
+
// src/state/received-capability-store.ts
|
|
26519
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
26520
|
+
var path20 = __toESM(require("path"), 1);
|
|
26521
|
+
var FILE_NAME = "received-capabilities.json";
|
|
26522
|
+
var ReceivedCapabilityStore = class {
|
|
26523
|
+
constructor(dataDir) {
|
|
26524
|
+
this.dataDir = dataDir;
|
|
26525
|
+
}
|
|
26526
|
+
dataDir;
|
|
26527
|
+
caps = /* @__PURE__ */ new Map();
|
|
26528
|
+
load() {
|
|
26529
|
+
this.caps.clear();
|
|
26530
|
+
const file = path20.join(this.dataDir, FILE_NAME);
|
|
26531
|
+
let raw;
|
|
26532
|
+
try {
|
|
26533
|
+
raw = fs18.readFileSync(file, "utf8");
|
|
26534
|
+
} catch (err) {
|
|
26535
|
+
if (err?.code !== "ENOENT") this.renameBak(file);
|
|
26536
|
+
return;
|
|
26537
|
+
}
|
|
26538
|
+
let parsed;
|
|
26539
|
+
try {
|
|
26540
|
+
parsed = JSON.parse(raw);
|
|
26541
|
+
} catch {
|
|
26542
|
+
this.renameBak(file);
|
|
26543
|
+
return;
|
|
26544
|
+
}
|
|
26545
|
+
const arr = parsed?.receivedCaps;
|
|
26546
|
+
if (!Array.isArray(arr)) return;
|
|
26547
|
+
for (const entry of arr) {
|
|
26548
|
+
const r = ReceivedCapabilitySchema.safeParse(entry);
|
|
26549
|
+
if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
|
|
26550
|
+
}
|
|
26551
|
+
}
|
|
26552
|
+
list() {
|
|
26553
|
+
return Array.from(this.caps.values());
|
|
26554
|
+
}
|
|
26555
|
+
get(ownerPrincipalId) {
|
|
26556
|
+
return this.caps.get(ownerPrincipalId) ?? null;
|
|
26557
|
+
}
|
|
26558
|
+
hasOwner(ownerPrincipalId) {
|
|
26559
|
+
return this.caps.has(ownerPrincipalId);
|
|
26560
|
+
}
|
|
26561
|
+
upsert(cap) {
|
|
26562
|
+
this.caps.set(cap.ownerPrincipalId, cap);
|
|
26563
|
+
this.flush();
|
|
26564
|
+
}
|
|
26565
|
+
remove(ownerPrincipalId) {
|
|
26566
|
+
if (!this.caps.delete(ownerPrincipalId)) return;
|
|
26567
|
+
this.flush();
|
|
26568
|
+
}
|
|
26569
|
+
flush() {
|
|
26570
|
+
const file = path20.join(this.dataDir, FILE_NAME);
|
|
26571
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
26572
|
+
const content = JSON.stringify(
|
|
26573
|
+
{ receivedCaps: Array.from(this.caps.values()) },
|
|
26574
|
+
null,
|
|
26575
|
+
2
|
|
26576
|
+
);
|
|
26577
|
+
fs18.mkdirSync(this.dataDir, { recursive: true });
|
|
26578
|
+
fs18.writeFileSync(tmp, content, { mode: 384 });
|
|
26579
|
+
fs18.renameSync(tmp, file);
|
|
26580
|
+
}
|
|
26581
|
+
renameBak(file) {
|
|
26582
|
+
try {
|
|
26583
|
+
fs18.renameSync(file, `${file}.bak`);
|
|
26584
|
+
} catch {
|
|
26585
|
+
}
|
|
26586
|
+
}
|
|
26587
|
+
};
|
|
26588
|
+
|
|
26589
|
+
// src/state/legacy-migration.ts
|
|
26590
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
26591
|
+
var path21 = __toESM(require("path"), 1);
|
|
26592
|
+
var TARGETS = [
|
|
26593
|
+
{ src: "capabilities.json", bak: "capabilities.legacy.bak" },
|
|
26594
|
+
{ src: "remote-personas", bak: "remote-personas.legacy.bak" },
|
|
26595
|
+
{ src: "peers.json", bak: "peers.legacy.bak" }
|
|
26596
|
+
];
|
|
26597
|
+
function migrateLegacyFiles(dataDir) {
|
|
26598
|
+
for (const t of TARGETS) {
|
|
26599
|
+
const srcPath = path21.join(dataDir, t.src);
|
|
26600
|
+
const bakPath = path21.join(dataDir, t.bak);
|
|
26601
|
+
if (!fs19.existsSync(srcPath)) continue;
|
|
26602
|
+
if (fs19.existsSync(bakPath)) continue;
|
|
26603
|
+
try {
|
|
26604
|
+
fs19.renameSync(srcPath, bakPath);
|
|
26605
|
+
} catch {
|
|
26606
|
+
}
|
|
26607
|
+
}
|
|
26608
|
+
}
|
|
26609
|
+
|
|
26610
|
+
// src/received-capability/connect-remote.ts
|
|
26611
|
+
var crypto6 = __toESM(require("crypto"), 1);
|
|
26612
|
+
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
26613
|
+
var RPC_TIMEOUT_MS = 5e3;
|
|
26614
|
+
async function connectRemote(args) {
|
|
26615
|
+
const ws = new wrapper_default(args.url);
|
|
26616
|
+
await new Promise((resolve2, reject) => {
|
|
26617
|
+
const onError = (e) => reject(e);
|
|
26618
|
+
ws.once("open", () => {
|
|
26619
|
+
ws.off("error", onError);
|
|
26620
|
+
resolve2();
|
|
26621
|
+
});
|
|
26622
|
+
ws.once("error", onError);
|
|
26623
|
+
});
|
|
26624
|
+
ws.send(
|
|
26625
|
+
JSON.stringify({
|
|
26626
|
+
type: "auth",
|
|
26627
|
+
token: args.token,
|
|
26628
|
+
selfPrincipalId: args.selfPrincipalId,
|
|
26629
|
+
selfDisplayName: args.selfDisplayName
|
|
26630
|
+
})
|
|
26631
|
+
);
|
|
26632
|
+
await new Promise((resolve2, reject) => {
|
|
26633
|
+
const onMessage = (raw) => {
|
|
26634
|
+
let f;
|
|
25857
26635
|
try {
|
|
25858
|
-
|
|
25859
|
-
} catch
|
|
25860
|
-
|
|
25861
|
-
if (code === "ENOENT") continue;
|
|
25862
|
-
continue;
|
|
26636
|
+
f = JSON.parse(raw.toString());
|
|
26637
|
+
} catch {
|
|
26638
|
+
return;
|
|
25863
26639
|
}
|
|
25864
|
-
|
|
25865
|
-
|
|
25866
|
-
|
|
25867
|
-
|
|
25868
|
-
|
|
25869
|
-
|
|
26640
|
+
if (f.type === "auth:ok" || f.type === "ready") {
|
|
26641
|
+
ws.off("message", onMessage);
|
|
26642
|
+
resolve2();
|
|
26643
|
+
return;
|
|
26644
|
+
}
|
|
26645
|
+
if (f.type === "auth:error" || f.code === 4401) {
|
|
26646
|
+
ws.off("message", onMessage);
|
|
26647
|
+
reject(new Error(`auth failed: ${f.message ?? f.type ?? "unknown"}`));
|
|
26648
|
+
}
|
|
26649
|
+
};
|
|
26650
|
+
ws.on("message", onMessage);
|
|
26651
|
+
setTimeout(() => {
|
|
26652
|
+
ws.off("message", onMessage);
|
|
26653
|
+
reject(new Error("auth handshake timeout"));
|
|
26654
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
26655
|
+
});
|
|
26656
|
+
function call(method, payload) {
|
|
26657
|
+
const requestId = crypto6.randomUUID();
|
|
26658
|
+
return new Promise((resolve2, reject) => {
|
|
26659
|
+
const onMessage = (raw) => {
|
|
26660
|
+
let f;
|
|
26661
|
+
try {
|
|
26662
|
+
f = JSON.parse(raw.toString());
|
|
26663
|
+
} catch {
|
|
26664
|
+
return;
|
|
26665
|
+
}
|
|
26666
|
+
if (f.requestId !== requestId) return;
|
|
26667
|
+
ws.off("message", onMessage);
|
|
26668
|
+
if (f.error || f.type?.endsWith(":error")) {
|
|
26669
|
+
reject(new Error(f.message ?? f.error ?? "rpc error"));
|
|
26670
|
+
} else {
|
|
26671
|
+
resolve2(f);
|
|
26672
|
+
}
|
|
26673
|
+
};
|
|
26674
|
+
ws.on("message", onMessage);
|
|
26675
|
+
ws.send(JSON.stringify({ type: method, requestId, ...payload }));
|
|
26676
|
+
setTimeout(() => {
|
|
26677
|
+
ws.off("message", onMessage);
|
|
26678
|
+
reject(new Error(`${method} timeout`));
|
|
26679
|
+
}, RPC_TIMEOUT_MS);
|
|
26680
|
+
});
|
|
26681
|
+
}
|
|
26682
|
+
return {
|
|
26683
|
+
whoami: async () => {
|
|
26684
|
+
const f = await call("whoami", {});
|
|
26685
|
+
return {
|
|
26686
|
+
ownerId: f.owner.id,
|
|
26687
|
+
displayName: f.owner.displayName,
|
|
26688
|
+
capabilityId: f.capability.id,
|
|
26689
|
+
grants: f.capability.grants
|
|
26690
|
+
};
|
|
26691
|
+
},
|
|
26692
|
+
close: () => {
|
|
26693
|
+
try {
|
|
26694
|
+
ws.close();
|
|
26695
|
+
} catch {
|
|
25870
26696
|
}
|
|
25871
26697
|
}
|
|
25872
|
-
|
|
26698
|
+
};
|
|
26699
|
+
}
|
|
26700
|
+
|
|
26701
|
+
// src/migrations/2026-05-20-flatten-sessions.ts
|
|
26702
|
+
var fs20 = __toESM(require("fs"), 1);
|
|
26703
|
+
var path22 = __toESM(require("path"), 1);
|
|
26704
|
+
var MIGRATION_FLAG_NAME = ".migration.v1.done";
|
|
26705
|
+
function migrateFlattenSessions(opts) {
|
|
26706
|
+
const dataDir = opts.dataDir;
|
|
26707
|
+
const now = opts.now ?? Date.now;
|
|
26708
|
+
const sessionsDir = path22.join(dataDir, "sessions");
|
|
26709
|
+
const flagPath = path22.join(sessionsDir, MIGRATION_FLAG_NAME);
|
|
26710
|
+
if (existsSync4(flagPath)) {
|
|
26711
|
+
return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
|
|
26712
|
+
}
|
|
26713
|
+
let movedBare = 0;
|
|
26714
|
+
let movedVmOwner = 0;
|
|
26715
|
+
let archivedListener = 0;
|
|
26716
|
+
const defaultDir = path22.join(sessionsDir, "default");
|
|
26717
|
+
if (existsSync4(defaultDir)) {
|
|
26718
|
+
for (const entry of readdirSafe(defaultDir)) {
|
|
26719
|
+
if (!entry.endsWith(".json")) continue;
|
|
26720
|
+
const src = path22.join(defaultDir, entry);
|
|
26721
|
+
const dst = path22.join(sessionsDir, entry);
|
|
26722
|
+
fs20.renameSync(src, dst);
|
|
26723
|
+
movedBare += 1;
|
|
26724
|
+
}
|
|
26725
|
+
rmdirIfEmpty(defaultDir);
|
|
26726
|
+
}
|
|
26727
|
+
for (const pid of readdirSafe(sessionsDir)) {
|
|
26728
|
+
const personaDir = path22.join(sessionsDir, pid);
|
|
26729
|
+
if (!isDir(personaDir)) continue;
|
|
26730
|
+
if (pid === "default") continue;
|
|
26731
|
+
const ownerSrc = path22.join(personaDir, "owner");
|
|
26732
|
+
if (existsSync4(ownerSrc) && isDir(ownerSrc)) {
|
|
26733
|
+
const ownerDst = path22.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
|
|
26734
|
+
fs20.mkdirSync(ownerDst, { recursive: true });
|
|
26735
|
+
for (const file of readdirSafe(ownerSrc)) {
|
|
26736
|
+
if (!file.endsWith(".json")) continue;
|
|
26737
|
+
fs20.renameSync(path22.join(ownerSrc, file), path22.join(ownerDst, file));
|
|
26738
|
+
movedVmOwner += 1;
|
|
26739
|
+
}
|
|
26740
|
+
rmdirIfEmpty(ownerSrc);
|
|
26741
|
+
}
|
|
26742
|
+
const listenerSrc = path22.join(personaDir, "listener");
|
|
26743
|
+
if (existsSync4(listenerSrc) && isDir(listenerSrc)) {
|
|
26744
|
+
const archiveDst = path22.join(dataDir, ".legacy", `listener-${pid}`);
|
|
26745
|
+
fs20.mkdirSync(archiveDst, { recursive: true });
|
|
26746
|
+
for (const file of readdirSafe(listenerSrc)) {
|
|
26747
|
+
if (!file.endsWith(".json")) continue;
|
|
26748
|
+
fs20.renameSync(path22.join(listenerSrc, file), path22.join(archiveDst, file));
|
|
26749
|
+
archivedListener += 1;
|
|
26750
|
+
}
|
|
26751
|
+
rmdirIfEmpty(listenerSrc);
|
|
26752
|
+
}
|
|
26753
|
+
rmdirIfEmpty(personaDir);
|
|
26754
|
+
}
|
|
26755
|
+
fs20.mkdirSync(sessionsDir, { recursive: true });
|
|
26756
|
+
fs20.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
|
|
26757
|
+
return {
|
|
26758
|
+
skipped: false,
|
|
26759
|
+
flagWritten: true,
|
|
26760
|
+
movedBare,
|
|
26761
|
+
movedVmOwner,
|
|
26762
|
+
archivedListener
|
|
26763
|
+
};
|
|
26764
|
+
}
|
|
26765
|
+
function existsSync4(p2) {
|
|
26766
|
+
try {
|
|
26767
|
+
fs20.statSync(p2);
|
|
26768
|
+
return true;
|
|
26769
|
+
} catch {
|
|
26770
|
+
return false;
|
|
25873
26771
|
}
|
|
25874
|
-
}
|
|
25875
|
-
function
|
|
25876
|
-
|
|
25877
|
-
|
|
26772
|
+
}
|
|
26773
|
+
function isDir(p2) {
|
|
26774
|
+
try {
|
|
26775
|
+
return fs20.statSync(p2).isDirectory();
|
|
26776
|
+
} catch {
|
|
25878
26777
|
return false;
|
|
25879
26778
|
}
|
|
25880
|
-
|
|
25881
|
-
|
|
25882
|
-
|
|
25883
|
-
|
|
25884
|
-
|
|
25885
|
-
|
|
25886
|
-
}
|
|
26779
|
+
}
|
|
26780
|
+
function readdirSafe(p2) {
|
|
26781
|
+
try {
|
|
26782
|
+
return fs20.readdirSync(p2);
|
|
26783
|
+
} catch {
|
|
26784
|
+
return [];
|
|
25887
26785
|
}
|
|
25888
|
-
return false;
|
|
25889
26786
|
}
|
|
25890
|
-
function
|
|
26787
|
+
function rmdirIfEmpty(p2) {
|
|
25891
26788
|
try {
|
|
25892
|
-
|
|
26789
|
+
fs20.rmdirSync(p2);
|
|
25893
26790
|
} catch {
|
|
25894
|
-
return null;
|
|
25895
26791
|
}
|
|
25896
26792
|
}
|
|
25897
26793
|
|
|
25898
|
-
// src/
|
|
26794
|
+
// src/transport/http-router.ts
|
|
26795
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
25899
26796
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
26797
|
+
|
|
26798
|
+
// src/attachment/mime.ts
|
|
26799
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
25900
26800
|
var TEXT_PLAIN = "text/plain; charset=utf-8";
|
|
25901
26801
|
var EXT_TO_NATIVE_MIME = {
|
|
25902
26802
|
// 图片
|
|
@@ -26003,14 +26903,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
26003
26903
|
".mk"
|
|
26004
26904
|
]);
|
|
26005
26905
|
function lookupMime(filePathOrName) {
|
|
26006
|
-
const ext =
|
|
26906
|
+
const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
|
|
26007
26907
|
if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
|
|
26008
26908
|
if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
|
|
26009
26909
|
return "application/octet-stream";
|
|
26010
26910
|
}
|
|
26011
26911
|
|
|
26012
26912
|
// src/attachment/sign-url.ts
|
|
26013
|
-
var
|
|
26913
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
26014
26914
|
var HMAC_ALGO = "sha256";
|
|
26015
26915
|
function base64urlEncode(buf) {
|
|
26016
26916
|
const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
|
|
@@ -26027,7 +26927,7 @@ function decodeAbsPathFromUrl(encoded) {
|
|
|
26027
26927
|
}
|
|
26028
26928
|
function computeSig(secret, absPath, e) {
|
|
26029
26929
|
const msg = e === null ? absPath : `${absPath}|${e}`;
|
|
26030
|
-
return
|
|
26930
|
+
return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
|
|
26031
26931
|
}
|
|
26032
26932
|
function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
|
|
26033
26933
|
const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
|
|
@@ -26062,7 +26962,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
|
|
|
26062
26962
|
if (provided.length !== expected.length) {
|
|
26063
26963
|
return { ok: false, code: "BAD_SIG" };
|
|
26064
26964
|
}
|
|
26065
|
-
if (!
|
|
26965
|
+
if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
|
|
26066
26966
|
return { ok: false, code: "BAD_SIG" };
|
|
26067
26967
|
}
|
|
26068
26968
|
if (e !== null && now() / 1e3 > e) {
|
|
@@ -26083,7 +26983,7 @@ function createHttpRouter(deps) {
|
|
|
26083
26983
|
sendJson(res, 200, { ok: true, version: deps.daemonVersion });
|
|
26084
26984
|
return true;
|
|
26085
26985
|
}
|
|
26086
|
-
if (!url.pathname.startsWith("/
|
|
26986
|
+
if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
|
|
26087
26987
|
return false;
|
|
26088
26988
|
}
|
|
26089
26989
|
if (url.pathname.startsWith("/files/") && req.method === "GET") {
|
|
@@ -26123,43 +27023,8 @@ function createHttpRouter(deps) {
|
|
|
26123
27023
|
sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
|
|
26124
27024
|
return true;
|
|
26125
27025
|
}
|
|
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
|
-
}
|
|
26157
27026
|
const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
|
|
26158
27027
|
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
|
-
}
|
|
26163
27028
|
const sid = sessionFilesMatch[1];
|
|
26164
27029
|
const pathParam = url.searchParams.get("path");
|
|
26165
27030
|
if (!pathParam) {
|
|
@@ -26167,7 +27032,7 @@ function createHttpRouter(deps) {
|
|
|
26167
27032
|
return true;
|
|
26168
27033
|
}
|
|
26169
27034
|
let absPath;
|
|
26170
|
-
if (
|
|
27035
|
+
if (import_node_path15.default.isAbsolute(pathParam)) {
|
|
26171
27036
|
absPath = pathParam;
|
|
26172
27037
|
} else if (deps.sessionStore) {
|
|
26173
27038
|
const file = deps.sessionStore.read(sid);
|
|
@@ -26175,7 +27040,7 @@ function createHttpRouter(deps) {
|
|
|
26175
27040
|
sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
|
|
26176
27041
|
return true;
|
|
26177
27042
|
}
|
|
26178
|
-
absPath =
|
|
27043
|
+
absPath = import_node_path15.default.join(file.cwd, pathParam);
|
|
26179
27044
|
} else {
|
|
26180
27045
|
sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
|
|
26181
27046
|
return true;
|
|
@@ -26200,18 +27065,12 @@ function sendJson(res, status, body) {
|
|
|
26200
27065
|
res.end(JSON.stringify(body));
|
|
26201
27066
|
}
|
|
26202
27067
|
function withCtx(ctx, body) {
|
|
26203
|
-
return { ...body, role: ctx.role
|
|
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);
|
|
27068
|
+
return { ...body, role: ctx.role };
|
|
26210
27069
|
}
|
|
26211
27070
|
function streamFile(res, absPath, logger) {
|
|
26212
27071
|
let stat;
|
|
26213
27072
|
try {
|
|
26214
|
-
stat =
|
|
27073
|
+
stat = import_node_fs13.default.statSync(absPath);
|
|
26215
27074
|
} catch (err) {
|
|
26216
27075
|
const code = err?.code;
|
|
26217
27076
|
if (code === "ENOENT") {
|
|
@@ -26226,7 +27085,7 @@ function streamFile(res, absPath, logger) {
|
|
|
26226
27085
|
return;
|
|
26227
27086
|
}
|
|
26228
27087
|
const mime = lookupMime(absPath);
|
|
26229
|
-
const basename =
|
|
27088
|
+
const basename = import_node_path15.default.basename(absPath);
|
|
26230
27089
|
res.writeHead(200, {
|
|
26231
27090
|
"Content-Type": mime,
|
|
26232
27091
|
"Content-Length": String(stat.size),
|
|
@@ -26234,7 +27093,7 @@ function streamFile(res, absPath, logger) {
|
|
|
26234
27093
|
// 防止浏览器把任意 mime 当 html 渲染
|
|
26235
27094
|
"X-Content-Type-Options": "nosniff"
|
|
26236
27095
|
});
|
|
26237
|
-
const stream =
|
|
27096
|
+
const stream = import_node_fs13.default.createReadStream(absPath);
|
|
26238
27097
|
stream.on("error", (err) => {
|
|
26239
27098
|
logger?.warn("streamFile read error", { absPath, err: err.message });
|
|
26240
27099
|
res.destroy();
|
|
@@ -26242,6 +27101,147 @@ function streamFile(res, absPath, logger) {
|
|
|
26242
27101
|
stream.pipe(res);
|
|
26243
27102
|
}
|
|
26244
27103
|
|
|
27104
|
+
// src/attachment/group.ts
|
|
27105
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
27106
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
27107
|
+
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
27108
|
+
init_protocol();
|
|
27109
|
+
var GroupFileStore = class {
|
|
27110
|
+
dataDir;
|
|
27111
|
+
logger;
|
|
27112
|
+
cache = /* @__PURE__ */ new Map();
|
|
27113
|
+
constructor(opts) {
|
|
27114
|
+
this.dataDir = opts.dataDir;
|
|
27115
|
+
this.logger = opts.logger;
|
|
27116
|
+
}
|
|
27117
|
+
rootForScope(scope) {
|
|
27118
|
+
return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
|
|
27119
|
+
}
|
|
27120
|
+
/** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
|
|
27121
|
+
filePath(scope, sessionId) {
|
|
27122
|
+
return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
|
|
27123
|
+
}
|
|
27124
|
+
cacheKey(scope, sessionId) {
|
|
27125
|
+
return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
|
|
27126
|
+
}
|
|
27127
|
+
/** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
|
|
27128
|
+
readFile(scope, sessionId) {
|
|
27129
|
+
const file = this.filePath(scope, sessionId);
|
|
27130
|
+
try {
|
|
27131
|
+
const raw = import_node_fs14.default.readFileSync(file, "utf8");
|
|
27132
|
+
const parsed = JSON.parse(raw);
|
|
27133
|
+
if (!Array.isArray(parsed)) {
|
|
27134
|
+
this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
|
|
27135
|
+
file
|
|
27136
|
+
});
|
|
27137
|
+
return [];
|
|
27138
|
+
}
|
|
27139
|
+
const out = [];
|
|
27140
|
+
for (const entry of parsed) {
|
|
27141
|
+
const r = GroupFileEntrySchema.safeParse(entry);
|
|
27142
|
+
if (r.success) out.push(r.data);
|
|
27143
|
+
}
|
|
27144
|
+
return out;
|
|
27145
|
+
} catch (err) {
|
|
27146
|
+
const code = err?.code;
|
|
27147
|
+
if (code === "ENOENT") return [];
|
|
27148
|
+
this.logger?.warn("GroupFileStore.readFile failed", {
|
|
27149
|
+
file,
|
|
27150
|
+
err: err.message
|
|
27151
|
+
});
|
|
27152
|
+
return [];
|
|
27153
|
+
}
|
|
27154
|
+
}
|
|
27155
|
+
writeFile(scope, sessionId, entries) {
|
|
27156
|
+
const file = this.filePath(scope, sessionId);
|
|
27157
|
+
import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
|
|
27158
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
27159
|
+
import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
27160
|
+
import_node_fs14.default.renameSync(tmp, file);
|
|
27161
|
+
}
|
|
27162
|
+
/** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
|
|
27163
|
+
list(scope, sessionId) {
|
|
27164
|
+
const key = this.cacheKey(scope, sessionId);
|
|
27165
|
+
const cached = this.cache.get(key);
|
|
27166
|
+
if (cached) return cached.entries;
|
|
27167
|
+
const entries = this.readFile(scope, sessionId);
|
|
27168
|
+
this.cache.set(key, { entries });
|
|
27169
|
+
return entries;
|
|
27170
|
+
}
|
|
27171
|
+
/**
|
|
27172
|
+
* upsert:
|
|
27173
|
+
* - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
|
|
27174
|
+
* (保留首次入群人 / 入群时刻)
|
|
27175
|
+
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
27176
|
+
*
|
|
27177
|
+
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
27178
|
+
*/
|
|
27179
|
+
upsert(scope, sessionId, input, now = Date.now()) {
|
|
27180
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27181
|
+
const idx = entries.findIndex((e) => e.relPath === input.relPath);
|
|
27182
|
+
let next;
|
|
27183
|
+
if (idx >= 0) {
|
|
27184
|
+
const prev = entries[idx];
|
|
27185
|
+
next = {
|
|
27186
|
+
...prev,
|
|
27187
|
+
size: input.size,
|
|
27188
|
+
mime: input.mime,
|
|
27189
|
+
lastEditedAt: now,
|
|
27190
|
+
stale: false
|
|
27191
|
+
// label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
|
|
27192
|
+
};
|
|
27193
|
+
entries[idx] = next;
|
|
27194
|
+
} else {
|
|
27195
|
+
next = {
|
|
27196
|
+
id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
|
|
27197
|
+
relPath: input.relPath,
|
|
27198
|
+
from: input.from,
|
|
27199
|
+
label: input.label,
|
|
27200
|
+
size: input.size,
|
|
27201
|
+
mime: input.mime,
|
|
27202
|
+
addedAt: now
|
|
27203
|
+
// agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
|
|
27204
|
+
};
|
|
27205
|
+
entries.push(next);
|
|
27206
|
+
}
|
|
27207
|
+
this.writeFile(scope, sessionId, entries);
|
|
27208
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27209
|
+
return next;
|
|
27210
|
+
}
|
|
27211
|
+
/**
|
|
27212
|
+
* 标记一个 relPath stale(agent rm / mv 后文件不在)。
|
|
27213
|
+
* - 命中 → stale=true,UI 灰显
|
|
27214
|
+
* - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
|
|
27215
|
+
*
|
|
27216
|
+
* 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
|
|
27217
|
+
*/
|
|
27218
|
+
markStale(scope, sessionId, relPath) {
|
|
27219
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27220
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
27221
|
+
if (idx < 0) return;
|
|
27222
|
+
if (entries[idx].stale) return;
|
|
27223
|
+
entries[idx] = { ...entries[idx], stale: true };
|
|
27224
|
+
this.writeFile(scope, sessionId, entries);
|
|
27225
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27226
|
+
}
|
|
27227
|
+
/**
|
|
27228
|
+
* 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
|
|
27229
|
+
* agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
|
|
27230
|
+
* owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
|
|
27231
|
+
*
|
|
27232
|
+
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
27233
|
+
*/
|
|
27234
|
+
remove(scope, sessionId, relPath) {
|
|
27235
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27236
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
27237
|
+
if (idx < 0) return false;
|
|
27238
|
+
entries.splice(idx, 1);
|
|
27239
|
+
this.writeFile(scope, sessionId, entries);
|
|
27240
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27241
|
+
return true;
|
|
27242
|
+
}
|
|
27243
|
+
};
|
|
27244
|
+
|
|
26245
27245
|
// src/discovery/state-file.ts
|
|
26246
27246
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
26247
27247
|
var import_node_path17 = __toESM(require("path"), 1);
|
|
@@ -26918,39 +27918,44 @@ var AUTH_FILE_NAME = "auth.json";
|
|
|
26918
27918
|
function authFilePath(dataDir) {
|
|
26919
27919
|
return import_node_path22.default.join(dataDir, AUTH_FILE_NAME);
|
|
26920
27920
|
}
|
|
26921
|
-
function
|
|
27921
|
+
function loadOrCreateAuth(opts) {
|
|
26922
27922
|
const file = authFilePath(opts.dataDir);
|
|
26923
|
-
const generate = opts.generate ?? defaultGenerate;
|
|
26924
|
-
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
26925
27923
|
const existing = readAuthFile(file);
|
|
26926
|
-
|
|
26927
|
-
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
27924
|
+
const genToken = opts.generate ?? defaultGenerateToken2;
|
|
27925
|
+
const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
|
|
27926
|
+
const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
|
|
27927
|
+
if (existing) {
|
|
27928
|
+
if (!existing.ownerPrincipalId) {
|
|
27929
|
+
const ownerPrincipalId2 = genId();
|
|
27930
|
+
writeAuthFile(file, {
|
|
27931
|
+
token: existing.token,
|
|
27932
|
+
ownerPrincipalId: ownerPrincipalId2,
|
|
27933
|
+
createdAt: existing.createdAt
|
|
27934
|
+
});
|
|
27935
|
+
return { token: existing.token, ownerPrincipalId: ownerPrincipalId2 };
|
|
27936
|
+
}
|
|
27937
|
+
return { token: existing.token, ownerPrincipalId: existing.ownerPrincipalId };
|
|
26932
27938
|
}
|
|
26933
|
-
const token =
|
|
26934
|
-
const
|
|
26935
|
-
|
|
26936
|
-
|
|
26937
|
-
writeAuthFile(file, next);
|
|
26938
|
-
return next;
|
|
27939
|
+
const token = genToken();
|
|
27940
|
+
const ownerPrincipalId = genId();
|
|
27941
|
+
writeAuthFile(file, { token, ownerPrincipalId, createdAt: now.toISOString() });
|
|
27942
|
+
return { token, ownerPrincipalId };
|
|
26939
27943
|
}
|
|
26940
|
-
function
|
|
27944
|
+
function defaultGenerateToken2() {
|
|
26941
27945
|
return import_node_crypto8.default.randomBytes(32).toString("base64url");
|
|
26942
27946
|
}
|
|
27947
|
+
function defaultGenerateOwnerPrincipalId() {
|
|
27948
|
+
return `owner-${import_node_crypto8.default.randomUUID()}`;
|
|
27949
|
+
}
|
|
26943
27950
|
function readAuthFile(file) {
|
|
26944
27951
|
try {
|
|
26945
27952
|
const raw = import_node_fs20.default.readFileSync(file, "utf8");
|
|
26946
27953
|
const parsed = JSON.parse(raw);
|
|
26947
|
-
if (typeof parsed?.token !== "string" || parsed.token.length === 0)
|
|
26948
|
-
return null;
|
|
26949
|
-
}
|
|
27954
|
+
if (typeof parsed?.token !== "string" || parsed.token.length === 0) return null;
|
|
26950
27955
|
return {
|
|
26951
27956
|
token: parsed.token,
|
|
26952
|
-
|
|
26953
|
-
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt :
|
|
27957
|
+
ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : "",
|
|
27958
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
26954
27959
|
};
|
|
26955
27960
|
} catch (err) {
|
|
26956
27961
|
const code = err?.code;
|
|
@@ -27063,10 +28068,63 @@ function forkSession(input) {
|
|
|
27063
28068
|
return { forkedToolSessionId, forkedFilePath };
|
|
27064
28069
|
}
|
|
27065
28070
|
|
|
28071
|
+
// src/permission/capability.ts
|
|
28072
|
+
function matchResource(grant, target) {
|
|
28073
|
+
if (grant.type === "*") return true;
|
|
28074
|
+
if (grant.type !== target.type) return false;
|
|
28075
|
+
if (grant.id === "*") return true;
|
|
28076
|
+
return grant.id === target.id;
|
|
28077
|
+
}
|
|
28078
|
+
function assertGrant(grants, resource, action) {
|
|
28079
|
+
for (const g2 of grants) {
|
|
28080
|
+
if (!matchResource(g2.resource, resource)) continue;
|
|
28081
|
+
if (g2.actions.includes(action)) return true;
|
|
28082
|
+
if (g2.actions.includes("admin")) return true;
|
|
28083
|
+
}
|
|
28084
|
+
return false;
|
|
28085
|
+
}
|
|
28086
|
+
|
|
28087
|
+
// src/permission/session-access.ts
|
|
28088
|
+
function canAccessSession(ctx, sessionId, action, deps) {
|
|
28089
|
+
if (ctx.principal.kind === "owner") return true;
|
|
28090
|
+
const file = deps.readSession(sessionId);
|
|
28091
|
+
if (!file) return true;
|
|
28092
|
+
return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
|
|
28093
|
+
}
|
|
28094
|
+
function canAccessPersona(grants, personaId, action) {
|
|
28095
|
+
if (!personaId) return false;
|
|
28096
|
+
const resource = { type: "persona", id: personaId };
|
|
28097
|
+
return assertGrant(grants, resource, action);
|
|
28098
|
+
}
|
|
28099
|
+
|
|
27066
28100
|
// src/handlers/session.ts
|
|
28101
|
+
init_protocol();
|
|
27067
28102
|
function buildSessionHandlers(deps) {
|
|
27068
|
-
const { manager, observer, getAdapter: getAdapter2 } = deps;
|
|
27069
|
-
const
|
|
28103
|
+
const { manager, observer, getAdapter: getAdapter2, store, ownerPrincipalId } = deps;
|
|
28104
|
+
const ensureSessionAccess = (ctx, sessionId, action) => {
|
|
28105
|
+
if (!ctx) return;
|
|
28106
|
+
const ok = canAccessSession(ctx, sessionId, action, {
|
|
28107
|
+
readSession: (sid) => store.read(sid)
|
|
28108
|
+
});
|
|
28109
|
+
if (!ok) {
|
|
28110
|
+
throw new ClawdError(
|
|
28111
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28112
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} session ${sessionId}`
|
|
28113
|
+
);
|
|
28114
|
+
}
|
|
28115
|
+
};
|
|
28116
|
+
const ensurePersonaAccess = (ctx, personaId, action) => {
|
|
28117
|
+
if (!ctx) return;
|
|
28118
|
+
if (ctx.principal.kind === "owner") return;
|
|
28119
|
+
const ok = canAccessPersona(ctx.grants, personaId, action);
|
|
28120
|
+
if (!ok) {
|
|
28121
|
+
throw new ClawdError(
|
|
28122
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28123
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} on persona:${personaId ?? "<none>"}`
|
|
28124
|
+
);
|
|
28125
|
+
}
|
|
28126
|
+
};
|
|
28127
|
+
const create = async (frame, _client, ctx) => {
|
|
27070
28128
|
const args = SessionCreateArgs.parse(frame);
|
|
27071
28129
|
if (args.ownerPersonaId) {
|
|
27072
28130
|
const persona = deps.personaRegistry.get(args.ownerPersonaId);
|
|
@@ -27074,58 +28132,75 @@ function buildSessionHandlers(deps) {
|
|
|
27074
28132
|
throw new Error(`persona not found: ${args.ownerPersonaId}`);
|
|
27075
28133
|
}
|
|
27076
28134
|
}
|
|
27077
|
-
|
|
28135
|
+
ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
|
|
28136
|
+
const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
|
|
28137
|
+
const { response, broadcast } = manager.create(args, creatorPrincipalId);
|
|
27078
28138
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27079
28139
|
};
|
|
27080
|
-
const list = async () => {
|
|
28140
|
+
const list = async (_frame, _client, ctx) => {
|
|
27081
28141
|
const { response } = manager.list();
|
|
28142
|
+
if (ctx && ctx.principal.kind === "guest") {
|
|
28143
|
+
const sessions = response.sessions ?? [];
|
|
28144
|
+
const capId = ctx.capabilityId;
|
|
28145
|
+
const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === capId);
|
|
28146
|
+
return { response: { type: "session:list", sessions: filtered } };
|
|
28147
|
+
}
|
|
27082
28148
|
return { response: { type: "session:list", ...response } };
|
|
27083
28149
|
};
|
|
27084
|
-
const get = async (frame) => {
|
|
28150
|
+
const get = async (frame, _client, ctx) => {
|
|
27085
28151
|
const args = SessionIdArgs.parse(frame);
|
|
28152
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27086
28153
|
const { response } = manager.get(args);
|
|
27087
28154
|
return { response: { type: "session:info", ...response } };
|
|
27088
28155
|
};
|
|
27089
|
-
const update = async (frame) => {
|
|
28156
|
+
const update = async (frame, _client, ctx) => {
|
|
27090
28157
|
const args = SessionUpdateArgs.parse(frame);
|
|
28158
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27091
28159
|
const { response, broadcast } = manager.update(args);
|
|
27092
28160
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27093
28161
|
};
|
|
27094
|
-
const del = async (frame) => {
|
|
28162
|
+
const del = async (frame, _client, ctx) => {
|
|
27095
28163
|
const args = SessionIdArgs.parse(frame);
|
|
28164
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27096
28165
|
const { broadcast } = manager.delete(args);
|
|
27097
28166
|
return {
|
|
27098
28167
|
response: { type: "session:deleted", sessionId: args.sessionId },
|
|
27099
28168
|
broadcast
|
|
27100
28169
|
};
|
|
27101
28170
|
};
|
|
27102
|
-
const send = async (frame) => {
|
|
28171
|
+
const send = async (frame, _client, ctx) => {
|
|
27103
28172
|
const args = SessionSendArgs.parse(frame);
|
|
28173
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27104
28174
|
const { broadcast } = manager.send(args);
|
|
27105
28175
|
return { response: { type: "session:send", ok: true }, broadcast };
|
|
27106
28176
|
};
|
|
27107
|
-
const stop = async (frame) => {
|
|
28177
|
+
const stop = async (frame, _client, ctx) => {
|
|
27108
28178
|
const args = SessionIdArgs.parse(frame);
|
|
28179
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27109
28180
|
const { broadcast } = await manager.stop(args);
|
|
27110
28181
|
return { response: { type: "session:stop", ok: true }, broadcast };
|
|
27111
28182
|
};
|
|
27112
|
-
const interrupt = async (frame) => {
|
|
28183
|
+
const interrupt = async (frame, _client, ctx) => {
|
|
27113
28184
|
const args = SessionIdArgs.parse(frame);
|
|
28185
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27114
28186
|
const { broadcast } = await manager.interrupt(args);
|
|
27115
28187
|
return { response: { type: "session:interrupt", ok: true }, broadcast };
|
|
27116
28188
|
};
|
|
27117
|
-
const rewind = async (frame) => {
|
|
28189
|
+
const rewind = async (frame, _client, ctx) => {
|
|
27118
28190
|
const args = SessionRewindArgs.parse(frame);
|
|
28191
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27119
28192
|
const { response, broadcast } = await manager.rewind(args);
|
|
27120
28193
|
return { response: { type: "session:rewind", ...response }, broadcast };
|
|
27121
28194
|
};
|
|
27122
|
-
const rewindDiff = async (frame) => {
|
|
28195
|
+
const rewindDiff = async (frame, _client, ctx) => {
|
|
27123
28196
|
const args = SessionRewindDiffArgs.parse(frame);
|
|
28197
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27124
28198
|
const { response } = await manager.rewindDiff(args);
|
|
27125
28199
|
return { response: { type: "session:rewind-diff", ...response } };
|
|
27126
28200
|
};
|
|
27127
|
-
const rewindableMessageIds = async (frame) => {
|
|
28201
|
+
const rewindableMessageIds = async (frame, _client, ctx) => {
|
|
27128
28202
|
const args = SessionRewindableMessageIdsArgs.parse(frame);
|
|
28203
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27129
28204
|
const { response } = manager.rewindableMessageIds(args);
|
|
27130
28205
|
return {
|
|
27131
28206
|
response: { type: "session:rewindable-message-ids", ...response }
|
|
@@ -27136,19 +28211,22 @@ function buildSessionHandlers(deps) {
|
|
|
27136
28211
|
const result = forkSession(args);
|
|
27137
28212
|
return { response: { type: "session:fork", ...result } };
|
|
27138
28213
|
};
|
|
27139
|
-
const newSession = async (frame) => {
|
|
28214
|
+
const newSession = async (frame, _client, ctx) => {
|
|
27140
28215
|
const args = SessionIdArgs.parse(frame);
|
|
28216
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27141
28217
|
observer.stop(args.sessionId);
|
|
27142
28218
|
const { response, broadcast } = manager.newSession(args);
|
|
27143
28219
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27144
28220
|
};
|
|
27145
|
-
const resume = async (frame) => {
|
|
28221
|
+
const resume = async (frame, _client, ctx) => {
|
|
27146
28222
|
const args = SessionResumeArgs.parse(frame);
|
|
28223
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27147
28224
|
const { response, broadcast } = manager.resume(args);
|
|
27148
28225
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27149
28226
|
};
|
|
27150
|
-
const observe = async (frame) => {
|
|
28227
|
+
const observe = async (frame, _client, ctx) => {
|
|
27151
28228
|
const args = SessionObserveArgs.parse(frame);
|
|
28229
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27152
28230
|
const { response: file } = manager.get({ sessionId: args.sessionId });
|
|
27153
28231
|
const sessionFile = file;
|
|
27154
28232
|
manager.ensureSession(sessionFile);
|
|
@@ -27162,14 +28240,17 @@ function buildSessionHandlers(deps) {
|
|
|
27162
28240
|
});
|
|
27163
28241
|
return { response: { type: "session:observe", ok: true } };
|
|
27164
28242
|
};
|
|
27165
|
-
const events = async (frame) => {
|
|
28243
|
+
const events = async (frame, _client, ctx) => {
|
|
27166
28244
|
const args = SessionEventsArgs.parse(frame);
|
|
28245
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27167
28246
|
const { response } = manager.getEvents(args);
|
|
27168
28247
|
return { response: { type: "session:events", ...response } };
|
|
27169
28248
|
};
|
|
27170
|
-
const subscribe = async (frame, client) => {
|
|
27171
|
-
if (typeof frame.sessionId === "string")
|
|
28249
|
+
const subscribe = async (frame, client, ctx) => {
|
|
28250
|
+
if (typeof frame.sessionId === "string") {
|
|
28251
|
+
ensureSessionAccess(ctx, frame.sessionId, "read");
|
|
27172
28252
|
addSubscription(client, frame.sessionId);
|
|
28253
|
+
}
|
|
27173
28254
|
return {
|
|
27174
28255
|
response: { type: "subscribed", sessionId: frame.sessionId }
|
|
27175
28256
|
};
|
|
@@ -27181,8 +28262,9 @@ function buildSessionHandlers(deps) {
|
|
|
27181
28262
|
response: { type: "unsubscribed", sessionId: frame.sessionId }
|
|
27182
28263
|
};
|
|
27183
28264
|
};
|
|
27184
|
-
const pin = async (frame) => {
|
|
28265
|
+
const pin = async (frame, _client, ctx) => {
|
|
27185
28266
|
const args = SessionPinArgs.parse(frame);
|
|
28267
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27186
28268
|
const { response, broadcast } = manager.pin(args);
|
|
27187
28269
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27188
28270
|
};
|
|
@@ -27191,13 +28273,15 @@ function buildSessionHandlers(deps) {
|
|
|
27191
28273
|
const { response, broadcast } = manager.reorderPins(args);
|
|
27192
28274
|
return { response: { type: "session:reorderPins", ...response }, broadcast };
|
|
27193
28275
|
};
|
|
27194
|
-
const answerQuestion = async (frame) => {
|
|
28276
|
+
const answerQuestion = async (frame, _client, ctx) => {
|
|
27195
28277
|
const args = AnswerQuestionArgs.parse(frame);
|
|
28278
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27196
28279
|
const { response, broadcast } = manager.answerQuestion(args);
|
|
27197
28280
|
return { response: { type: "session:answerQuestion", ...response }, broadcast };
|
|
27198
28281
|
};
|
|
27199
|
-
const cancelQuestion = async (frame) => {
|
|
28282
|
+
const cancelQuestion = async (frame, _client, ctx) => {
|
|
27200
28283
|
const args = CancelQuestionArgs.parse(frame);
|
|
28284
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27201
28285
|
const { response, broadcast } = await manager.cancelQuestion(args);
|
|
27202
28286
|
return { response: { type: "session:cancelQuestion", ...response }, broadcast };
|
|
27203
28287
|
};
|
|
@@ -27292,6 +28376,7 @@ function listRecentDirs(store, limit = 50) {
|
|
|
27292
28376
|
}
|
|
27293
28377
|
|
|
27294
28378
|
// src/handlers/history.ts
|
|
28379
|
+
init_protocol();
|
|
27295
28380
|
function buildHistoryHandlers(deps) {
|
|
27296
28381
|
const { manager, history, store } = deps;
|
|
27297
28382
|
const projects = async () => {
|
|
@@ -27303,17 +28388,27 @@ function buildHistoryHandlers(deps) {
|
|
|
27303
28388
|
const sessions = await history.listSessions(args);
|
|
27304
28389
|
return { response: { type: "history:list", sessions } };
|
|
27305
28390
|
};
|
|
27306
|
-
const read = async (frame) => {
|
|
28391
|
+
const read = async (frame, _client, ctx) => {
|
|
27307
28392
|
const args = HistoryReadArgs.parse(frame);
|
|
27308
28393
|
const { response: file } = manager.get({ sessionId: args.sessionId });
|
|
27309
|
-
|
|
28394
|
+
const f = file;
|
|
28395
|
+
if (ctx && ctx.principal.kind === "guest") {
|
|
28396
|
+
const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
|
|
28397
|
+
if (!ok) {
|
|
28398
|
+
throw new ClawdError(
|
|
28399
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28400
|
+
`principal guest:${ctx.principal.id} cannot read history of session ${args.sessionId}`
|
|
28401
|
+
);
|
|
28402
|
+
}
|
|
28403
|
+
}
|
|
28404
|
+
if (!f.toolSessionId) {
|
|
27310
28405
|
return {
|
|
27311
28406
|
response: { type: "history:read", messages: [], total: 0, offset: 0 }
|
|
27312
28407
|
};
|
|
27313
28408
|
}
|
|
27314
28409
|
const res = await history.read({
|
|
27315
|
-
cwd:
|
|
27316
|
-
toolSessionId:
|
|
28410
|
+
cwd: f.cwd,
|
|
28411
|
+
toolSessionId: f.toolSessionId,
|
|
27317
28412
|
limit: args.limit,
|
|
27318
28413
|
offset: args.offset
|
|
27319
28414
|
});
|
|
@@ -27495,6 +28590,345 @@ function buildCapabilitiesHandlers(deps) {
|
|
|
27495
28590
|
};
|
|
27496
28591
|
}
|
|
27497
28592
|
|
|
28593
|
+
// src/handlers/capability.ts
|
|
28594
|
+
init_zod();
|
|
28595
|
+
init_protocol();
|
|
28596
|
+
var DeleteArgsSchema = external_exports.object({
|
|
28597
|
+
capabilityId: external_exports.string().min(1)
|
|
28598
|
+
}).strict();
|
|
28599
|
+
function buildCapabilityHandlers(deps) {
|
|
28600
|
+
const { manager, getShareBaseUrl } = deps;
|
|
28601
|
+
const issue = async (frame) => {
|
|
28602
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28603
|
+
const args = CapabilityIssueArgsSchema.parse(rest);
|
|
28604
|
+
const { token, capability } = manager.issue({
|
|
28605
|
+
displayName: args.displayName,
|
|
28606
|
+
grants: args.grants,
|
|
28607
|
+
...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
|
|
28608
|
+
...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
|
|
28609
|
+
});
|
|
28610
|
+
const base = getShareBaseUrl().replace(/\/$/, "");
|
|
28611
|
+
const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
|
|
28612
|
+
return {
|
|
28613
|
+
response: {
|
|
28614
|
+
type: "capability:issued",
|
|
28615
|
+
token,
|
|
28616
|
+
shareUrl,
|
|
28617
|
+
capability: stripSecretHash(capability)
|
|
28618
|
+
}
|
|
28619
|
+
};
|
|
28620
|
+
};
|
|
28621
|
+
const list = async () => {
|
|
28622
|
+
return {
|
|
28623
|
+
response: {
|
|
28624
|
+
type: "capability:list",
|
|
28625
|
+
capabilities: manager.list().map(stripSecretHash)
|
|
28626
|
+
}
|
|
28627
|
+
};
|
|
28628
|
+
};
|
|
28629
|
+
const del = async (frame) => {
|
|
28630
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28631
|
+
const args = DeleteArgsSchema.parse(rest);
|
|
28632
|
+
const result = manager.delete(args.capabilityId);
|
|
28633
|
+
if (!result) {
|
|
28634
|
+
throw new ClawdError(
|
|
28635
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28636
|
+
`capability not found: ${args.capabilityId}`
|
|
28637
|
+
);
|
|
28638
|
+
}
|
|
28639
|
+
return {
|
|
28640
|
+
response: {
|
|
28641
|
+
type: "capability:deleted",
|
|
28642
|
+
capabilityId: args.capabilityId
|
|
28643
|
+
}
|
|
28644
|
+
};
|
|
28645
|
+
};
|
|
28646
|
+
const bindPeer = async (frame) => {
|
|
28647
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28648
|
+
const args = CapabilityBindPeerArgsSchema.parse(rest);
|
|
28649
|
+
try {
|
|
28650
|
+
manager.recordPeerHello(args.capabilityId, {
|
|
28651
|
+
ownerPrincipalId: args.peerOwnerId,
|
|
28652
|
+
displayName: args.peerDisplayName,
|
|
28653
|
+
now: Date.now()
|
|
28654
|
+
});
|
|
28655
|
+
} catch (e) {
|
|
28656
|
+
throw new ClawdError(
|
|
28657
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28658
|
+
`capability:bindPeer: ${e.message}`
|
|
28659
|
+
);
|
|
28660
|
+
}
|
|
28661
|
+
return {
|
|
28662
|
+
response: { type: "capability:bindPeer:ok", capabilityId: args.capabilityId }
|
|
28663
|
+
};
|
|
28664
|
+
};
|
|
28665
|
+
return {
|
|
28666
|
+
"capability:issue": issue,
|
|
28667
|
+
"capability:list": list,
|
|
28668
|
+
"capability:delete": del,
|
|
28669
|
+
"capability:bindPeer": bindPeer
|
|
28670
|
+
};
|
|
28671
|
+
}
|
|
28672
|
+
|
|
28673
|
+
// src/handlers/inbox.ts
|
|
28674
|
+
init_protocol();
|
|
28675
|
+
function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
|
|
28676
|
+
if (ctx.principal.kind === "owner") {
|
|
28677
|
+
if (!argsPeerOwnerId) {
|
|
28678
|
+
throw new ClawdError(
|
|
28679
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28680
|
+
"inbox: owner ctx requires peerOwnerId"
|
|
28681
|
+
);
|
|
28682
|
+
}
|
|
28683
|
+
return argsPeerOwnerId;
|
|
28684
|
+
}
|
|
28685
|
+
if (!ctx.capabilityId) {
|
|
28686
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
|
|
28687
|
+
}
|
|
28688
|
+
const cap = capManager.findById(ctx.capabilityId);
|
|
28689
|
+
if (!cap?.peerOwnerId) {
|
|
28690
|
+
throw new ClawdError(
|
|
28691
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28692
|
+
`inbox: guest cap ${ctx.capabilityId} has no peerOwnerId (recordPeerHello not yet)`
|
|
28693
|
+
);
|
|
28694
|
+
}
|
|
28695
|
+
if (argsPeerOwnerId && argsPeerOwnerId !== cap.peerOwnerId) {
|
|
28696
|
+
throw new ClawdError(
|
|
28697
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28698
|
+
`inbox: guest args.peerOwnerId mismatch (cap.peerOwnerId=${cap.peerOwnerId}, args=${argsPeerOwnerId})`
|
|
28699
|
+
);
|
|
28700
|
+
}
|
|
28701
|
+
return cap.peerOwnerId;
|
|
28702
|
+
}
|
|
28703
|
+
function buildInboxHandlers(deps) {
|
|
28704
|
+
const { manager, capManager } = deps;
|
|
28705
|
+
const postMessage = async (frame, _client, ctx) => {
|
|
28706
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28707
|
+
const args = InboxPostMessageArgsSchema.parse(rest);
|
|
28708
|
+
if (!ctx) {
|
|
28709
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
|
|
28710
|
+
}
|
|
28711
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28712
|
+
const message = manager.postMessage({
|
|
28713
|
+
peerOwnerId,
|
|
28714
|
+
senderPrincipalId: ctx.principal.id,
|
|
28715
|
+
text: args.text,
|
|
28716
|
+
id: args.id,
|
|
28717
|
+
createdAt: args.createdAt
|
|
28718
|
+
});
|
|
28719
|
+
return {
|
|
28720
|
+
response: { type: "inbox:postMessage:ok", message }
|
|
28721
|
+
};
|
|
28722
|
+
};
|
|
28723
|
+
const list = async (frame, _client, ctx) => {
|
|
28724
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28725
|
+
const args = InboxListArgsSchema.parse(rest);
|
|
28726
|
+
if (!ctx) {
|
|
28727
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
|
|
28728
|
+
}
|
|
28729
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28730
|
+
const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
|
|
28731
|
+
return {
|
|
28732
|
+
response: { type: "inbox:list:ok", peerOwnerId, messages }
|
|
28733
|
+
};
|
|
28734
|
+
};
|
|
28735
|
+
const markRead = async (frame, _client, ctx) => {
|
|
28736
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28737
|
+
const args = InboxMarkReadArgsSchema.parse(rest);
|
|
28738
|
+
if (!ctx) {
|
|
28739
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
|
|
28740
|
+
}
|
|
28741
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28742
|
+
const updated = manager.markRead({
|
|
28743
|
+
peerOwnerId,
|
|
28744
|
+
principalId: ctx.principal.id,
|
|
28745
|
+
upToCreatedAt: args.upToCreatedAt
|
|
28746
|
+
});
|
|
28747
|
+
return {
|
|
28748
|
+
response: {
|
|
28749
|
+
type: "inbox:markRead:ok",
|
|
28750
|
+
peerOwnerId,
|
|
28751
|
+
upToCreatedAt: args.upToCreatedAt,
|
|
28752
|
+
updated
|
|
28753
|
+
}
|
|
28754
|
+
};
|
|
28755
|
+
};
|
|
28756
|
+
return {
|
|
28757
|
+
"inbox:postMessage": postMessage,
|
|
28758
|
+
"inbox:list": list,
|
|
28759
|
+
"inbox:markRead": markRead
|
|
28760
|
+
};
|
|
28761
|
+
}
|
|
28762
|
+
|
|
28763
|
+
// src/handlers/received-capability.ts
|
|
28764
|
+
init_protocol();
|
|
28765
|
+
function ensureOwner(ctx) {
|
|
28766
|
+
if (!ctx || ctx.principal.kind !== "owner") {
|
|
28767
|
+
throw new ClawdError(
|
|
28768
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28769
|
+
"UNAUTHORIZED: received-capability:* requires owner ctx"
|
|
28770
|
+
);
|
|
28771
|
+
}
|
|
28772
|
+
}
|
|
28773
|
+
function ensureGuestCtx(ctx) {
|
|
28774
|
+
if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
|
|
28775
|
+
throw new ClawdError(
|
|
28776
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28777
|
+
"UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
|
|
28778
|
+
);
|
|
28779
|
+
}
|
|
28780
|
+
}
|
|
28781
|
+
function buildReceivedCapabilityHandlers(deps) {
|
|
28782
|
+
const now = deps.now ?? Date.now;
|
|
28783
|
+
const list = async (_frame, _client, ctx) => {
|
|
28784
|
+
ensureOwner(ctx);
|
|
28785
|
+
return {
|
|
28786
|
+
response: {
|
|
28787
|
+
type: "received-capability:list:ok",
|
|
28788
|
+
receivedCaps: deps.store.list()
|
|
28789
|
+
}
|
|
28790
|
+
};
|
|
28791
|
+
};
|
|
28792
|
+
const add = async (frame, _client, ctx) => {
|
|
28793
|
+
ensureOwner(ctx);
|
|
28794
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28795
|
+
const args = ReceivedCapabilityAddArgsSchema.parse(rest);
|
|
28796
|
+
let conn;
|
|
28797
|
+
try {
|
|
28798
|
+
conn = await deps.connectRemote({
|
|
28799
|
+
url: args.remoteUrl,
|
|
28800
|
+
token: args.token,
|
|
28801
|
+
selfPrincipalId: deps.selfPrincipalId(),
|
|
28802
|
+
selfDisplayName: deps.selfDisplayName()
|
|
28803
|
+
});
|
|
28804
|
+
} catch (e) {
|
|
28805
|
+
throw new ClawdError(
|
|
28806
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28807
|
+
`INVITE_REMOTE_UNREACHABLE: ${e.message}`
|
|
28808
|
+
);
|
|
28809
|
+
}
|
|
28810
|
+
try {
|
|
28811
|
+
const wh = await conn.whoami();
|
|
28812
|
+
const rc = {
|
|
28813
|
+
ownerPrincipalId: wh.ownerId,
|
|
28814
|
+
ownerDisplayName: wh.displayName,
|
|
28815
|
+
remoteUrl: args.remoteUrl,
|
|
28816
|
+
capabilityId: wh.capabilityId,
|
|
28817
|
+
capabilityToken: args.token,
|
|
28818
|
+
grants: wh.grants,
|
|
28819
|
+
receivedAt: now()
|
|
28820
|
+
};
|
|
28821
|
+
deps.store.upsert(rc);
|
|
28822
|
+
deps.broadcast({ type: "received-capability:added", receivedCap: rc });
|
|
28823
|
+
return {
|
|
28824
|
+
response: { type: "received-capability:add:ok", receivedCap: rc }
|
|
28825
|
+
};
|
|
28826
|
+
} finally {
|
|
28827
|
+
try {
|
|
28828
|
+
conn.close();
|
|
28829
|
+
} catch {
|
|
28830
|
+
}
|
|
28831
|
+
}
|
|
28832
|
+
};
|
|
28833
|
+
const remove = async (frame, _client, ctx) => {
|
|
28834
|
+
ensureOwner(ctx);
|
|
28835
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28836
|
+
const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
|
|
28837
|
+
deps.store.remove(args.ownerPrincipalId);
|
|
28838
|
+
deps.broadcast({
|
|
28839
|
+
type: "received-capability:removed",
|
|
28840
|
+
ownerPrincipalId: args.ownerPrincipalId
|
|
28841
|
+
});
|
|
28842
|
+
return {
|
|
28843
|
+
response: {
|
|
28844
|
+
type: "received-capability:remove:ok",
|
|
28845
|
+
ownerPrincipalId: args.ownerPrincipalId
|
|
28846
|
+
}
|
|
28847
|
+
};
|
|
28848
|
+
};
|
|
28849
|
+
const autoAttach = async (frame, _client, ctx) => {
|
|
28850
|
+
ensureGuestCtx(ctx);
|
|
28851
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28852
|
+
const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
|
|
28853
|
+
const rc = {
|
|
28854
|
+
ownerPrincipalId: args.ownerPrincipalId,
|
|
28855
|
+
ownerDisplayName: args.ownerDisplayName,
|
|
28856
|
+
remoteUrl: args.remoteUrl,
|
|
28857
|
+
capabilityId: args.capabilityId,
|
|
28858
|
+
capabilityToken: args.token,
|
|
28859
|
+
grants: args.grants,
|
|
28860
|
+
receivedAt: now()
|
|
28861
|
+
};
|
|
28862
|
+
deps.store.upsert(rc);
|
|
28863
|
+
deps.broadcast({ type: "received-capability:added", receivedCap: rc });
|
|
28864
|
+
return {
|
|
28865
|
+
response: { type: "received-capability:autoAttach:ok" }
|
|
28866
|
+
};
|
|
28867
|
+
};
|
|
28868
|
+
return {
|
|
28869
|
+
"received-capability:list": list,
|
|
28870
|
+
"received-capability:add": add,
|
|
28871
|
+
"received-capability:remove": remove,
|
|
28872
|
+
"received-capability:autoAttach": autoAttach
|
|
28873
|
+
};
|
|
28874
|
+
}
|
|
28875
|
+
|
|
28876
|
+
// src/handlers/whoami.ts
|
|
28877
|
+
init_protocol();
|
|
28878
|
+
function buildWhoamiHandler(deps) {
|
|
28879
|
+
return async (_frame, _client, ctx) => {
|
|
28880
|
+
if (!ctx) {
|
|
28881
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
|
|
28882
|
+
}
|
|
28883
|
+
const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
|
|
28884
|
+
let capability;
|
|
28885
|
+
if (ctx.principal.kind === "owner") {
|
|
28886
|
+
capability = {
|
|
28887
|
+
id: deps.ownerPrincipalId,
|
|
28888
|
+
displayName: deps.ownerDisplayName,
|
|
28889
|
+
grants: ctx.grants,
|
|
28890
|
+
issuedAt: 0,
|
|
28891
|
+
usedCount: 0
|
|
28892
|
+
};
|
|
28893
|
+
} else {
|
|
28894
|
+
if (!ctx.capabilityId) {
|
|
28895
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
|
|
28896
|
+
}
|
|
28897
|
+
const cap = deps.capabilityManager.findById(ctx.capabilityId);
|
|
28898
|
+
if (!cap) {
|
|
28899
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
|
|
28900
|
+
}
|
|
28901
|
+
capability = stripSecretHash(cap);
|
|
28902
|
+
}
|
|
28903
|
+
const grantedPersonas = [];
|
|
28904
|
+
const isGuest = ctx.principal.kind === "guest";
|
|
28905
|
+
const hasWildcard = capability.grants.some((g2) => g2.resource.type === "*");
|
|
28906
|
+
if (hasWildcard) {
|
|
28907
|
+
for (const id of deps.personaStore.list()) {
|
|
28908
|
+
const file = deps.personaStore.read(id);
|
|
28909
|
+
if (!file) continue;
|
|
28910
|
+
if (isGuest && !file.public) continue;
|
|
28911
|
+
grantedPersonas.push({ id: file.personaId, displayName: file.label });
|
|
28912
|
+
}
|
|
28913
|
+
} else {
|
|
28914
|
+
for (const g2 of capability.grants) {
|
|
28915
|
+
if (g2.resource.type !== "persona") continue;
|
|
28916
|
+
const file = deps.personaStore.read(g2.resource.id);
|
|
28917
|
+
if (!file) continue;
|
|
28918
|
+
grantedPersonas.push({ id: file.personaId, displayName: file.label });
|
|
28919
|
+
}
|
|
28920
|
+
}
|
|
28921
|
+
return {
|
|
28922
|
+
response: {
|
|
28923
|
+
type: "whoami:ok",
|
|
28924
|
+
owner,
|
|
28925
|
+
capability,
|
|
28926
|
+
grantedPersonas
|
|
28927
|
+
}
|
|
28928
|
+
};
|
|
28929
|
+
};
|
|
28930
|
+
}
|
|
28931
|
+
|
|
27498
28932
|
// src/handlers/meta.ts
|
|
27499
28933
|
var import_node_os13 = __toESM(require("os"), 1);
|
|
27500
28934
|
init_protocol();
|
|
@@ -27558,9 +28992,14 @@ function buildPersonaHandlers(deps) {
|
|
|
27558
28992
|
const persona = personaManager.create(args);
|
|
27559
28993
|
return { response: { type: "persona:info", ...persona } };
|
|
27560
28994
|
};
|
|
27561
|
-
const list = async () => {
|
|
28995
|
+
const list = async (_frame, _client, ctx) => {
|
|
28996
|
+
const all = personaRegistry.list();
|
|
28997
|
+
const isGuest = ctx?.principal.kind === "guest";
|
|
28998
|
+
const personas = isGuest ? all.filter(
|
|
28999
|
+
(p2) => p2.public && assertGrant(ctx.grants, { type: "persona", id: p2.personaId }, "read")
|
|
29000
|
+
) : all;
|
|
27562
29001
|
return {
|
|
27563
|
-
response: { type: "persona:list", personas
|
|
29002
|
+
response: { type: "persona:list", personas }
|
|
27564
29003
|
};
|
|
27565
29004
|
};
|
|
27566
29005
|
const get = async (frame) => {
|
|
@@ -27571,7 +29010,6 @@ function buildPersonaHandlers(deps) {
|
|
|
27571
29010
|
}
|
|
27572
29011
|
const personality = personaManager.readPersonality(args.personaId) ?? "";
|
|
27573
29012
|
const skills = personaManager.listSkills(args.personaId);
|
|
27574
|
-
const plugins = personaManager.listEnabledPlugins(args.personaId);
|
|
27575
29013
|
const sandboxSettings = personaManager.readSandboxSettings(args.personaId);
|
|
27576
29014
|
return {
|
|
27577
29015
|
response: {
|
|
@@ -27579,7 +29017,6 @@ function buildPersonaHandlers(deps) {
|
|
|
27579
29017
|
...persona,
|
|
27580
29018
|
personality,
|
|
27581
29019
|
skills,
|
|
27582
|
-
plugins,
|
|
27583
29020
|
sandboxSettings
|
|
27584
29021
|
}
|
|
27585
29022
|
};
|
|
@@ -27598,31 +29035,16 @@ function buildPersonaHandlers(deps) {
|
|
|
27598
29035
|
response: { type: "persona:deleted", personaId: args.personaId }
|
|
27599
29036
|
};
|
|
27600
29037
|
};
|
|
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
|
-
};
|
|
27613
29038
|
return {
|
|
27614
29039
|
"persona:create": create,
|
|
27615
29040
|
"persona:list": list,
|
|
27616
29041
|
"persona:get": get,
|
|
27617
29042
|
"persona:update": update,
|
|
27618
|
-
"persona:delete": del
|
|
27619
|
-
"persona:issueToken": issueToken,
|
|
27620
|
-
"persona:revokeToken": revokeToken
|
|
29043
|
+
"persona:delete": del
|
|
27621
29044
|
};
|
|
27622
29045
|
}
|
|
27623
29046
|
|
|
27624
29047
|
// src/handlers/attachment.ts
|
|
27625
|
-
var import_node_path26 = __toESM(require("path"), 1);
|
|
27626
29048
|
init_protocol();
|
|
27627
29049
|
init_protocol();
|
|
27628
29050
|
var DEFAULT_TTL_SECONDS = 24 * 3600;
|
|
@@ -27647,41 +29069,8 @@ function buildAttachmentHandlers(deps) {
|
|
|
27647
29069
|
"httpBaseUrl unavailable (daemon HTTP not ready)"
|
|
27648
29070
|
);
|
|
27649
29071
|
}
|
|
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
|
-
}
|
|
27683
29072
|
const ttl = args.ttlSeconds === null ? null : args.ttlSeconds ?? DEFAULT_TTL_SECONDS;
|
|
27684
|
-
const parts = signUrlParts(secret,
|
|
29073
|
+
const parts = signUrlParts(secret, args.absPath, ttl);
|
|
27685
29074
|
const url = buildSignedFileUrl(httpBaseUrl, parts);
|
|
27686
29075
|
return {
|
|
27687
29076
|
response: {
|
|
@@ -27748,23 +29137,11 @@ function buildAttachmentHandlers(deps) {
|
|
|
27748
29137
|
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
27749
29138
|
return { response: { type: "attachment.groupList", entries } };
|
|
27750
29139
|
};
|
|
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
|
-
};
|
|
27762
29140
|
return {
|
|
27763
29141
|
"attachment.signUrl": signUrl,
|
|
27764
29142
|
"attachment.groupAdd": groupAdd,
|
|
27765
29143
|
"attachment.groupRemove": groupRemove,
|
|
27766
|
-
"attachment.groupList": groupList
|
|
27767
|
-
"attachment.groupListPersona": groupListPersona
|
|
29144
|
+
"attachment.groupList": groupList
|
|
27768
29145
|
};
|
|
27769
29146
|
}
|
|
27770
29147
|
|
|
@@ -27782,15 +29159,141 @@ function buildMethodHandlers(deps) {
|
|
|
27782
29159
|
personaManager: deps.personaManager,
|
|
27783
29160
|
personaRegistry: deps.personaRegistry
|
|
27784
29161
|
}),
|
|
29162
|
+
...buildCapabilityHandlers({
|
|
29163
|
+
manager: deps.capabilityManager,
|
|
29164
|
+
getShareBaseUrl: deps.getShareBaseUrl
|
|
29165
|
+
}),
|
|
29166
|
+
...buildInboxHandlers({
|
|
29167
|
+
manager: deps.inboxManager,
|
|
29168
|
+
capManager: deps.capabilityManager
|
|
29169
|
+
}),
|
|
29170
|
+
...buildReceivedCapabilityHandlers({
|
|
29171
|
+
store: deps.receivedCapabilityStore,
|
|
29172
|
+
connectRemote: deps.connectRemote,
|
|
29173
|
+
broadcast: deps.broadcastToOwners,
|
|
29174
|
+
selfPrincipalId: () => deps.ownerPrincipalId,
|
|
29175
|
+
selfDisplayName: () => deps.ownerDisplayName
|
|
29176
|
+
}),
|
|
29177
|
+
whoami: buildWhoamiHandler({
|
|
29178
|
+
ownerDisplayName: deps.ownerDisplayName,
|
|
29179
|
+
ownerPrincipalId: deps.ownerPrincipalId,
|
|
29180
|
+
personaStore: deps.personaStore,
|
|
29181
|
+
capabilityManager: deps.capabilityManager
|
|
29182
|
+
}),
|
|
27785
29183
|
...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
|
|
27786
29184
|
};
|
|
27787
29185
|
}
|
|
27788
29186
|
|
|
29187
|
+
// src/handlers/method-grants.ts
|
|
29188
|
+
var ADMIN_ANY = {
|
|
29189
|
+
kind: "fixed",
|
|
29190
|
+
resource: { type: "*" },
|
|
29191
|
+
action: "admin"
|
|
29192
|
+
};
|
|
29193
|
+
var CAPABILITY_SCOPED = { kind: "capability-scoped" };
|
|
29194
|
+
var METHOD_GRANT_MAP = {
|
|
29195
|
+
// ---- public(meta-only,guest 也能调) ----
|
|
29196
|
+
"info": { kind: "public" },
|
|
29197
|
+
"ping": { kind: "public" },
|
|
29198
|
+
// v2 Phase 6: whoami 是身份反查,任何 authed connection (owner / guest) 都能调;
|
|
29199
|
+
// handler 内部按 ctx.principal.kind 决定返回 "合成 owner self capability" 还是
|
|
29200
|
+
// "查 capabilityManager 拿 caller 的 guest capability"。
|
|
29201
|
+
"whoami": { kind: "public" },
|
|
29202
|
+
// ---- capability platform(admin-only,本 PR 新增) ----
|
|
29203
|
+
"capability:issue": ADMIN_ANY,
|
|
29204
|
+
"capability:list": ADMIN_ANY,
|
|
29205
|
+
"capability:delete": ADMIN_ANY,
|
|
29206
|
+
"capability:bindPeer": ADMIN_ANY,
|
|
29207
|
+
// ---- inbox: channel-based IM (capability platform v3) ----
|
|
29208
|
+
// 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
|
|
29209
|
+
// guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
|
|
29210
|
+
"inbox:list": { kind: "public" },
|
|
29211
|
+
"inbox:markRead": { kind: "public" },
|
|
29212
|
+
"inbox:postMessage": { kind: "public" },
|
|
29213
|
+
// ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
|
|
29214
|
+
// owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
|
|
29215
|
+
"received-capability:list": ADMIN_ANY,
|
|
29216
|
+
"received-capability:add": ADMIN_ANY,
|
|
29217
|
+
"received-capability:remove": ADMIN_ANY,
|
|
29218
|
+
"received-capability:autoAttach": { kind: "public" },
|
|
29219
|
+
// ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
|
|
29220
|
+
// dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
|
|
29221
|
+
// owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
|
|
29222
|
+
"session:create": CAPABILITY_SCOPED,
|
|
29223
|
+
"session:list": CAPABILITY_SCOPED,
|
|
29224
|
+
"session:get": CAPABILITY_SCOPED,
|
|
29225
|
+
"session:update": CAPABILITY_SCOPED,
|
|
29226
|
+
"session:delete": CAPABILITY_SCOPED,
|
|
29227
|
+
"session:send": CAPABILITY_SCOPED,
|
|
29228
|
+
"session:stop": CAPABILITY_SCOPED,
|
|
29229
|
+
"session:interrupt": CAPABILITY_SCOPED,
|
|
29230
|
+
"session:rewind": CAPABILITY_SCOPED,
|
|
29231
|
+
"session:rewind-diff": CAPABILITY_SCOPED,
|
|
29232
|
+
"session:rewindable-message-ids": CAPABILITY_SCOPED,
|
|
29233
|
+
"session:fork": CAPABILITY_SCOPED,
|
|
29234
|
+
"session:new": CAPABILITY_SCOPED,
|
|
29235
|
+
"session:resume": CAPABILITY_SCOPED,
|
|
29236
|
+
"session:observe": CAPABILITY_SCOPED,
|
|
29237
|
+
"session:events": CAPABILITY_SCOPED,
|
|
29238
|
+
"session:subscribe": CAPABILITY_SCOPED,
|
|
29239
|
+
"session:unsubscribe": CAPABILITY_SCOPED,
|
|
29240
|
+
"session:pin": CAPABILITY_SCOPED,
|
|
29241
|
+
"session:reorderPins": ADMIN_ANY,
|
|
29242
|
+
// owner 全局操作,无 personaId 维度
|
|
29243
|
+
"permission:respond": CAPABILITY_SCOPED,
|
|
29244
|
+
"session:answerQuestion": CAPABILITY_SCOPED,
|
|
29245
|
+
"session:cancelQuestion": CAPABILITY_SCOPED,
|
|
29246
|
+
"history:projects": ADMIN_ANY,
|
|
29247
|
+
"history:list": ADMIN_ANY,
|
|
29248
|
+
// history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
|
|
29249
|
+
// 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
|
|
29250
|
+
// handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
|
|
29251
|
+
"history:read": CAPABILITY_SCOPED,
|
|
29252
|
+
"history:subagents": ADMIN_ANY,
|
|
29253
|
+
"history:subagent-read": ADMIN_ANY,
|
|
29254
|
+
"history:recentDirs": ADMIN_ANY,
|
|
29255
|
+
"workspace:list": ADMIN_ANY,
|
|
29256
|
+
"workspace:read": ADMIN_ANY,
|
|
29257
|
+
"skills:list": ADMIN_ANY,
|
|
29258
|
+
"agents:list": ADMIN_ANY,
|
|
29259
|
+
"git:root": ADMIN_ANY,
|
|
29260
|
+
"git:branch": ADMIN_ANY,
|
|
29261
|
+
"git:branches": ADMIN_ANY,
|
|
29262
|
+
"capabilities:get": ADMIN_ANY,
|
|
29263
|
+
"persona:create": ADMIN_ANY,
|
|
29264
|
+
// persona:list 下沉到 CAPABILITY_SCOPED:guest 调用合法(用于 People 面板展开
|
|
29265
|
+
// 对方分享的 personas),handler 端按 ctx + persona.public + assertGrant 过滤。
|
|
29266
|
+
"persona:list": CAPABILITY_SCOPED,
|
|
29267
|
+
"persona:get": ADMIN_ANY,
|
|
29268
|
+
"persona:update": ADMIN_ANY,
|
|
29269
|
+
"persona:delete": ADMIN_ANY,
|
|
29270
|
+
"session:pty:input": ADMIN_ANY,
|
|
29271
|
+
"session:pty:resize": ADMIN_ANY,
|
|
29272
|
+
// file-sharing attachment.* RPC:dispatcher 用 admin-only 兜底(wire-level 拦),
|
|
29273
|
+
// 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
|
|
29274
|
+
"attachment.signUrl": ADMIN_ANY,
|
|
29275
|
+
"attachment.groupAdd": ADMIN_ANY,
|
|
29276
|
+
"attachment.groupRemove": ADMIN_ANY,
|
|
29277
|
+
"attachment.groupList": ADMIN_ANY
|
|
29278
|
+
};
|
|
29279
|
+
function computeGrantForFrame(method, frame) {
|
|
29280
|
+
const rule = METHOD_GRANT_MAP[method];
|
|
29281
|
+
if (!rule) return { kind: "public" };
|
|
29282
|
+
if (rule.kind === "public") return { kind: "public" };
|
|
29283
|
+
if (rule.kind === "capability-scoped") return { kind: "public" };
|
|
29284
|
+
if (rule.kind === "fixed") {
|
|
29285
|
+
return { kind: "check", resource: rule.resource, action: rule.action };
|
|
29286
|
+
}
|
|
29287
|
+
const picked = rule.pick(frame);
|
|
29288
|
+
if (!picked) return { kind: "public" };
|
|
29289
|
+
return { kind: "check", resource: picked.resource, action: picked.action };
|
|
29290
|
+
}
|
|
29291
|
+
|
|
27789
29292
|
// src/index.ts
|
|
27790
29293
|
async function startDaemon(config) {
|
|
27791
29294
|
const logger = createLogger({
|
|
27792
29295
|
level: config.logLevel,
|
|
27793
|
-
file:
|
|
29296
|
+
file: import_node_path26.default.join(config.dataDir, "clawd.log")
|
|
27794
29297
|
});
|
|
27795
29298
|
logger.info("starting clawd", { version, config: { port: config.port, host: config.host, dataDir: config.dataDir } });
|
|
27796
29299
|
const stateMgr = new StateFileManager({ dataDir: config.dataDir });
|
|
@@ -27801,47 +29304,116 @@ async function startDaemon(config) {
|
|
|
27801
29304
|
if (pre.status === "stale") {
|
|
27802
29305
|
logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
|
|
27803
29306
|
}
|
|
29307
|
+
const auth = loadOrCreateAuth({ dataDir: config.dataDir });
|
|
27804
29308
|
let resolvedAuthToken = null;
|
|
27805
|
-
let authFile = null;
|
|
27806
29309
|
if (config.authToken && config.authToken.trim()) {
|
|
27807
29310
|
resolvedAuthToken = config.authToken.trim();
|
|
27808
29311
|
} else if (config.tunnel) {
|
|
27809
|
-
|
|
27810
|
-
resolvedAuthToken = authFile.token;
|
|
29312
|
+
resolvedAuthToken = auth.token;
|
|
27811
29313
|
}
|
|
29314
|
+
const ownerPrincipalId = auth.ownerPrincipalId;
|
|
29315
|
+
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
27812
29316
|
const authMode = resolvedAuthToken == null ? "none" : "first-message";
|
|
29317
|
+
migrateLegacyFiles(config.dataDir);
|
|
29318
|
+
const inboxStore = new InboxStore(config.dataDir);
|
|
29319
|
+
const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
|
|
29320
|
+
wsServer?.broadcastToOwners(frame);
|
|
29321
|
+
});
|
|
29322
|
+
const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
|
|
29323
|
+
receivedCapabilityStore.load();
|
|
29324
|
+
const capabilityStore = new CapabilityStore(config.dataDir);
|
|
29325
|
+
const capabilityRegistry = new CapabilityRegistry(capabilityStore);
|
|
29326
|
+
const capabilityManager = new CapabilityManager(capabilityRegistry, {
|
|
29327
|
+
onIssued: (cap, token) => {
|
|
29328
|
+
wsServer?.broadcastToOwners({
|
|
29329
|
+
type: "capability:tokenIssued",
|
|
29330
|
+
capability: stripSecretHash(cap),
|
|
29331
|
+
token
|
|
29332
|
+
});
|
|
29333
|
+
},
|
|
29334
|
+
onDeleted: (cap) => {
|
|
29335
|
+
const deletedAt = Date.now();
|
|
29336
|
+
wsServer?.broadcastToOwners({
|
|
29337
|
+
type: "capability:tokenDeleted",
|
|
29338
|
+
capabilityId: cap.id,
|
|
29339
|
+
deletedAt
|
|
29340
|
+
});
|
|
29341
|
+
wsServer?.closeConnectionsByCapability(cap.id);
|
|
29342
|
+
const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
|
|
29343
|
+
if (cleanup.removed.length > 0) {
|
|
29344
|
+
logger.info("capability delete cascade: guest sessions removed", {
|
|
29345
|
+
capabilityId: cap.id,
|
|
29346
|
+
removedDirs: cleanup.removed
|
|
29347
|
+
});
|
|
29348
|
+
}
|
|
29349
|
+
}
|
|
29350
|
+
});
|
|
29351
|
+
migrateDropPersonsDir(config.dataDir);
|
|
27813
29352
|
let wsServer = null;
|
|
27814
29353
|
const authGate = authMode === "first-message" ? new AuthGate({
|
|
27815
29354
|
shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
|
|
29355
|
+
// Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
|
|
29356
|
+
// owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
|
|
27816
29357
|
expectedToken: resolvedAuthToken,
|
|
29358
|
+
authenticate: (t, selfPrincipalId, selfDisplayName) => {
|
|
29359
|
+
return authenticate(t, {
|
|
29360
|
+
isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
|
|
29361
|
+
ownerPrincipalId,
|
|
29362
|
+
ownerDisplayName,
|
|
29363
|
+
capabilityRegistry,
|
|
29364
|
+
selfPrincipalId,
|
|
29365
|
+
selfDisplayName,
|
|
29366
|
+
onGuestHello: (capId, ownerId, name) => {
|
|
29367
|
+
capabilityManager.recordPeerHello(capId, {
|
|
29368
|
+
ownerPrincipalId: ownerId,
|
|
29369
|
+
displayName: name,
|
|
29370
|
+
now: Date.now()
|
|
29371
|
+
});
|
|
29372
|
+
}
|
|
29373
|
+
});
|
|
29374
|
+
},
|
|
29375
|
+
onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
|
|
29376
|
+
buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
|
|
27817
29377
|
closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
|
|
27818
29378
|
sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
|
|
27819
29379
|
}) : null;
|
|
27820
29380
|
resetRegistry();
|
|
27821
|
-
const
|
|
29381
|
+
const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
|
|
29382
|
+
if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
|
|
29383
|
+
logger.info("sessions migration applied", {
|
|
29384
|
+
movedBare: migrateResult.movedBare,
|
|
29385
|
+
movedVmOwner: migrateResult.movedVmOwner,
|
|
29386
|
+
archivedListener: migrateResult.archivedListener
|
|
29387
|
+
});
|
|
29388
|
+
}
|
|
29389
|
+
const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
|
|
29390
|
+
const store = sessionStoreFactory.forBare();
|
|
27822
29391
|
const workspace = new WorkspaceBrowser();
|
|
27823
29392
|
const skills = new SkillsScanner();
|
|
27824
29393
|
const agents = new AgentsScanner();
|
|
27825
29394
|
const history = new ClaudeHistoryReader();
|
|
27826
29395
|
let transport = null;
|
|
27827
|
-
const personaStore = new PersonaStore(
|
|
29396
|
+
const personaStore = new PersonaStore(import_node_path26.default.join(config.dataDir, "personas"));
|
|
27828
29397
|
const defaultsRoot = findDefaultsRoot();
|
|
27829
29398
|
if (defaultsRoot) {
|
|
27830
29399
|
seedDefaultPersonas({ store: personaStore, defaultsRoot, logger });
|
|
27831
29400
|
} else {
|
|
27832
29401
|
logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
|
|
27833
29402
|
}
|
|
27834
|
-
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
27835
29403
|
const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
|
|
27836
29404
|
const manager = new SessionManager({
|
|
27837
29405
|
store,
|
|
29406
|
+
// Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
|
|
29407
|
+
// 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
|
|
29408
|
+
storeFactory: sessionStoreFactory,
|
|
27838
29409
|
logger,
|
|
27839
29410
|
getAdapter,
|
|
27840
29411
|
historyReader: history,
|
|
27841
29412
|
dataDir: config.dataDir,
|
|
27842
|
-
personaRoot:
|
|
29413
|
+
personaRoot: import_node_path26.default.join(config.dataDir, "personas"),
|
|
27843
29414
|
personaStore,
|
|
27844
29415
|
ownerDisplayName,
|
|
29416
|
+
ownerPrincipalId,
|
|
27845
29417
|
mode: config.mode,
|
|
27846
29418
|
broadcastFrame: (frame, target) => {
|
|
27847
29419
|
if (target === "all") {
|
|
@@ -27862,7 +29434,7 @@ async function startDaemon(config) {
|
|
|
27862
29434
|
// 文件可能 agent 写完又被自己删(罕见),用 size=0 / fallback mime 兜底。
|
|
27863
29435
|
attachmentGroup: {
|
|
27864
29436
|
onFileEdit: (input) => {
|
|
27865
|
-
const absPath =
|
|
29437
|
+
const absPath = import_node_path26.default.isAbsolute(input.relPath) ? input.relPath : import_node_path26.default.join(input.cwd, input.relPath);
|
|
27866
29438
|
let size = 0;
|
|
27867
29439
|
try {
|
|
27868
29440
|
size = import_node_fs24.default.statSync(absPath).size;
|
|
@@ -27961,43 +29533,63 @@ async function startDaemon(config) {
|
|
|
27961
29533
|
httpToken: resolvedAuthToken,
|
|
27962
29534
|
// file-sharing attachment.* RPC。signUrl 用 owner token 做 HMAC secret;group RPC
|
|
27963
29535
|
// 根据 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 才被暴露。
|
|
27970
29536
|
attachment: {
|
|
27971
29537
|
groupFileStore,
|
|
27972
|
-
sessionStore: { read: (sid) => manager.findOwnedSession(sid) },
|
|
27973
29538
|
getHttpBaseUrl,
|
|
27974
|
-
// HMAC sign secret
|
|
27975
|
-
//
|
|
27976
|
-
getSignSecret: () =>
|
|
27977
|
-
// group RPC
|
|
29539
|
+
// HMAC sign secret:复用 ~/.clawd/auth.json owner token(持久跨重启)。
|
|
29540
|
+
// noAuth 模式 resolvedAuthToken 为 null → handler 自己返 NOT_IMPLEMENTED。
|
|
29541
|
+
getSignSecret: () => resolvedAuthToken ?? "",
|
|
29542
|
+
// group RPC:根据 sessionId 反查 scope;owner-mode persona session 走
|
|
27978
29543
|
// 'persona/<pid>/owner',default 走 'default'。
|
|
27979
|
-
getSessionScope: (
|
|
27980
|
-
|
|
29544
|
+
getSessionScope: (sessionId) => {
|
|
29545
|
+
const file = store.read(sessionId);
|
|
29546
|
+
if (!file) return null;
|
|
29547
|
+
if (file.ownerPersonaId) {
|
|
29548
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
|
|
29549
|
+
}
|
|
29550
|
+
return { kind: "default" };
|
|
29551
|
+
}
|
|
29552
|
+
},
|
|
29553
|
+
// Task 1.9: capability:issue/list/revoke handler 依赖
|
|
29554
|
+
capabilityManager,
|
|
29555
|
+
// v2 Phase 5: capability:issue 返回的 shareUrl 用此 base URL 拼接。
|
|
29556
|
+
// tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
|
|
29557
|
+
getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
|
|
29558
|
+
// v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
|
|
29559
|
+
ownerDisplayName,
|
|
29560
|
+
// owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
|
|
29561
|
+
ownerPrincipalId,
|
|
29562
|
+
personaStore,
|
|
29563
|
+
// capability handler 也用 (capability:issue 走 registry / capability:list)
|
|
29564
|
+
capabilityRegistry,
|
|
29565
|
+
// capability platform v3 inbox: handler 接 manager (post/list/markRead),
|
|
29566
|
+
// cascade 接 store (list 统计 deletedInboxEvents).
|
|
29567
|
+
inboxManager,
|
|
29568
|
+
inboxStore,
|
|
29569
|
+
// bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
|
|
29570
|
+
receivedCapabilityStore,
|
|
29571
|
+
// received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
|
|
29572
|
+
broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
|
|
29573
|
+
// received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
|
|
29574
|
+
connectRemote
|
|
27981
29575
|
});
|
|
27982
29576
|
const authResolver = new AuthContextResolver({
|
|
27983
|
-
ownerToken: resolvedAuthToken
|
|
27984
|
-
personaRegistry
|
|
29577
|
+
ownerToken: resolvedAuthToken
|
|
27985
29578
|
});
|
|
27986
29579
|
const httpRouter = createHttpRouter({
|
|
27987
29580
|
authResolver,
|
|
27988
29581
|
daemonVersion: version,
|
|
27989
29582
|
logger,
|
|
27990
|
-
personaStore,
|
|
27991
|
-
groupFileStore,
|
|
27992
29583
|
sessionStore: store,
|
|
27993
|
-
// /files HMAC verify
|
|
27994
|
-
|
|
27995
|
-
getSignSecret: () => authFile?.signSecret ?? null
|
|
29584
|
+
// /files HMAC verify 用同一份 owner token 做 secret(与 attachment.signUrl 同源)
|
|
29585
|
+
getSignSecret: () => resolvedAuthToken ?? null
|
|
27996
29586
|
});
|
|
27997
29587
|
wsServer = new LocalWsServer({
|
|
27998
29588
|
host: config.host,
|
|
27999
29589
|
port: config.port,
|
|
28000
29590
|
logger,
|
|
29591
|
+
// broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
|
|
29592
|
+
ownerPrincipalId,
|
|
28001
29593
|
readyFrameBuilder: (ctx) => buildReadyFrame(
|
|
28002
29594
|
{
|
|
28003
29595
|
manager,
|
|
@@ -28013,6 +29605,29 @@ async function startDaemon(config) {
|
|
|
28013
29605
|
),
|
|
28014
29606
|
protocolVersion: PROTOCOL_VERSION,
|
|
28015
29607
|
authGate: authGate ?? void 0,
|
|
29608
|
+
// noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
|
|
29609
|
+
// 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
|
|
29610
|
+
// capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
|
|
29611
|
+
// 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
|
|
29612
|
+
tryVerifyCapabilityToken: (token, selfPrincipalId, selfDisplayName) => {
|
|
29613
|
+
const v2 = capabilityRegistry.verifyToken(token);
|
|
29614
|
+
if (!v2.ok) return null;
|
|
29615
|
+
if (selfPrincipalId && selfDisplayName) {
|
|
29616
|
+
try {
|
|
29617
|
+
capabilityManager.recordPeerHello(v2.capability.id, {
|
|
29618
|
+
ownerPrincipalId: selfPrincipalId,
|
|
29619
|
+
displayName: selfDisplayName,
|
|
29620
|
+
now: Date.now()
|
|
29621
|
+
});
|
|
29622
|
+
} catch {
|
|
29623
|
+
}
|
|
29624
|
+
}
|
|
29625
|
+
return {
|
|
29626
|
+
principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
|
|
29627
|
+
grants: v2.capability.grants,
|
|
29628
|
+
capabilityId: v2.capability.id
|
|
29629
|
+
};
|
|
29630
|
+
},
|
|
28016
29631
|
// file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
|
|
28017
29632
|
httpRequestHandler: httpRouter,
|
|
28018
29633
|
// 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
|
|
@@ -28065,7 +29680,18 @@ async function startDaemon(config) {
|
|
|
28065
29680
|
const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
|
|
28066
29681
|
const handler = handlers[type];
|
|
28067
29682
|
if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
|
|
28068
|
-
const
|
|
29683
|
+
const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
|
|
29684
|
+
const verdict = computeGrantForFrame(type, frame);
|
|
29685
|
+
if (verdict.kind === "check") {
|
|
29686
|
+
const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
|
|
29687
|
+
if (!ok) {
|
|
29688
|
+
throw new ClawdError(
|
|
29689
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
29690
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
|
|
29691
|
+
);
|
|
29692
|
+
}
|
|
29693
|
+
}
|
|
29694
|
+
const result = await handler(frame, client, ctx);
|
|
28069
29695
|
if (requestId && result.response) {
|
|
28070
29696
|
client.send({ ...result.response, requestId });
|
|
28071
29697
|
}
|
|
@@ -28123,15 +29749,15 @@ async function startDaemon(config) {
|
|
|
28123
29749
|
});
|
|
28124
29750
|
try {
|
|
28125
29751
|
const r = await tunnelMgr.start({ localPort: config.port });
|
|
28126
|
-
stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url
|
|
29752
|
+
stateSnapshot = { ...stateSnapshot, tunnelUrl: r.url };
|
|
28127
29753
|
stateMgr.write(stateSnapshot);
|
|
28128
29754
|
currentTunnelUrl = r.url;
|
|
28129
29755
|
const connectUrl = resolvedAuthToken ? `${r.url}#token=${resolvedAuthToken}` : r.url;
|
|
28130
29756
|
const lines = [
|
|
28131
29757
|
`Tunnel: ${r.url}`,
|
|
28132
29758
|
...resolvedAuthToken ? [`Connect: ${connectUrl}`] : [],
|
|
28133
|
-
`Frpc config: ${
|
|
28134
|
-
`Frpc log: ${
|
|
29759
|
+
`Frpc config: ${import_node_path26.default.join(config.dataDir, "frpc.toml")}`,
|
|
29760
|
+
`Frpc log: ${import_node_path26.default.join(config.dataDir, "frpc.log")}`
|
|
28135
29761
|
];
|
|
28136
29762
|
const width = Math.max(...lines.map((l) => l.length));
|
|
28137
29763
|
const bar = "\u2550".repeat(width + 4);
|
|
@@ -28144,30 +29770,19 @@ ${bar}
|
|
|
28144
29770
|
|
|
28145
29771
|
`);
|
|
28146
29772
|
try {
|
|
28147
|
-
const connectPath =
|
|
29773
|
+
const connectPath = import_node_path26.default.join(config.dataDir, "connect.txt");
|
|
28148
29774
|
import_node_fs24.default.writeFileSync(connectPath, lines.join("\n") + "\n", { mode: 384 });
|
|
28149
29775
|
} catch {
|
|
28150
29776
|
}
|
|
28151
29777
|
} catch (err) {
|
|
28152
|
-
const tunnelError = err?.message ?? String(err);
|
|
28153
29778
|
try {
|
|
28154
29779
|
await tunnelMgr.stop();
|
|
28155
29780
|
} catch {
|
|
28156
29781
|
}
|
|
28157
29782
|
tunnelMgr = null;
|
|
28158
|
-
|
|
28159
|
-
|
|
28160
|
-
|
|
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 });
|
|
29783
|
+
stateMgr.delete();
|
|
29784
|
+
await wss.stop();
|
|
29785
|
+
throw err;
|
|
28171
29786
|
}
|
|
28172
29787
|
}
|
|
28173
29788
|
const shutdown = async () => {
|
|
@@ -28188,6 +29803,13 @@ ${bar}
|
|
|
28188
29803
|
manager
|
|
28189
29804
|
};
|
|
28190
29805
|
}
|
|
29806
|
+
function migrateDropPersonsDir(dataDir) {
|
|
29807
|
+
const dir = import_node_path26.default.join(dataDir, "persons");
|
|
29808
|
+
try {
|
|
29809
|
+
import_node_fs24.default.rmSync(dir, { recursive: true, force: true });
|
|
29810
|
+
} catch {
|
|
29811
|
+
}
|
|
29812
|
+
}
|
|
28191
29813
|
|
|
28192
29814
|
// src/cli.ts
|
|
28193
29815
|
async function main() {
|