@clawos-dev/clawd 0.2.74-beta.143.123e6e3 → 0.2.75
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 +2349 -601
- 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
|
];
|
|
@@ -609,8 +641,8 @@ var init_parseUtil = __esm({
|
|
|
609
641
|
init_errors2();
|
|
610
642
|
init_en();
|
|
611
643
|
makeIssue = (params) => {
|
|
612
|
-
const { data, path:
|
|
613
|
-
const fullPath = [...
|
|
644
|
+
const { data, path: path39, errorMaps, issueData } = params;
|
|
645
|
+
const fullPath = [...path39, ...issueData.path || []];
|
|
614
646
|
const fullIssue = {
|
|
615
647
|
...issueData,
|
|
616
648
|
path: fullPath
|
|
@@ -921,11 +953,11 @@ var init_types = __esm({
|
|
|
921
953
|
init_parseUtil();
|
|
922
954
|
init_util();
|
|
923
955
|
ParseInputLazyPath = class {
|
|
924
|
-
constructor(parent, value,
|
|
956
|
+
constructor(parent, value, path39, key) {
|
|
925
957
|
this._cachedPath = [];
|
|
926
958
|
this.parent = parent;
|
|
927
959
|
this.data = value;
|
|
928
|
-
this._path =
|
|
960
|
+
this._path = path39;
|
|
929
961
|
this._key = key;
|
|
930
962
|
}
|
|
931
963
|
get path() {
|
|
@@ -4309,12 +4341,12 @@ var init_zod = __esm({
|
|
|
4309
4341
|
});
|
|
4310
4342
|
|
|
4311
4343
|
// ../protocol/src/attachment-schemas.ts
|
|
4312
|
-
var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema
|
|
4344
|
+
var TOKEN_ROLES, GROUP_FILE_SOURCES, GroupFileEntrySchema, AttachmentSignUrlArgs, AttachmentSignUrlResponseSchema, AttachmentGroupAddArgs, AttachmentGroupAddResponseSchema, AttachmentGroupRemoveArgs, AttachmentGroupRemoveResponseSchema, AttachmentGroupListArgs, AttachmentGroupListResponseSchema;
|
|
4313
4345
|
var init_attachment_schemas = __esm({
|
|
4314
4346
|
"../protocol/src/attachment-schemas.ts"() {
|
|
4315
4347
|
"use strict";
|
|
4316
4348
|
init_zod();
|
|
4317
|
-
TOKEN_ROLES = ["owner"
|
|
4349
|
+
TOKEN_ROLES = ["owner"];
|
|
4318
4350
|
GROUP_FILE_SOURCES = ["agent", "owner"];
|
|
4319
4351
|
GroupFileEntrySchema = external_exports.object({
|
|
4320
4352
|
/** daemon 派发的稳定 id(用于 RPC remove / UI key) */
|
|
@@ -4370,31 +4402,15 @@ var init_attachment_schemas = __esm({
|
|
|
4370
4402
|
AttachmentGroupListResponseSchema = external_exports.object({
|
|
4371
4403
|
entries: external_exports.array(GroupFileEntrySchema)
|
|
4372
4404
|
});
|
|
4373
|
-
AttachmentGroupListPersonaArgs = external_exports.object({
|
|
4374
|
-
personaId: external_exports.string().min(1)
|
|
4375
|
-
});
|
|
4376
|
-
AttachmentGroupListPersonaResponseSchema = external_exports.object({
|
|
4377
|
-
perSession: external_exports.array(
|
|
4378
|
-
external_exports.object({
|
|
4379
|
-
sessionId: external_exports.string().min(1),
|
|
4380
|
-
entries: external_exports.array(GroupFileEntrySchema)
|
|
4381
|
-
})
|
|
4382
|
-
)
|
|
4383
|
-
});
|
|
4384
4405
|
}
|
|
4385
4406
|
});
|
|
4386
4407
|
|
|
4387
4408
|
// ../protocol/src/persona-schemas.ts
|
|
4388
|
-
var
|
|
4409
|
+
var PersonaFileSchema, PersonaSkillSummarySchema, PersonaPluginSummarySchema, PersonaSandboxSettingsSchema, PersonaInfoResponseSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema;
|
|
4389
4410
|
var init_persona_schemas = __esm({
|
|
4390
4411
|
"../protocol/src/persona-schemas.ts"() {
|
|
4391
4412
|
"use strict";
|
|
4392
4413
|
init_zod();
|
|
4393
|
-
PersonaTokenEntrySchema = external_exports.object({
|
|
4394
|
-
label: external_exports.string(),
|
|
4395
|
-
issuedAt: external_exports.number(),
|
|
4396
|
-
revoked: external_exports.boolean()
|
|
4397
|
-
});
|
|
4398
4414
|
PersonaFileSchema = external_exports.object({
|
|
4399
4415
|
personaId: external_exports.string(),
|
|
4400
4416
|
label: external_exports.string(),
|
|
@@ -4403,13 +4419,6 @@ var init_persona_schemas = __esm({
|
|
|
4403
4419
|
// 8-key set defined UI-side in `lib/session-icons.ts`; protocol stays plain string
|
|
4404
4420
|
// for forward compatibility, consumer fallbacks to default when key not found.
|
|
4405
4421
|
iconKey: external_exports.string().optional(),
|
|
4406
|
-
/**
|
|
4407
|
-
* Persona token 列表:file-sharing HTTP Bearer 鉴权用。
|
|
4408
|
-
* - 写:`PersonaManager.issueToken` / `revokeToken`
|
|
4409
|
-
* - 读:`PersonaRegistry.findByToken` 反向查表(需 persona.public === true)
|
|
4410
|
-
* - optional:兼容历史 persona.json 不含此字段的情形(旧 daemon 写文件时省略)
|
|
4411
|
-
*/
|
|
4412
|
-
tokenMap: external_exports.record(external_exports.string(), PersonaTokenEntrySchema).optional(),
|
|
4413
4422
|
createdAt: external_exports.number(),
|
|
4414
4423
|
updatedAt: external_exports.number()
|
|
4415
4424
|
}).strict();
|
|
@@ -4468,14 +4477,6 @@ var init_persona_schemas = __esm({
|
|
|
4468
4477
|
iconKey: external_exports.string().nullable().optional()
|
|
4469
4478
|
}).strict()
|
|
4470
4479
|
}).strict();
|
|
4471
|
-
PersonaIssueTokenArgsSchema = external_exports.object({
|
|
4472
|
-
personaId: external_exports.string().min(1),
|
|
4473
|
-
label: external_exports.string().min(1)
|
|
4474
|
-
});
|
|
4475
|
-
PersonaRevokeTokenArgsSchema = external_exports.object({
|
|
4476
|
-
personaId: external_exports.string().min(1),
|
|
4477
|
-
token: external_exports.string().min(1)
|
|
4478
|
-
});
|
|
4479
4480
|
}
|
|
4480
4481
|
});
|
|
4481
4482
|
|
|
@@ -4582,6 +4583,15 @@ var init_schemas = __esm({
|
|
|
4582
4583
|
// 才能让 alice 重连同一 sub-session(持久化在 alice localStorage 之外,由 daemon push 同步)。
|
|
4583
4584
|
// optional 是因为 owner-mode session 不带;只要写入就是 listener-mode 必填三元组。
|
|
4584
4585
|
chatId: external_exports.string().min(1).optional(),
|
|
4586
|
+
/**
|
|
4587
|
+
* 创建该 session 的 principal id(owner uuid 或 guest cap.id),从 session:create handler
|
|
4588
|
+
* 的 ctx.principal.id 派生。Person identity Q2「按对端人聚合 sessions」需要它:
|
|
4589
|
+
* UI 端按 `creatorPrincipalId === ownerPrincipalId` 区分「我自己」vs「别人接入」,
|
|
4590
|
+
* 后者再通过 PersonAlias 表反查 personId 二级分组。
|
|
4591
|
+
*
|
|
4592
|
+
* optional:兼容 2026-05-21 之前的老 session 数据,新建一定有值。
|
|
4593
|
+
*/
|
|
4594
|
+
creatorPrincipalId: external_exports.string().min(1).optional(),
|
|
4585
4595
|
createdAt: external_exports.string().min(1),
|
|
4586
4596
|
updatedAt: external_exports.string().min(1)
|
|
4587
4597
|
});
|
|
@@ -4991,7 +5001,20 @@ var init_schemas = __esm({
|
|
|
4991
5001
|
AuthRequestFrameSchema = external_exports.object({
|
|
4992
5002
|
type: external_exports.literal("auth"),
|
|
4993
5003
|
token: external_exports.string().min(1),
|
|
4994
|
-
scheme: external_exports.literal("bearer").optional()
|
|
5004
|
+
scheme: external_exports.literal("bearer").optional(),
|
|
5005
|
+
/**
|
|
5006
|
+
* guest 端 ws 接入时自报对端稳定 ownerPrincipalId。AuthGate.authenticate 收到
|
|
5007
|
+
* 后调 capabilityManager.recordPeerHello(capId, ownerPrincipalId, displayName)
|
|
5008
|
+
* 把 cap.peerOwnerId / firstUsedByPeerAt 字段回写,让 owner 端 UI 在 People
|
|
5009
|
+
* tab 顶层 PeerOwner 视图 + Personas tab 二级分组拿到对端身份。
|
|
5010
|
+
*/
|
|
5011
|
+
selfPrincipalId: external_exports.string().min(1).optional(),
|
|
5012
|
+
/**
|
|
5013
|
+
* 同 selfPrincipalId:自报 owner displayName(人类可读),让对端 daemon 把
|
|
5014
|
+
* 显示名写入 cap.peerOwnerDisplayName(cap.displayName 本来就是 owner 颁时填
|
|
5015
|
+
* 的"颁给谁/用途",hello 收到的 displayName 是辅助)。
|
|
5016
|
+
*/
|
|
5017
|
+
selfDisplayName: external_exports.string().optional()
|
|
4995
5018
|
});
|
|
4996
5019
|
AuthOkFrameSchema = external_exports.object({
|
|
4997
5020
|
type: external_exports.literal("auth:ok"),
|
|
@@ -5029,10 +5052,9 @@ var init_schemas = __esm({
|
|
|
5029
5052
|
// PR 2 daemon 实现 single HTTP server + auth-context 后,daemon 必返这五个字段,
|
|
5030
5053
|
// 届时可考虑改成 required。spec §11 第 3 条:"tokenRole 是协议字段,daemon 查 token
|
|
5031
5054
|
// 表后显式下发,UI 不自行推导"——所以 UI 一律读这里,禁止本地推导。
|
|
5032
|
-
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP
|
|
5055
|
+
/** 'owner' = 拿到 owner-token 的连接(不限来源 IP)。来源 TOKEN_ROLES(spec §11 #1 中央真理源)。
|
|
5056
|
+
* 历史 'personal' role(persona file-sharing token)在 2026-05-21 删除;tokenPersonaId 一同移除。 */
|
|
5033
5057
|
tokenRole: external_exports.enum(TOKEN_ROLES).optional(),
|
|
5034
|
-
/** tokenRole='personal' 时携带(绑定到具体 persona);owner 不携带 */
|
|
5035
|
-
tokenPersonaId: external_exports.string().min(1).optional(),
|
|
5036
5058
|
/** socket.remoteAddress 是否落在 127.0.0.1 / ::1;本期无 RPC 消费,保留协议槽位给未来 "Reveal in Finder" 等本机动作 */
|
|
5037
5059
|
isLoopback: external_exports.boolean().optional(),
|
|
5038
5060
|
/** UI 拼 file-sharing HTTP 路由的前缀(http(s)://host:port),不含尾斜杠;frpc 反代时返反代地址 */
|
|
@@ -5059,6 +5081,194 @@ var init_persona_mode = __esm({
|
|
|
5059
5081
|
}
|
|
5060
5082
|
});
|
|
5061
5083
|
|
|
5084
|
+
// ../protocol/src/principal.ts
|
|
5085
|
+
function makeOwnerPrincipal(id, displayName) {
|
|
5086
|
+
return PrincipalSchema.parse({ id, kind: "owner", displayName });
|
|
5087
|
+
}
|
|
5088
|
+
var PrincipalKindSchema, PrincipalSchema;
|
|
5089
|
+
var init_principal = __esm({
|
|
5090
|
+
"../protocol/src/principal.ts"() {
|
|
5091
|
+
"use strict";
|
|
5092
|
+
init_zod();
|
|
5093
|
+
PrincipalKindSchema = external_exports.enum(["owner", "guest"]);
|
|
5094
|
+
PrincipalSchema = external_exports.object({
|
|
5095
|
+
id: external_exports.string().min(1),
|
|
5096
|
+
kind: PrincipalKindSchema,
|
|
5097
|
+
displayName: external_exports.string()
|
|
5098
|
+
}).strict();
|
|
5099
|
+
}
|
|
5100
|
+
});
|
|
5101
|
+
|
|
5102
|
+
// ../protocol/src/capability.ts
|
|
5103
|
+
function stripSecretHash(cap) {
|
|
5104
|
+
const { secretHash: _hash, ...wire } = cap;
|
|
5105
|
+
return wire;
|
|
5106
|
+
}
|
|
5107
|
+
var ResourceSchema, ActionSchema, GrantSchema, CapabilitySchema, CapabilityWireSchema, CapabilityErrorCodeSchema, WhoamiResponseSchema, CapabilityIssueArgsSchema, CapabilityBindPeerArgsSchema;
|
|
5108
|
+
var init_capability = __esm({
|
|
5109
|
+
"../protocol/src/capability.ts"() {
|
|
5110
|
+
"use strict";
|
|
5111
|
+
init_zod();
|
|
5112
|
+
ResourceSchema = external_exports.discriminatedUnion("type", [
|
|
5113
|
+
external_exports.object({ type: external_exports.literal("persona"), id: external_exports.string().min(1) }).strict(),
|
|
5114
|
+
external_exports.object({ type: external_exports.literal("chat"), id: external_exports.string().min(1) }).strict(),
|
|
5115
|
+
external_exports.object({ type: external_exports.literal("*") }).strict()
|
|
5116
|
+
]);
|
|
5117
|
+
ActionSchema = external_exports.enum(["read", "send", "admin"]);
|
|
5118
|
+
GrantSchema = external_exports.object({
|
|
5119
|
+
resource: ResourceSchema,
|
|
5120
|
+
actions: external_exports.array(ActionSchema).min(1)
|
|
5121
|
+
}).strict();
|
|
5122
|
+
CapabilitySchema = external_exports.object({
|
|
5123
|
+
id: external_exports.string().min(1),
|
|
5124
|
+
// sha256(token) hex 64
|
|
5125
|
+
secretHash: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
5126
|
+
displayName: external_exports.string(),
|
|
5127
|
+
// 空数组合法 = 纯访客(只能 DM)
|
|
5128
|
+
grants: external_exports.array(GrantSchema),
|
|
5129
|
+
issuedAt: external_exports.number().int().nonnegative(),
|
|
5130
|
+
expiresAt: external_exports.number().int().positive().optional(),
|
|
5131
|
+
maxUses: external_exports.number().int().positive().optional(),
|
|
5132
|
+
usedCount: external_exports.number().int().nonnegative(),
|
|
5133
|
+
/**
|
|
5134
|
+
* guest 在 auth 帧捎带 selfPrincipalId 时由 AuthGate.authenticate 回写;
|
|
5135
|
+
* 未被消费的 cap 此字段缺失。
|
|
5136
|
+
*/
|
|
5137
|
+
peerOwnerId: external_exports.string().min(1).optional(),
|
|
5138
|
+
/** 首次 client hello 时间戳(与 peerOwnerId 同步写入) */
|
|
5139
|
+
firstUsedByPeerAt: external_exports.number().int().positive().optional(),
|
|
5140
|
+
/**
|
|
5141
|
+
* 对方 daemon owner 的 displayName,guest auth 帧 selfDisplayName 触发
|
|
5142
|
+
* recordPeerHello 时同步回写。视角 B (2026-05-23) UI 派生联系人列表用此字段
|
|
5143
|
+
* 显示对方名字(避免 fallback 到我颁 cap 时填的 displayName)。
|
|
5144
|
+
*/
|
|
5145
|
+
peerDisplayName: external_exports.string().optional()
|
|
5146
|
+
}).strict();
|
|
5147
|
+
CapabilityWireSchema = CapabilitySchema.omit({ secretHash: true });
|
|
5148
|
+
CapabilityErrorCodeSchema = external_exports.enum([
|
|
5149
|
+
"TOKEN_INVALID",
|
|
5150
|
+
"TOKEN_REVOKED",
|
|
5151
|
+
"TOKEN_EXPIRED",
|
|
5152
|
+
"TOKEN_EXHAUSTED"
|
|
5153
|
+
]);
|
|
5154
|
+
WhoamiResponseSchema = external_exports.object({
|
|
5155
|
+
type: external_exports.literal("whoami:ok"),
|
|
5156
|
+
owner: external_exports.object({
|
|
5157
|
+
id: external_exports.string(),
|
|
5158
|
+
kind: external_exports.enum(["owner", "guest"]),
|
|
5159
|
+
displayName: external_exports.string()
|
|
5160
|
+
}).strict(),
|
|
5161
|
+
capability: CapabilityWireSchema,
|
|
5162
|
+
grantedPersonas: external_exports.array(
|
|
5163
|
+
external_exports.object({
|
|
5164
|
+
id: external_exports.string().min(1),
|
|
5165
|
+
displayName: external_exports.string()
|
|
5166
|
+
}).strict()
|
|
5167
|
+
)
|
|
5168
|
+
}).strict();
|
|
5169
|
+
CapabilityIssueArgsSchema = external_exports.object({
|
|
5170
|
+
displayName: external_exports.string().min(1),
|
|
5171
|
+
grants: external_exports.array(GrantSchema),
|
|
5172
|
+
expiresAt: external_exports.number().int().positive().optional(),
|
|
5173
|
+
maxUses: external_exports.number().int().positive().optional()
|
|
5174
|
+
}).strict();
|
|
5175
|
+
CapabilityBindPeerArgsSchema = external_exports.object({
|
|
5176
|
+
capabilityId: external_exports.string().min(1),
|
|
5177
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5178
|
+
peerDisplayName: external_exports.string()
|
|
5179
|
+
}).strict();
|
|
5180
|
+
}
|
|
5181
|
+
});
|
|
5182
|
+
|
|
5183
|
+
// ../protocol/src/inbox.ts
|
|
5184
|
+
var InboxMessageSchema, InboxPostMessageArgsSchema, InboxListArgsSchema, InboxMarkReadArgsSchema;
|
|
5185
|
+
var init_inbox = __esm({
|
|
5186
|
+
"../protocol/src/inbox.ts"() {
|
|
5187
|
+
"use strict";
|
|
5188
|
+
init_zod();
|
|
5189
|
+
InboxMessageSchema = external_exports.object({
|
|
5190
|
+
id: external_exports.string().min(1),
|
|
5191
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5192
|
+
senderPrincipalId: external_exports.string().min(1),
|
|
5193
|
+
text: external_exports.string().min(1),
|
|
5194
|
+
createdAt: external_exports.number().int().nonnegative(),
|
|
5195
|
+
readBy: external_exports.record(external_exports.string().min(1), external_exports.number().int().positive()).default({})
|
|
5196
|
+
}).strict();
|
|
5197
|
+
InboxPostMessageArgsSchema = external_exports.object({
|
|
5198
|
+
peerOwnerId: external_exports.string().min(1).optional(),
|
|
5199
|
+
id: external_exports.string().min(1),
|
|
5200
|
+
text: external_exports.string().min(1),
|
|
5201
|
+
createdAt: external_exports.number().int().nonnegative()
|
|
5202
|
+
}).strict();
|
|
5203
|
+
InboxListArgsSchema = external_exports.object({
|
|
5204
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5205
|
+
sinceCreatedAt: external_exports.number().int().nonnegative().optional()
|
|
5206
|
+
}).strict();
|
|
5207
|
+
InboxMarkReadArgsSchema = external_exports.object({
|
|
5208
|
+
peerOwnerId: external_exports.string().min(1),
|
|
5209
|
+
upToCreatedAt: external_exports.number().int().nonnegative()
|
|
5210
|
+
}).strict();
|
|
5211
|
+
}
|
|
5212
|
+
});
|
|
5213
|
+
|
|
5214
|
+
// ../protocol/src/received-capability.ts
|
|
5215
|
+
var ReceivedCapabilitySchema, ReceivedCapabilityWireSchema, ReceivedCapabilityAddArgsSchema, ReceivedCapabilityAddOkSchema, ReceivedCapabilityRemoveArgsSchema, ReceivedCapabilityRemoveOkSchema, ReceivedCapabilityListOkSchema, ReceivedCapabilityAutoAttachArgsSchema, ReceivedCapabilityAutoAttachOkSchema, ReceivedCapabilityAddedFrameSchema, ReceivedCapabilityRemovedFrameSchema;
|
|
5216
|
+
var init_received_capability = __esm({
|
|
5217
|
+
"../protocol/src/received-capability.ts"() {
|
|
5218
|
+
"use strict";
|
|
5219
|
+
init_zod();
|
|
5220
|
+
init_capability();
|
|
5221
|
+
ReceivedCapabilitySchema = external_exports.object({
|
|
5222
|
+
ownerPrincipalId: external_exports.string().min(1),
|
|
5223
|
+
ownerDisplayName: external_exports.string(),
|
|
5224
|
+
remoteUrl: external_exports.string().min(1),
|
|
5225
|
+
capabilityId: external_exports.string().min(1),
|
|
5226
|
+
capabilityToken: external_exports.string().min(1),
|
|
5227
|
+
grants: external_exports.array(GrantSchema),
|
|
5228
|
+
receivedAt: external_exports.number().int().nonnegative()
|
|
5229
|
+
}).strict();
|
|
5230
|
+
ReceivedCapabilityWireSchema = ReceivedCapabilitySchema;
|
|
5231
|
+
ReceivedCapabilityAddArgsSchema = external_exports.object({
|
|
5232
|
+
remoteUrl: external_exports.string().min(1),
|
|
5233
|
+
token: external_exports.string().min(1)
|
|
5234
|
+
}).strict();
|
|
5235
|
+
ReceivedCapabilityAddOkSchema = external_exports.object({
|
|
5236
|
+
type: external_exports.literal("received-capability:add:ok"),
|
|
5237
|
+
receivedCap: ReceivedCapabilityWireSchema
|
|
5238
|
+
}).strict();
|
|
5239
|
+
ReceivedCapabilityRemoveArgsSchema = external_exports.object({
|
|
5240
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5241
|
+
}).strict();
|
|
5242
|
+
ReceivedCapabilityRemoveOkSchema = external_exports.object({
|
|
5243
|
+
type: external_exports.literal("received-capability:remove:ok"),
|
|
5244
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5245
|
+
}).strict();
|
|
5246
|
+
ReceivedCapabilityListOkSchema = external_exports.object({
|
|
5247
|
+
type: external_exports.literal("received-capability:list:ok"),
|
|
5248
|
+
receivedCaps: external_exports.array(ReceivedCapabilityWireSchema)
|
|
5249
|
+
}).strict();
|
|
5250
|
+
ReceivedCapabilityAutoAttachArgsSchema = external_exports.object({
|
|
5251
|
+
ownerPrincipalId: external_exports.string().min(1),
|
|
5252
|
+
ownerDisplayName: external_exports.string(),
|
|
5253
|
+
remoteUrl: external_exports.string().min(1),
|
|
5254
|
+
capabilityId: external_exports.string().min(1),
|
|
5255
|
+
token: external_exports.string().min(1),
|
|
5256
|
+
grants: external_exports.array(GrantSchema)
|
|
5257
|
+
}).strict();
|
|
5258
|
+
ReceivedCapabilityAutoAttachOkSchema = external_exports.object({
|
|
5259
|
+
type: external_exports.literal("received-capability:autoAttach:ok")
|
|
5260
|
+
}).strict();
|
|
5261
|
+
ReceivedCapabilityAddedFrameSchema = external_exports.object({
|
|
5262
|
+
type: external_exports.literal("received-capability:added"),
|
|
5263
|
+
receivedCap: ReceivedCapabilityWireSchema
|
|
5264
|
+
}).strict();
|
|
5265
|
+
ReceivedCapabilityRemovedFrameSchema = external_exports.object({
|
|
5266
|
+
type: external_exports.literal("received-capability:removed"),
|
|
5267
|
+
ownerPrincipalId: external_exports.string().min(1)
|
|
5268
|
+
}).strict();
|
|
5269
|
+
}
|
|
5270
|
+
});
|
|
5271
|
+
|
|
5062
5272
|
// ../protocol/src/runtime.ts
|
|
5063
5273
|
var init_runtime = __esm({
|
|
5064
5274
|
"../protocol/src/runtime.ts"() {
|
|
@@ -5071,6 +5281,10 @@ var init_runtime = __esm({
|
|
|
5071
5281
|
init_persona_schemas();
|
|
5072
5282
|
init_persona_mode();
|
|
5073
5283
|
init_attachment_schemas();
|
|
5284
|
+
init_principal();
|
|
5285
|
+
init_capability();
|
|
5286
|
+
init_inbox();
|
|
5287
|
+
init_received_capability();
|
|
5074
5288
|
}
|
|
5075
5289
|
});
|
|
5076
5290
|
|
|
@@ -5345,8 +5559,8 @@ var require_req = __commonJS({
|
|
|
5345
5559
|
if (req.originalUrl) {
|
|
5346
5560
|
_req.url = req.originalUrl;
|
|
5347
5561
|
} else {
|
|
5348
|
-
const
|
|
5349
|
-
_req.url = typeof
|
|
5562
|
+
const path39 = req.path;
|
|
5563
|
+
_req.url = typeof path39 === "string" ? path39 : req.url ? req.url.path || req.url : void 0;
|
|
5350
5564
|
}
|
|
5351
5565
|
if (req.query) {
|
|
5352
5566
|
_req.query = req.query;
|
|
@@ -5511,14 +5725,14 @@ var require_redact = __commonJS({
|
|
|
5511
5725
|
}
|
|
5512
5726
|
return obj;
|
|
5513
5727
|
}
|
|
5514
|
-
function parsePath(
|
|
5728
|
+
function parsePath(path39) {
|
|
5515
5729
|
const parts = [];
|
|
5516
5730
|
let current = "";
|
|
5517
5731
|
let inBrackets = false;
|
|
5518
5732
|
let inQuotes = false;
|
|
5519
5733
|
let quoteChar = "";
|
|
5520
|
-
for (let i = 0; i <
|
|
5521
|
-
const char =
|
|
5734
|
+
for (let i = 0; i < path39.length; i++) {
|
|
5735
|
+
const char = path39[i];
|
|
5522
5736
|
if (!inBrackets && char === ".") {
|
|
5523
5737
|
if (current) {
|
|
5524
5738
|
parts.push(current);
|
|
@@ -5649,10 +5863,10 @@ var require_redact = __commonJS({
|
|
|
5649
5863
|
return current;
|
|
5650
5864
|
}
|
|
5651
5865
|
function redactPaths(obj, paths, censor, remove = false) {
|
|
5652
|
-
for (const
|
|
5653
|
-
const parts = parsePath(
|
|
5866
|
+
for (const path39 of paths) {
|
|
5867
|
+
const parts = parsePath(path39);
|
|
5654
5868
|
if (parts.includes("*")) {
|
|
5655
|
-
redactWildcardPath(obj, parts, censor,
|
|
5869
|
+
redactWildcardPath(obj, parts, censor, path39, remove);
|
|
5656
5870
|
} else {
|
|
5657
5871
|
if (remove) {
|
|
5658
5872
|
removeKey(obj, parts);
|
|
@@ -5737,8 +5951,8 @@ var require_redact = __commonJS({
|
|
|
5737
5951
|
}
|
|
5738
5952
|
} else {
|
|
5739
5953
|
if (afterWildcard.includes("*")) {
|
|
5740
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
5741
|
-
const fullPath = [...pathArray.slice(0, pathLength), ...
|
|
5954
|
+
const wrappedCensor = typeof censor === "function" ? (value, path39) => {
|
|
5955
|
+
const fullPath = [...pathArray.slice(0, pathLength), ...path39];
|
|
5742
5956
|
return censor(value, fullPath);
|
|
5743
5957
|
} : censor;
|
|
5744
5958
|
redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove);
|
|
@@ -5773,8 +5987,8 @@ var require_redact = __commonJS({
|
|
|
5773
5987
|
return null;
|
|
5774
5988
|
}
|
|
5775
5989
|
const pathStructure = /* @__PURE__ */ new Map();
|
|
5776
|
-
for (const
|
|
5777
|
-
const parts = parsePath(
|
|
5990
|
+
for (const path39 of pathsToClone) {
|
|
5991
|
+
const parts = parsePath(path39);
|
|
5778
5992
|
let current = pathStructure;
|
|
5779
5993
|
for (let i = 0; i < parts.length; i++) {
|
|
5780
5994
|
const part = parts[i];
|
|
@@ -5826,24 +6040,24 @@ var require_redact = __commonJS({
|
|
|
5826
6040
|
}
|
|
5827
6041
|
return cloneSelectively(obj, pathStructure);
|
|
5828
6042
|
}
|
|
5829
|
-
function validatePath(
|
|
5830
|
-
if (typeof
|
|
6043
|
+
function validatePath(path39) {
|
|
6044
|
+
if (typeof path39 !== "string") {
|
|
5831
6045
|
throw new Error("Paths must be (non-empty) strings");
|
|
5832
6046
|
}
|
|
5833
|
-
if (
|
|
6047
|
+
if (path39 === "") {
|
|
5834
6048
|
throw new Error("Invalid redaction path ()");
|
|
5835
6049
|
}
|
|
5836
|
-
if (
|
|
5837
|
-
throw new Error(`Invalid redaction path (${
|
|
6050
|
+
if (path39.includes("..")) {
|
|
6051
|
+
throw new Error(`Invalid redaction path (${path39})`);
|
|
5838
6052
|
}
|
|
5839
|
-
if (
|
|
5840
|
-
throw new Error(`Invalid redaction path (${
|
|
6053
|
+
if (path39.includes(",")) {
|
|
6054
|
+
throw new Error(`Invalid redaction path (${path39})`);
|
|
5841
6055
|
}
|
|
5842
6056
|
let bracketCount = 0;
|
|
5843
6057
|
let inQuotes = false;
|
|
5844
6058
|
let quoteChar = "";
|
|
5845
|
-
for (let i = 0; i <
|
|
5846
|
-
const char =
|
|
6059
|
+
for (let i = 0; i < path39.length; i++) {
|
|
6060
|
+
const char = path39[i];
|
|
5847
6061
|
if ((char === '"' || char === "'") && bracketCount > 0) {
|
|
5848
6062
|
if (!inQuotes) {
|
|
5849
6063
|
inQuotes = true;
|
|
@@ -5857,20 +6071,20 @@ var require_redact = __commonJS({
|
|
|
5857
6071
|
} else if (char === "]" && !inQuotes) {
|
|
5858
6072
|
bracketCount--;
|
|
5859
6073
|
if (bracketCount < 0) {
|
|
5860
|
-
throw new Error(`Invalid redaction path (${
|
|
6074
|
+
throw new Error(`Invalid redaction path (${path39})`);
|
|
5861
6075
|
}
|
|
5862
6076
|
}
|
|
5863
6077
|
}
|
|
5864
6078
|
if (bracketCount !== 0) {
|
|
5865
|
-
throw new Error(`Invalid redaction path (${
|
|
6079
|
+
throw new Error(`Invalid redaction path (${path39})`);
|
|
5866
6080
|
}
|
|
5867
6081
|
}
|
|
5868
6082
|
function validatePaths(paths) {
|
|
5869
6083
|
if (!Array.isArray(paths)) {
|
|
5870
6084
|
throw new TypeError("paths must be an array");
|
|
5871
6085
|
}
|
|
5872
|
-
for (const
|
|
5873
|
-
validatePath(
|
|
6086
|
+
for (const path39 of paths) {
|
|
6087
|
+
validatePath(path39);
|
|
5874
6088
|
}
|
|
5875
6089
|
}
|
|
5876
6090
|
function slowRedact(options = {}) {
|
|
@@ -6038,8 +6252,8 @@ var require_redaction = __commonJS({
|
|
|
6038
6252
|
if (shape[k2] === null) {
|
|
6039
6253
|
o[k2] = (value) => topCensor(value, [k2]);
|
|
6040
6254
|
} else {
|
|
6041
|
-
const wrappedCensor = typeof censor === "function" ? (value,
|
|
6042
|
-
return censor(value, [k2, ...
|
|
6255
|
+
const wrappedCensor = typeof censor === "function" ? (value, path39) => {
|
|
6256
|
+
return censor(value, [k2, ...path39]);
|
|
6043
6257
|
} : censor;
|
|
6044
6258
|
o[k2] = Redact({
|
|
6045
6259
|
paths: shape[k2],
|
|
@@ -6257,10 +6471,10 @@ var require_atomic_sleep = __commonJS({
|
|
|
6257
6471
|
var require_sonic_boom = __commonJS({
|
|
6258
6472
|
"../node_modules/.pnpm/sonic-boom@4.2.1/node_modules/sonic-boom/index.js"(exports2, module2) {
|
|
6259
6473
|
"use strict";
|
|
6260
|
-
var
|
|
6474
|
+
var fs34 = require("fs");
|
|
6261
6475
|
var EventEmitter2 = require("events");
|
|
6262
6476
|
var inherits = require("util").inherits;
|
|
6263
|
-
var
|
|
6477
|
+
var path39 = require("path");
|
|
6264
6478
|
var sleep = require_atomic_sleep();
|
|
6265
6479
|
var assert = require("assert");
|
|
6266
6480
|
var BUSY_WRITE_TIMEOUT = 100;
|
|
@@ -6314,20 +6528,20 @@ var require_sonic_boom = __commonJS({
|
|
|
6314
6528
|
const mode = sonic.mode;
|
|
6315
6529
|
if (sonic.sync) {
|
|
6316
6530
|
try {
|
|
6317
|
-
if (sonic.mkdir)
|
|
6318
|
-
const fd =
|
|
6531
|
+
if (sonic.mkdir) fs34.mkdirSync(path39.dirname(file), { recursive: true });
|
|
6532
|
+
const fd = fs34.openSync(file, flags, mode);
|
|
6319
6533
|
fileOpened(null, fd);
|
|
6320
6534
|
} catch (err) {
|
|
6321
6535
|
fileOpened(err);
|
|
6322
6536
|
throw err;
|
|
6323
6537
|
}
|
|
6324
6538
|
} else if (sonic.mkdir) {
|
|
6325
|
-
|
|
6539
|
+
fs34.mkdir(path39.dirname(file), { recursive: true }, (err) => {
|
|
6326
6540
|
if (err) return fileOpened(err);
|
|
6327
|
-
|
|
6541
|
+
fs34.open(file, flags, mode, fileOpened);
|
|
6328
6542
|
});
|
|
6329
6543
|
} else {
|
|
6330
|
-
|
|
6544
|
+
fs34.open(file, flags, mode, fileOpened);
|
|
6331
6545
|
}
|
|
6332
6546
|
}
|
|
6333
6547
|
function SonicBoom(opts) {
|
|
@@ -6368,8 +6582,8 @@ var require_sonic_boom = __commonJS({
|
|
|
6368
6582
|
this.flush = flushBuffer;
|
|
6369
6583
|
this.flushSync = flushBufferSync;
|
|
6370
6584
|
this._actualWrite = actualWriteBuffer;
|
|
6371
|
-
fsWriteSync = () =>
|
|
6372
|
-
fsWrite = () =>
|
|
6585
|
+
fsWriteSync = () => fs34.writeSync(this.fd, this._writingBuf);
|
|
6586
|
+
fsWrite = () => fs34.write(this.fd, this._writingBuf, this.release);
|
|
6373
6587
|
} else if (contentMode === void 0 || contentMode === kContentModeUtf8) {
|
|
6374
6588
|
this._writingBuf = "";
|
|
6375
6589
|
this.write = write;
|
|
@@ -6378,15 +6592,15 @@ var require_sonic_boom = __commonJS({
|
|
|
6378
6592
|
this._actualWrite = actualWrite;
|
|
6379
6593
|
fsWriteSync = () => {
|
|
6380
6594
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6381
|
-
return
|
|
6595
|
+
return fs34.writeSync(this.fd, this._writingBuf);
|
|
6382
6596
|
}
|
|
6383
|
-
return
|
|
6597
|
+
return fs34.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6384
6598
|
};
|
|
6385
6599
|
fsWrite = () => {
|
|
6386
6600
|
if (Buffer.isBuffer(this._writingBuf)) {
|
|
6387
|
-
return
|
|
6601
|
+
return fs34.write(this.fd, this._writingBuf, this.release);
|
|
6388
6602
|
}
|
|
6389
|
-
return
|
|
6603
|
+
return fs34.write(this.fd, this._writingBuf, "utf8", this.release);
|
|
6390
6604
|
};
|
|
6391
6605
|
} else {
|
|
6392
6606
|
throw new Error(`SonicBoom supports "${kContentModeUtf8}" and "${kContentModeBuffer}", but passed ${contentMode}`);
|
|
@@ -6443,7 +6657,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6443
6657
|
}
|
|
6444
6658
|
}
|
|
6445
6659
|
if (this._fsync) {
|
|
6446
|
-
|
|
6660
|
+
fs34.fsyncSync(this.fd);
|
|
6447
6661
|
}
|
|
6448
6662
|
const len = this._len;
|
|
6449
6663
|
if (this._reopening) {
|
|
@@ -6557,7 +6771,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6557
6771
|
const onDrain = () => {
|
|
6558
6772
|
if (!this._fsync) {
|
|
6559
6773
|
try {
|
|
6560
|
-
|
|
6774
|
+
fs34.fsync(this.fd, (err) => {
|
|
6561
6775
|
this._flushPending = false;
|
|
6562
6776
|
cb(err);
|
|
6563
6777
|
});
|
|
@@ -6659,7 +6873,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6659
6873
|
const fd = this.fd;
|
|
6660
6874
|
this.once("ready", () => {
|
|
6661
6875
|
if (fd !== this.fd) {
|
|
6662
|
-
|
|
6876
|
+
fs34.close(fd, (err) => {
|
|
6663
6877
|
if (err) {
|
|
6664
6878
|
return this.emit("error", err);
|
|
6665
6879
|
}
|
|
@@ -6708,7 +6922,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6708
6922
|
buf = this._bufs[0];
|
|
6709
6923
|
}
|
|
6710
6924
|
try {
|
|
6711
|
-
const n = Buffer.isBuffer(buf) ?
|
|
6925
|
+
const n = Buffer.isBuffer(buf) ? fs34.writeSync(this.fd, buf) : fs34.writeSync(this.fd, buf, "utf8");
|
|
6712
6926
|
const releasedBufObj = releaseWritingBuf(buf, this._len, n);
|
|
6713
6927
|
buf = releasedBufObj.writingBuf;
|
|
6714
6928
|
this._len = releasedBufObj.len;
|
|
@@ -6724,7 +6938,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6724
6938
|
}
|
|
6725
6939
|
}
|
|
6726
6940
|
try {
|
|
6727
|
-
|
|
6941
|
+
fs34.fsyncSync(this.fd);
|
|
6728
6942
|
} catch {
|
|
6729
6943
|
}
|
|
6730
6944
|
}
|
|
@@ -6745,7 +6959,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6745
6959
|
buf = mergeBuf(this._bufs[0], this._lens[0]);
|
|
6746
6960
|
}
|
|
6747
6961
|
try {
|
|
6748
|
-
const n =
|
|
6962
|
+
const n = fs34.writeSync(this.fd, buf);
|
|
6749
6963
|
buf = buf.subarray(n);
|
|
6750
6964
|
this._len = Math.max(this._len - n, 0);
|
|
6751
6965
|
if (buf.length <= 0) {
|
|
@@ -6773,13 +6987,13 @@ var require_sonic_boom = __commonJS({
|
|
|
6773
6987
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : this._bufs.shift() || "";
|
|
6774
6988
|
if (this.sync) {
|
|
6775
6989
|
try {
|
|
6776
|
-
const written = Buffer.isBuffer(this._writingBuf) ?
|
|
6990
|
+
const written = Buffer.isBuffer(this._writingBuf) ? fs34.writeSync(this.fd, this._writingBuf) : fs34.writeSync(this.fd, this._writingBuf, "utf8");
|
|
6777
6991
|
release(null, written);
|
|
6778
6992
|
} catch (err) {
|
|
6779
6993
|
release(err);
|
|
6780
6994
|
}
|
|
6781
6995
|
} else {
|
|
6782
|
-
|
|
6996
|
+
fs34.write(this.fd, this._writingBuf, release);
|
|
6783
6997
|
}
|
|
6784
6998
|
}
|
|
6785
6999
|
function actualWriteBuffer() {
|
|
@@ -6788,7 +7002,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6788
7002
|
this._writingBuf = this._writingBuf.length ? this._writingBuf : mergeBuf(this._bufs.shift(), this._lens.shift());
|
|
6789
7003
|
if (this.sync) {
|
|
6790
7004
|
try {
|
|
6791
|
-
const written =
|
|
7005
|
+
const written = fs34.writeSync(this.fd, this._writingBuf);
|
|
6792
7006
|
release(null, written);
|
|
6793
7007
|
} catch (err) {
|
|
6794
7008
|
release(err);
|
|
@@ -6797,7 +7011,7 @@ var require_sonic_boom = __commonJS({
|
|
|
6797
7011
|
if (kCopyBuffer) {
|
|
6798
7012
|
this._writingBuf = Buffer.from(this._writingBuf);
|
|
6799
7013
|
}
|
|
6800
|
-
|
|
7014
|
+
fs34.write(this.fd, this._writingBuf, release);
|
|
6801
7015
|
}
|
|
6802
7016
|
}
|
|
6803
7017
|
function actualClose(sonic) {
|
|
@@ -6813,12 +7027,12 @@ var require_sonic_boom = __commonJS({
|
|
|
6813
7027
|
sonic._lens = [];
|
|
6814
7028
|
assert(typeof sonic.fd === "number", `sonic.fd must be a number, got ${typeof sonic.fd}`);
|
|
6815
7029
|
try {
|
|
6816
|
-
|
|
7030
|
+
fs34.fsync(sonic.fd, closeWrapped);
|
|
6817
7031
|
} catch {
|
|
6818
7032
|
}
|
|
6819
7033
|
function closeWrapped() {
|
|
6820
7034
|
if (sonic.fd !== 1 && sonic.fd !== 2) {
|
|
6821
|
-
|
|
7035
|
+
fs34.close(sonic.fd, done);
|
|
6822
7036
|
} else {
|
|
6823
7037
|
done();
|
|
6824
7038
|
}
|
|
@@ -7075,7 +7289,7 @@ var require_thread_stream = __commonJS({
|
|
|
7075
7289
|
var { version: version2 } = require_package();
|
|
7076
7290
|
var { EventEmitter: EventEmitter2 } = require("events");
|
|
7077
7291
|
var { Worker } = require("worker_threads");
|
|
7078
|
-
var { join:
|
|
7292
|
+
var { join: join11 } = require("path");
|
|
7079
7293
|
var { pathToFileURL } = require("url");
|
|
7080
7294
|
var { wait } = require_wait();
|
|
7081
7295
|
var {
|
|
@@ -7111,7 +7325,7 @@ var require_thread_stream = __commonJS({
|
|
|
7111
7325
|
function createWorker(stream, opts) {
|
|
7112
7326
|
const { filename, workerData } = opts;
|
|
7113
7327
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
7114
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
7328
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join11(__dirname, "lib", "worker.js");
|
|
7115
7329
|
const worker = new Worker(toExecute, {
|
|
7116
7330
|
...opts.workerOpts,
|
|
7117
7331
|
trackUnmanagedFds: false,
|
|
@@ -7497,7 +7711,7 @@ var require_transport = __commonJS({
|
|
|
7497
7711
|
"use strict";
|
|
7498
7712
|
var { createRequire } = require("module");
|
|
7499
7713
|
var getCallers = require_caller();
|
|
7500
|
-
var { join:
|
|
7714
|
+
var { join: join11, isAbsolute, sep: sep2 } = require("path");
|
|
7501
7715
|
var sleep = require_atomic_sleep();
|
|
7502
7716
|
var onExit = require_on_exit_leak_free();
|
|
7503
7717
|
var ThreadStream = require_thread_stream();
|
|
@@ -7560,7 +7774,7 @@ var require_transport = __commonJS({
|
|
|
7560
7774
|
throw new Error("only one of target or targets can be specified");
|
|
7561
7775
|
}
|
|
7562
7776
|
if (targets) {
|
|
7563
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
7777
|
+
target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
|
|
7564
7778
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
7565
7779
|
return {
|
|
7566
7780
|
...dest,
|
|
@@ -7578,7 +7792,7 @@ var require_transport = __commonJS({
|
|
|
7578
7792
|
});
|
|
7579
7793
|
});
|
|
7580
7794
|
} else if (pipeline2) {
|
|
7581
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
7795
|
+
target = bundlerOverrides["pino-worker"] || join11(__dirname, "worker.js");
|
|
7582
7796
|
options.pipelines = [pipeline2.map((dest) => {
|
|
7583
7797
|
return {
|
|
7584
7798
|
...dest,
|
|
@@ -7600,12 +7814,12 @@ var require_transport = __commonJS({
|
|
|
7600
7814
|
return origin;
|
|
7601
7815
|
}
|
|
7602
7816
|
if (origin === "pino/file") {
|
|
7603
|
-
return
|
|
7817
|
+
return join11(__dirname, "..", "file.js");
|
|
7604
7818
|
}
|
|
7605
7819
|
let fixTarget2;
|
|
7606
7820
|
for (const filePath of callers) {
|
|
7607
7821
|
try {
|
|
7608
|
-
const context = filePath === "node:repl" ? process.cwd() +
|
|
7822
|
+
const context = filePath === "node:repl" ? process.cwd() + sep2 : filePath;
|
|
7609
7823
|
fixTarget2 = createRequire(context).resolve(origin);
|
|
7610
7824
|
break;
|
|
7611
7825
|
} catch (err) {
|
|
@@ -8590,7 +8804,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
8590
8804
|
return circularValue;
|
|
8591
8805
|
}
|
|
8592
8806
|
let res = "";
|
|
8593
|
-
let
|
|
8807
|
+
let join11 = ",";
|
|
8594
8808
|
const originalIndentation = indentation;
|
|
8595
8809
|
if (Array.isArray(value)) {
|
|
8596
8810
|
if (value.length === 0) {
|
|
@@ -8604,7 +8818,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
8604
8818
|
indentation += spacer;
|
|
8605
8819
|
res += `
|
|
8606
8820
|
${indentation}`;
|
|
8607
|
-
|
|
8821
|
+
join11 = `,
|
|
8608
8822
|
${indentation}`;
|
|
8609
8823
|
}
|
|
8610
8824
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -8612,13 +8826,13 @@ ${indentation}`;
|
|
|
8612
8826
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8613
8827
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
8614
8828
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
8615
|
-
res +=
|
|
8829
|
+
res += join11;
|
|
8616
8830
|
}
|
|
8617
8831
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
8618
8832
|
res += tmp !== void 0 ? tmp : "null";
|
|
8619
8833
|
if (value.length - 1 > maximumBreadth) {
|
|
8620
8834
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8621
|
-
res += `${
|
|
8835
|
+
res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8622
8836
|
}
|
|
8623
8837
|
if (spacer !== "") {
|
|
8624
8838
|
res += `
|
|
@@ -8639,7 +8853,7 @@ ${originalIndentation}`;
|
|
|
8639
8853
|
let separator = "";
|
|
8640
8854
|
if (spacer !== "") {
|
|
8641
8855
|
indentation += spacer;
|
|
8642
|
-
|
|
8856
|
+
join11 = `,
|
|
8643
8857
|
${indentation}`;
|
|
8644
8858
|
whitespace = " ";
|
|
8645
8859
|
}
|
|
@@ -8653,13 +8867,13 @@ ${indentation}`;
|
|
|
8653
8867
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
8654
8868
|
if (tmp !== void 0) {
|
|
8655
8869
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
8656
|
-
separator =
|
|
8870
|
+
separator = join11;
|
|
8657
8871
|
}
|
|
8658
8872
|
}
|
|
8659
8873
|
if (keyLength > maximumBreadth) {
|
|
8660
8874
|
const removedKeys = keyLength - maximumBreadth;
|
|
8661
8875
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
8662
|
-
separator =
|
|
8876
|
+
separator = join11;
|
|
8663
8877
|
}
|
|
8664
8878
|
if (spacer !== "" && separator.length > 1) {
|
|
8665
8879
|
res = `
|
|
@@ -8700,7 +8914,7 @@ ${originalIndentation}`;
|
|
|
8700
8914
|
}
|
|
8701
8915
|
const originalIndentation = indentation;
|
|
8702
8916
|
let res = "";
|
|
8703
|
-
let
|
|
8917
|
+
let join11 = ",";
|
|
8704
8918
|
if (Array.isArray(value)) {
|
|
8705
8919
|
if (value.length === 0) {
|
|
8706
8920
|
return "[]";
|
|
@@ -8713,7 +8927,7 @@ ${originalIndentation}`;
|
|
|
8713
8927
|
indentation += spacer;
|
|
8714
8928
|
res += `
|
|
8715
8929
|
${indentation}`;
|
|
8716
|
-
|
|
8930
|
+
join11 = `,
|
|
8717
8931
|
${indentation}`;
|
|
8718
8932
|
}
|
|
8719
8933
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -8721,13 +8935,13 @@ ${indentation}`;
|
|
|
8721
8935
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8722
8936
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
8723
8937
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
8724
|
-
res +=
|
|
8938
|
+
res += join11;
|
|
8725
8939
|
}
|
|
8726
8940
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
8727
8941
|
res += tmp !== void 0 ? tmp : "null";
|
|
8728
8942
|
if (value.length - 1 > maximumBreadth) {
|
|
8729
8943
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8730
|
-
res += `${
|
|
8944
|
+
res += `${join11}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8731
8945
|
}
|
|
8732
8946
|
if (spacer !== "") {
|
|
8733
8947
|
res += `
|
|
@@ -8740,7 +8954,7 @@ ${originalIndentation}`;
|
|
|
8740
8954
|
let whitespace = "";
|
|
8741
8955
|
if (spacer !== "") {
|
|
8742
8956
|
indentation += spacer;
|
|
8743
|
-
|
|
8957
|
+
join11 = `,
|
|
8744
8958
|
${indentation}`;
|
|
8745
8959
|
whitespace = " ";
|
|
8746
8960
|
}
|
|
@@ -8749,7 +8963,7 @@ ${indentation}`;
|
|
|
8749
8963
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
8750
8964
|
if (tmp !== void 0) {
|
|
8751
8965
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
8752
|
-
separator =
|
|
8966
|
+
separator = join11;
|
|
8753
8967
|
}
|
|
8754
8968
|
}
|
|
8755
8969
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -8807,20 +9021,20 @@ ${originalIndentation}`;
|
|
|
8807
9021
|
indentation += spacer;
|
|
8808
9022
|
let res2 = `
|
|
8809
9023
|
${indentation}`;
|
|
8810
|
-
const
|
|
9024
|
+
const join12 = `,
|
|
8811
9025
|
${indentation}`;
|
|
8812
9026
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
8813
9027
|
let i = 0;
|
|
8814
9028
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
8815
9029
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
8816
9030
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
8817
|
-
res2 +=
|
|
9031
|
+
res2 += join12;
|
|
8818
9032
|
}
|
|
8819
9033
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
8820
9034
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
8821
9035
|
if (value.length - 1 > maximumBreadth) {
|
|
8822
9036
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
8823
|
-
res2 += `${
|
|
9037
|
+
res2 += `${join12}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
8824
9038
|
}
|
|
8825
9039
|
res2 += `
|
|
8826
9040
|
${originalIndentation}`;
|
|
@@ -8836,16 +9050,16 @@ ${originalIndentation}`;
|
|
|
8836
9050
|
return '"[Object]"';
|
|
8837
9051
|
}
|
|
8838
9052
|
indentation += spacer;
|
|
8839
|
-
const
|
|
9053
|
+
const join11 = `,
|
|
8840
9054
|
${indentation}`;
|
|
8841
9055
|
let res = "";
|
|
8842
9056
|
let separator = "";
|
|
8843
9057
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
8844
9058
|
if (isTypedArrayWithEntries(value)) {
|
|
8845
|
-
res += stringifyTypedArray(value,
|
|
9059
|
+
res += stringifyTypedArray(value, join11, maximumBreadth);
|
|
8846
9060
|
keys = keys.slice(value.length);
|
|
8847
9061
|
maximumPropertiesToStringify -= value.length;
|
|
8848
|
-
separator =
|
|
9062
|
+
separator = join11;
|
|
8849
9063
|
}
|
|
8850
9064
|
if (deterministic) {
|
|
8851
9065
|
keys = sort(keys, comparator);
|
|
@@ -8856,13 +9070,13 @@ ${indentation}`;
|
|
|
8856
9070
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
8857
9071
|
if (tmp !== void 0) {
|
|
8858
9072
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
8859
|
-
separator =
|
|
9073
|
+
separator = join11;
|
|
8860
9074
|
}
|
|
8861
9075
|
}
|
|
8862
9076
|
if (keyLength > maximumBreadth) {
|
|
8863
9077
|
const removedKeys = keyLength - maximumBreadth;
|
|
8864
9078
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
8865
|
-
separator =
|
|
9079
|
+
separator = join11;
|
|
8866
9080
|
}
|
|
8867
9081
|
if (separator !== "") {
|
|
8868
9082
|
res = `
|
|
@@ -9953,11 +10167,11 @@ var init_lib = __esm({
|
|
|
9953
10167
|
}
|
|
9954
10168
|
}
|
|
9955
10169
|
},
|
|
9956
|
-
addToPath: function addToPath(
|
|
9957
|
-
var last =
|
|
10170
|
+
addToPath: function addToPath(path39, added, removed, oldPosInc, options) {
|
|
10171
|
+
var last = path39.lastComponent;
|
|
9958
10172
|
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
9959
10173
|
return {
|
|
9960
|
-
oldPos:
|
|
10174
|
+
oldPos: path39.oldPos + oldPosInc,
|
|
9961
10175
|
lastComponent: {
|
|
9962
10176
|
count: last.count + 1,
|
|
9963
10177
|
added,
|
|
@@ -9967,7 +10181,7 @@ var init_lib = __esm({
|
|
|
9967
10181
|
};
|
|
9968
10182
|
} else {
|
|
9969
10183
|
return {
|
|
9970
|
-
oldPos:
|
|
10184
|
+
oldPos: path39.oldPos + oldPosInc,
|
|
9971
10185
|
lastComponent: {
|
|
9972
10186
|
count: 1,
|
|
9973
10187
|
added,
|
|
@@ -10025,7 +10239,7 @@ var init_lib = __esm({
|
|
|
10025
10239
|
tokenize: function tokenize(value) {
|
|
10026
10240
|
return Array.from(value);
|
|
10027
10241
|
},
|
|
10028
|
-
join: function
|
|
10242
|
+
join: function join4(chars) {
|
|
10029
10243
|
return chars.join("");
|
|
10030
10244
|
},
|
|
10031
10245
|
postProcess: function postProcess(changeObjects) {
|
|
@@ -10398,10 +10612,10 @@ function attachmentToHistoryMessage(o, ts) {
|
|
|
10398
10612
|
const memories = raw.map((m2) => {
|
|
10399
10613
|
if (!m2 || typeof m2 !== "object") return null;
|
|
10400
10614
|
const rec = m2;
|
|
10401
|
-
const
|
|
10615
|
+
const path39 = typeof rec.path === "string" ? rec.path : null;
|
|
10402
10616
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
10403
|
-
if (!
|
|
10404
|
-
const entry = { path:
|
|
10617
|
+
if (!path39 || content == null) return null;
|
|
10618
|
+
const entry = { path: path39, content };
|
|
10405
10619
|
if (typeof rec.mtimeMs === "number") entry.mtimeMs = rec.mtimeMs;
|
|
10406
10620
|
return entry;
|
|
10407
10621
|
}).filter((m2) => m2 !== null);
|
|
@@ -10861,6 +11075,27 @@ var init_claude_history = __esm({
|
|
|
10861
11075
|
}
|
|
10862
11076
|
});
|
|
10863
11077
|
|
|
11078
|
+
// src/tools/sandbox.ts
|
|
11079
|
+
function shouldSandbox(cwd, personaRoot) {
|
|
11080
|
+
if (!personaRoot) return false;
|
|
11081
|
+
const sep2 = personaRoot.endsWith(path12.sep) ? "" : path12.sep;
|
|
11082
|
+
return cwd.startsWith(personaRoot + sep2) && cwd !== personaRoot;
|
|
11083
|
+
}
|
|
11084
|
+
function inferSandboxSettingsPath(cwd, personaRoot) {
|
|
11085
|
+
if (!shouldSandbox(cwd, personaRoot)) return null;
|
|
11086
|
+
const rel = path12.relative(personaRoot, cwd);
|
|
11087
|
+
const personaId = rel.split(path12.sep)[0];
|
|
11088
|
+
if (!personaId) return null;
|
|
11089
|
+
return path12.join(personaRoot, personaId, ".clawd", "sandbox-settings.json");
|
|
11090
|
+
}
|
|
11091
|
+
var path12;
|
|
11092
|
+
var init_sandbox = __esm({
|
|
11093
|
+
"src/tools/sandbox.ts"() {
|
|
11094
|
+
"use strict";
|
|
11095
|
+
path12 = __toESM(require("path"), 1);
|
|
11096
|
+
}
|
|
11097
|
+
});
|
|
11098
|
+
|
|
10864
11099
|
// src/tools/claude.ts
|
|
10865
11100
|
function macOSDesktopCandidates(home) {
|
|
10866
11101
|
return [
|
|
@@ -10910,7 +11145,7 @@ function buildSpawnArgs(ctx) {
|
|
|
10910
11145
|
];
|
|
10911
11146
|
if (ctx.model) args.push("--model", ctx.model);
|
|
10912
11147
|
switch (ctx.personaMode) {
|
|
10913
|
-
case "
|
|
11148
|
+
case "guest":
|
|
10914
11149
|
args.push("--setting-sources", "project,local");
|
|
10915
11150
|
break;
|
|
10916
11151
|
case "owner":
|
|
@@ -10925,7 +11160,8 @@ function buildSpawnArgs(ctx) {
|
|
|
10925
11160
|
throw new Error(`unexpected personaMode: ${String(_exhaustive)}`);
|
|
10926
11161
|
}
|
|
10927
11162
|
}
|
|
10928
|
-
|
|
11163
|
+
const sandboxSettings = ctx.extraSettings ?? inferSandboxSettingsPath(ctx.cwd, ctx.personaRoot);
|
|
11164
|
+
if (sandboxSettings) args.push("--settings", sandboxSettings);
|
|
10929
11165
|
if (ctx.extraSystemPrompt) args.push("--append-system-prompt", ctx.extraSystemPrompt);
|
|
10930
11166
|
if (ctx.effort) args.push("--effort", ctx.effort);
|
|
10931
11167
|
if (ctx.toolSessionId) args.push("--resume", ctx.toolSessionId);
|
|
@@ -11205,10 +11441,10 @@ function parseAttachment(obj) {
|
|
|
11205
11441
|
const memories = raw.map((m2) => {
|
|
11206
11442
|
if (!m2 || typeof m2 !== "object") return null;
|
|
11207
11443
|
const rec = m2;
|
|
11208
|
-
const
|
|
11444
|
+
const path39 = typeof rec.path === "string" ? rec.path : null;
|
|
11209
11445
|
const content = typeof rec.content === "string" ? rec.content : null;
|
|
11210
|
-
if (!
|
|
11211
|
-
const out = { path:
|
|
11446
|
+
if (!path39 || content == null) return null;
|
|
11447
|
+
const out = { path: path39, content };
|
|
11212
11448
|
if (typeof rec.mtimeMs === "number") out.mtimeMs = rec.mtimeMs;
|
|
11213
11449
|
return out;
|
|
11214
11450
|
}).filter((m2) => m2 !== null);
|
|
@@ -11324,6 +11560,7 @@ var init_claude = __esm({
|
|
|
11324
11560
|
init_protocol();
|
|
11325
11561
|
init_claude_history();
|
|
11326
11562
|
init_tool_result_extra();
|
|
11563
|
+
init_sandbox();
|
|
11327
11564
|
ATTACHMENT_SILENT_SUBTYPES2 = /* @__PURE__ */ new Set([
|
|
11328
11565
|
"hook_additional_context",
|
|
11329
11566
|
"hook_success",
|
|
@@ -18706,7 +18943,7 @@ var require_websocket = __commonJS({
|
|
|
18706
18943
|
var http2 = require("http");
|
|
18707
18944
|
var net = require("net");
|
|
18708
18945
|
var tls = require("tls");
|
|
18709
|
-
var { randomBytes, createHash } = require("crypto");
|
|
18946
|
+
var { randomBytes: randomBytes2, createHash: createHash3 } = require("crypto");
|
|
18710
18947
|
var { Duplex, Readable: Readable3 } = require("stream");
|
|
18711
18948
|
var { URL: URL2 } = require("url");
|
|
18712
18949
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -19236,7 +19473,7 @@ var require_websocket = __commonJS({
|
|
|
19236
19473
|
}
|
|
19237
19474
|
}
|
|
19238
19475
|
const defaultPort = isSecure ? 443 : 80;
|
|
19239
|
-
const key =
|
|
19476
|
+
const key = randomBytes2(16).toString("base64");
|
|
19240
19477
|
const request = isSecure ? https.request : http2.request;
|
|
19241
19478
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
19242
19479
|
let perMessageDeflate;
|
|
@@ -19366,7 +19603,7 @@ var require_websocket = __commonJS({
|
|
|
19366
19603
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
19367
19604
|
return;
|
|
19368
19605
|
}
|
|
19369
|
-
const digest =
|
|
19606
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
19370
19607
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
19371
19608
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
19372
19609
|
return;
|
|
@@ -19733,7 +19970,7 @@ var require_websocket_server = __commonJS({
|
|
|
19733
19970
|
var EventEmitter2 = require("events");
|
|
19734
19971
|
var http2 = require("http");
|
|
19735
19972
|
var { Duplex } = require("stream");
|
|
19736
|
-
var { createHash } = require("crypto");
|
|
19973
|
+
var { createHash: createHash3 } = require("crypto");
|
|
19737
19974
|
var extension2 = require_extension();
|
|
19738
19975
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
19739
19976
|
var subprotocol2 = require_subprotocol();
|
|
@@ -20034,7 +20271,7 @@ var require_websocket_server = __commonJS({
|
|
|
20034
20271
|
);
|
|
20035
20272
|
}
|
|
20036
20273
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
20037
|
-
const digest =
|
|
20274
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
20038
20275
|
const headers = [
|
|
20039
20276
|
"HTTP/1.1 101 Switching Protocols",
|
|
20040
20277
|
"Upgrade: websocket",
|
|
@@ -20653,6 +20890,9 @@ function createLogger(opts = {}) {
|
|
|
20653
20890
|
return wrap(base);
|
|
20654
20891
|
}
|
|
20655
20892
|
|
|
20893
|
+
// src/session/store-factory.ts
|
|
20894
|
+
var path5 = __toESM(require("path"), 1);
|
|
20895
|
+
|
|
20656
20896
|
// src/session/store.ts
|
|
20657
20897
|
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
20658
20898
|
var import_node_path4 = __toESM(require("path"), 1);
|
|
@@ -20661,10 +20901,14 @@ init_protocol();
|
|
|
20661
20901
|
// src/session/scope.ts
|
|
20662
20902
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
20663
20903
|
function scopeKey(scope) {
|
|
20664
|
-
|
|
20904
|
+
if (scope.kind === "default") return "default";
|
|
20905
|
+
if (scope.mode === "guest") return `persona:${scope.personaId}:guest:${scope.capId}`;
|
|
20906
|
+
return `persona:${scope.personaId}:${scope.mode}`;
|
|
20665
20907
|
}
|
|
20666
20908
|
function scopeSubPath(scope) {
|
|
20667
|
-
|
|
20909
|
+
if (scope.kind === "default") return ["default"];
|
|
20910
|
+
if (scope.mode === "guest") return [scope.personaId, "guests", scope.capId];
|
|
20911
|
+
return [scope.personaId, scope.mode];
|
|
20668
20912
|
}
|
|
20669
20913
|
function metaFromScope(scope, personaRoot) {
|
|
20670
20914
|
if (scope.kind === "default") return void 0;
|
|
@@ -20673,10 +20917,18 @@ function metaFromScope(scope, personaRoot) {
|
|
|
20673
20917
|
}
|
|
20674
20918
|
return {
|
|
20675
20919
|
idleKillEnabled: true,
|
|
20676
|
-
personaMode: "
|
|
20920
|
+
personaMode: "guest",
|
|
20677
20921
|
extraSettings: import_node_path3.default.join(personaRoot, scope.personaId, ".clawd", "sandbox-settings.json")
|
|
20678
20922
|
};
|
|
20679
20923
|
}
|
|
20924
|
+
function scopeForFile(file, ownerPrincipalId) {
|
|
20925
|
+
if (!file.ownerPersonaId) return { kind: "default" };
|
|
20926
|
+
const creator = file.creatorPrincipalId;
|
|
20927
|
+
if (ownerPrincipalId && creator && creator !== ownerPrincipalId) {
|
|
20928
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "guest", capId: creator };
|
|
20929
|
+
}
|
|
20930
|
+
return { kind: "persona", personaId: file.ownerPersonaId, mode: "owner" };
|
|
20931
|
+
}
|
|
20680
20932
|
|
|
20681
20933
|
// src/session/store.ts
|
|
20682
20934
|
function safeFileName(sessionId) {
|
|
@@ -20690,12 +20942,16 @@ function safeFileName(sessionId) {
|
|
|
20690
20942
|
var SessionStore = class {
|
|
20691
20943
|
root;
|
|
20692
20944
|
constructor(opts) {
|
|
20693
|
-
|
|
20694
|
-
|
|
20695
|
-
|
|
20696
|
-
"
|
|
20697
|
-
|
|
20698
|
-
|
|
20945
|
+
if ("root" in opts) {
|
|
20946
|
+
this.root = opts.root;
|
|
20947
|
+
} else {
|
|
20948
|
+
const scope = opts.scope ?? { kind: "default" };
|
|
20949
|
+
this.root = import_node_path4.default.join(
|
|
20950
|
+
opts.dataDir,
|
|
20951
|
+
"sessions",
|
|
20952
|
+
...scopeSubPath(scope).map(safeFileName)
|
|
20953
|
+
);
|
|
20954
|
+
}
|
|
20699
20955
|
}
|
|
20700
20956
|
filePath(sessionId) {
|
|
20701
20957
|
return import_node_path4.default.join(this.root, `${safeFileName(sessionId)}.json`);
|
|
@@ -20760,6 +21016,66 @@ var SessionStore = class {
|
|
|
20760
21016
|
}
|
|
20761
21017
|
};
|
|
20762
21018
|
|
|
21019
|
+
// src/session/store-factory.ts
|
|
21020
|
+
var SessionStoreFactory = class {
|
|
21021
|
+
dataDir;
|
|
21022
|
+
bareStore = null;
|
|
21023
|
+
vmOwnerStores = /* @__PURE__ */ new Map();
|
|
21024
|
+
// vmGuest 索引 key = `${pid}::${capId}`(按 cap.id 隔离,一张 cap 一个目录)
|
|
21025
|
+
vmGuestStores = /* @__PURE__ */ new Map();
|
|
21026
|
+
constructor(opts) {
|
|
21027
|
+
this.dataDir = opts.dataDir;
|
|
21028
|
+
}
|
|
21029
|
+
// ---- root path 派生(暴露给 migration / revoke cascade 等外部消费方) ----
|
|
21030
|
+
bareRoot() {
|
|
21031
|
+
return path5.join(this.dataDir, "sessions");
|
|
21032
|
+
}
|
|
21033
|
+
vmOwnerRoot(personaId) {
|
|
21034
|
+
return path5.join(
|
|
21035
|
+
this.dataDir,
|
|
21036
|
+
"personas",
|
|
21037
|
+
safeFileName(personaId),
|
|
21038
|
+
".clawd",
|
|
21039
|
+
"sessions",
|
|
21040
|
+
"owner"
|
|
21041
|
+
);
|
|
21042
|
+
}
|
|
21043
|
+
vmGuestRoot(personaId, capId) {
|
|
21044
|
+
return path5.join(
|
|
21045
|
+
this.dataDir,
|
|
21046
|
+
"personas",
|
|
21047
|
+
safeFileName(personaId),
|
|
21048
|
+
".clawd",
|
|
21049
|
+
"sessions",
|
|
21050
|
+
"guests",
|
|
21051
|
+
safeFileName(capId)
|
|
21052
|
+
);
|
|
21053
|
+
}
|
|
21054
|
+
// ---- SessionStore 工厂(缓存) ----
|
|
21055
|
+
forBare() {
|
|
21056
|
+
if (!this.bareStore) {
|
|
21057
|
+
this.bareStore = new SessionStore({ root: this.bareRoot() });
|
|
21058
|
+
}
|
|
21059
|
+
return this.bareStore;
|
|
21060
|
+
}
|
|
21061
|
+
forVmOwner(personaId) {
|
|
21062
|
+
const key = personaId;
|
|
21063
|
+
const cached = this.vmOwnerStores.get(key);
|
|
21064
|
+
if (cached) return cached;
|
|
21065
|
+
const st = new SessionStore({ root: this.vmOwnerRoot(personaId) });
|
|
21066
|
+
this.vmOwnerStores.set(key, st);
|
|
21067
|
+
return st;
|
|
21068
|
+
}
|
|
21069
|
+
forVmGuest(personaId, capId) {
|
|
21070
|
+
const key = `${personaId}::${capId}`;
|
|
21071
|
+
const cached = this.vmGuestStores.get(key);
|
|
21072
|
+
if (cached) return cached;
|
|
21073
|
+
const st = new SessionStore({ root: this.vmGuestRoot(personaId, capId) });
|
|
21074
|
+
this.vmGuestStores.set(key, st);
|
|
21075
|
+
return st;
|
|
21076
|
+
}
|
|
21077
|
+
};
|
|
21078
|
+
|
|
20763
21079
|
// src/session/manager.ts
|
|
20764
21080
|
var import_node_fs5 = __toESM(require("fs"), 1);
|
|
20765
21081
|
var import_node_path6 = __toESM(require("path"), 1);
|
|
@@ -20881,7 +21197,7 @@ function cloneState(s) {
|
|
|
20881
21197
|
pendingSend: s.pendingSend
|
|
20882
21198
|
};
|
|
20883
21199
|
}
|
|
20884
|
-
var IDLE_KILL_DELAY_MS =
|
|
21200
|
+
var IDLE_KILL_DELAY_MS = 6e4;
|
|
20885
21201
|
var SEEN_UUID_CAP = 2e3;
|
|
20886
21202
|
function applyEventBatch(state, events, deps) {
|
|
20887
21203
|
if (events.length === 0) return { state, effects: [] };
|
|
@@ -20921,7 +21237,10 @@ function buildSpawnContext(state, deps) {
|
|
|
20921
21237
|
toolSessionId: file.toolSessionId,
|
|
20922
21238
|
model: file.model,
|
|
20923
21239
|
permissionMode: file.permissionMode,
|
|
20924
|
-
effort: file.effort
|
|
21240
|
+
effort: file.effort,
|
|
21241
|
+
// Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
|
|
21242
|
+
// 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
|
|
21243
|
+
personaRoot: deps.personaRoot
|
|
20925
21244
|
};
|
|
20926
21245
|
const meta = state.subSessionMeta;
|
|
20927
21246
|
if (meta?.personaMode) {
|
|
@@ -21706,6 +22025,7 @@ var SessionRunner = class {
|
|
|
21706
22025
|
resolveContextWindow: this.hooks.resolveContextWindow,
|
|
21707
22026
|
genUuid: this.hooks.genUuid ?? v4_default,
|
|
21708
22027
|
ownerDisplayName: this.hooks.ownerDisplayName,
|
|
22028
|
+
personaRoot: this.hooks.personaRoot,
|
|
21709
22029
|
mode: this.hooks.mode ?? "sdk"
|
|
21710
22030
|
};
|
|
21711
22031
|
const { state, effects } = reduceSession(this.state, inputMsg, deps);
|
|
@@ -22137,9 +22457,18 @@ var SessionManager = class {
|
|
|
22137
22457
|
attachObserver(observer) {
|
|
22138
22458
|
this.attachedObserver = observer;
|
|
22139
22459
|
}
|
|
22140
|
-
// 按 scope 拿对应的 SessionStore
|
|
22141
|
-
//
|
|
22460
|
+
// 按 scope 拿对应的 SessionStore.
|
|
22461
|
+
// Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
|
|
22462
|
+
// default → factory.forBare() <dataDir>/sessions/
|
|
22463
|
+
// persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
|
|
22464
|
+
// persona/<pid>/guest → factory.forVmGuest(pid, capId) <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
|
|
22465
|
+
// 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
|
|
22142
22466
|
storeFor(scope) {
|
|
22467
|
+
if (this.deps.storeFactory) {
|
|
22468
|
+
if (scope.kind === "default") return this.deps.storeFactory.forBare();
|
|
22469
|
+
if (scope.mode === "owner") return this.deps.storeFactory.forVmOwner(scope.personaId);
|
|
22470
|
+
return this.deps.storeFactory.forVmGuest(scope.personaId, scope.capId);
|
|
22471
|
+
}
|
|
22143
22472
|
if (scope.kind === "default") return this.deps.store;
|
|
22144
22473
|
const key = scopeKey(scope);
|
|
22145
22474
|
const cached = this.storesByScope.get(key);
|
|
@@ -22159,19 +22488,18 @@ var SessionManager = class {
|
|
|
22159
22488
|
this.capabilitiesCache.set(tool, caps);
|
|
22160
22489
|
return caps;
|
|
22161
22490
|
}
|
|
22162
|
-
// 由 SessionFile
|
|
22163
|
-
//
|
|
22164
|
-
// session 出身(不可变);此函数只用于 SessionStore 的写入路由,不参与运行时行为
|
|
22165
|
-
// 决策(personaMode='owner' 等由路径 → scope → metaFromScope 派生)。
|
|
22166
|
-
// 仅覆盖 owner + default 两类——listener sub-session 不会从 default RPC 路径进来。
|
|
22491
|
+
// 由 SessionFile 反推 scope 的 thin wrapper —— 决策矩阵收敛到 scope.scopeForFile
|
|
22492
|
+
// free function(scope.spec.ts transition 表驱动测试),manager 只负责注入 deps.
|
|
22167
22493
|
scopeForFile(file) {
|
|
22168
|
-
return file
|
|
22494
|
+
return scopeForFile(file, this.deps.ownerPrincipalId);
|
|
22169
22495
|
}
|
|
22170
|
-
//
|
|
22171
|
-
//
|
|
22496
|
+
// 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned 跨 scope 查询.
|
|
22497
|
+
// Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
|
|
22498
|
+
// 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
|
|
22499
|
+
// <dataDir>/sessions/ 列子目录 (排除 'default').
|
|
22172
22500
|
listPersonaIdsOnDisk() {
|
|
22173
22501
|
if (!this.deps.dataDir) return [];
|
|
22174
|
-
const root = import_node_path6.default.join(this.deps.dataDir, "sessions");
|
|
22502
|
+
const root = this.deps.storeFactory ? import_node_path6.default.join(this.deps.dataDir, "personas") : import_node_path6.default.join(this.deps.dataDir, "sessions");
|
|
22175
22503
|
let entries;
|
|
22176
22504
|
try {
|
|
22177
22505
|
entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
|
|
@@ -22182,13 +22510,37 @@ var SessionManager = class {
|
|
|
22182
22510
|
}
|
|
22183
22511
|
return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
|
|
22184
22512
|
}
|
|
22185
|
-
//
|
|
22186
|
-
//
|
|
22513
|
+
// 扫某 persona 下 guests/ 子目录, 拿所有 capId.
|
|
22514
|
+
// 路径: <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
|
|
22515
|
+
// 只在 storeFactory 注入 (新布局) 下生效, 老布局无 guest 目录.
|
|
22516
|
+
listGuestCapIdsForPersona(personaId) {
|
|
22517
|
+
if (!this.deps.dataDir || !this.deps.storeFactory) return [];
|
|
22518
|
+
const root = import_node_path6.default.join(
|
|
22519
|
+
this.deps.dataDir,
|
|
22520
|
+
"personas",
|
|
22521
|
+
personaId,
|
|
22522
|
+
".clawd",
|
|
22523
|
+
"sessions",
|
|
22524
|
+
"guests"
|
|
22525
|
+
);
|
|
22526
|
+
let entries;
|
|
22527
|
+
try {
|
|
22528
|
+
entries = import_node_fs5.default.readdirSync(root, { withFileTypes: true });
|
|
22529
|
+
} catch (err) {
|
|
22530
|
+
const code = err?.code;
|
|
22531
|
+
if (code === "ENOENT") return [];
|
|
22532
|
+
throw err;
|
|
22533
|
+
}
|
|
22534
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
22535
|
+
}
|
|
22536
|
+
// owner / default + persona guests 三大分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
|
|
22537
|
+
// SessionManager 在这里定位。四层快慢路径:
|
|
22187
22538
|
// 1) active runner(用户当前在用的 session):runner.state.file 是 SessionFile 内存权威副本,
|
|
22188
22539
|
// 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
|
|
22189
22540
|
// 绝大多数场景
|
|
22190
22541
|
// 2) default scope 磁盘:inactive default session 命中
|
|
22191
22542
|
// 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
|
|
22543
|
+
// 4) 每个 persona 的 guests/<capId>/ 扫盘:guest 通过我颁发 token 接进来创建的 session
|
|
22192
22544
|
// listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
|
|
22193
22545
|
//
|
|
22194
22546
|
// 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
|
|
@@ -22200,8 +22552,13 @@ var SessionManager = class {
|
|
|
22200
22552
|
if (dflt) return dflt;
|
|
22201
22553
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22202
22554
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22203
|
-
const
|
|
22204
|
-
if (
|
|
22555
|
+
const found = ownerStore.read(sessionId);
|
|
22556
|
+
if (found) return found;
|
|
22557
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22558
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22559
|
+
const g2 = guestStore.read(sessionId);
|
|
22560
|
+
if (g2) return g2;
|
|
22561
|
+
}
|
|
22205
22562
|
}
|
|
22206
22563
|
return null;
|
|
22207
22564
|
}
|
|
@@ -22212,30 +22569,40 @@ var SessionManager = class {
|
|
|
22212
22569
|
const file = this.findOwnedSession(sessionId);
|
|
22213
22570
|
return file ? this.scopeForFile(file) : null;
|
|
22214
22571
|
}
|
|
22215
|
-
// 合并 default +
|
|
22216
|
-
//
|
|
22572
|
+
// 合并 default + 每个 persona 的 owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
|
|
22573
|
+
// guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
|
|
22574
|
+
// (creatorPrincipalId === capId), 这里仅负责数据源汇总.
|
|
22575
|
+
// listener sub-session 不走这条(listener 走 persona:listSubSessions RPC 单独入口)。
|
|
22217
22576
|
listAllOwned() {
|
|
22218
22577
|
const out = [];
|
|
22219
22578
|
out.push(...this.deps.store.list());
|
|
22220
22579
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22221
22580
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22222
22581
|
out.push(...ownerStore.list());
|
|
22582
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22583
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22584
|
+
out.push(...guestStore.list());
|
|
22585
|
+
}
|
|
22223
22586
|
}
|
|
22224
22587
|
return out.sort(
|
|
22225
22588
|
(a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
|
|
22226
22589
|
);
|
|
22227
22590
|
}
|
|
22228
|
-
// 写回 SessionFile,自动根据 ownerPersonaId 派生写入 scope
|
|
22229
|
-
// 调用方原先都是 `deps.store.write(updated)
|
|
22591
|
+
// 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
|
|
22592
|
+
// 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
|
|
22230
22593
|
writeOwned(file) {
|
|
22231
22594
|
return this.storeFor(this.scopeForFile(file)).write(file);
|
|
22232
22595
|
}
|
|
22233
|
-
// 删除 owned SessionFile(default
|
|
22596
|
+
// 删除 owned SessionFile(default / persona owner / persona guest),与 findOwnedSession 对称扫盘.
|
|
22234
22597
|
deleteOwned(sessionId) {
|
|
22235
22598
|
if (this.deps.store.delete(sessionId)) return true;
|
|
22236
22599
|
for (const personaId of this.listPersonaIdsOnDisk()) {
|
|
22237
22600
|
const ownerStore = this.storeFor({ kind: "persona", personaId, mode: "owner" });
|
|
22238
22601
|
if (ownerStore.delete(sessionId)) return true;
|
|
22602
|
+
for (const capId of this.listGuestCapIdsForPersona(personaId)) {
|
|
22603
|
+
const guestStore = this.storeFor({ kind: "persona", personaId, mode: "guest", capId });
|
|
22604
|
+
if (guestStore.delete(sessionId)) return true;
|
|
22605
|
+
}
|
|
22239
22606
|
}
|
|
22240
22607
|
return false;
|
|
22241
22608
|
}
|
|
@@ -22243,8 +22610,8 @@ var SessionManager = class {
|
|
|
22243
22610
|
// 经过 compressFrameForWire 后决定是 push collector 还是走 deps.broadcastFrame。
|
|
22244
22611
|
// scope 决定两件事:SessionStore 路由 + SubSessionMeta 派生(metaFromScope)。
|
|
22245
22612
|
// - default → 复用 deps.store,无 subSessionMeta(普通 session)
|
|
22246
|
-
// - persona/owner →
|
|
22247
|
-
// - persona/
|
|
22613
|
+
// - persona/owner → personas/<pid>/.clawd/sessions/owner/,meta={idleKillEnabled:false, personaMode:'owner'}
|
|
22614
|
+
// - persona/guest → personas/<pid>/.clawd/sessions/guests/<capId>/,meta={idleKillEnabled:true, personaMode:'guest', extraSettings:<sandbox>}
|
|
22248
22615
|
newRunner(file, scope) {
|
|
22249
22616
|
const adapter = this.deps.getAdapter(file.tool ?? "claude");
|
|
22250
22617
|
const store = this.storeFor(scope);
|
|
@@ -22263,6 +22630,9 @@ var SessionManager = class {
|
|
|
22263
22630
|
dataDir: this.deps.dataDir,
|
|
22264
22631
|
personaStore: this.deps.personaStore,
|
|
22265
22632
|
ownerDisplayName: this.deps.ownerDisplayName,
|
|
22633
|
+
// Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
|
|
22634
|
+
// cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
|
|
22635
|
+
personaRoot: this.deps.personaRoot,
|
|
22266
22636
|
// 透传给 ReducerDeps.mode:reducer send !procAlive spawn 路径据此判断要不要把
|
|
22267
22637
|
// readyForSend 翻 false(TUI 翻 false 让 user_text 走 pendingSend 暂存;SDK 不动保持直写)
|
|
22268
22638
|
mode: this.deps.mode,
|
|
@@ -22360,7 +22730,7 @@ var SessionManager = class {
|
|
|
22360
22730
|
}
|
|
22361
22731
|
}
|
|
22362
22732
|
// ---- 命令方法:均返回 { response, broadcast[] },由 dispatcher 聚合 ----
|
|
22363
|
-
create(args) {
|
|
22733
|
+
create(args, creatorPrincipalId) {
|
|
22364
22734
|
let cwd;
|
|
22365
22735
|
if (args.ownerPersonaId) {
|
|
22366
22736
|
cwd = derivePersonaSpawnCwd(
|
|
@@ -22401,10 +22771,11 @@ var SessionManager = class {
|
|
|
22401
22771
|
iconKey: args.iconKey,
|
|
22402
22772
|
forkedFromSessionId: args.forkedFromSessionId,
|
|
22403
22773
|
ownerPersonaId: args.ownerPersonaId,
|
|
22774
|
+
creatorPrincipalId,
|
|
22404
22775
|
createdAt: iso,
|
|
22405
22776
|
updatedAt: iso
|
|
22406
22777
|
};
|
|
22407
|
-
const scope =
|
|
22778
|
+
const scope = this.scopeForFile(file);
|
|
22408
22779
|
const written = this.storeFor(scope).write(file);
|
|
22409
22780
|
return { response: written, broadcast: [] };
|
|
22410
22781
|
}
|
|
@@ -22829,28 +23200,18 @@ var SessionManager = class {
|
|
|
22829
23200
|
this.runners.set(file.sessionId, r);
|
|
22830
23201
|
return r;
|
|
22831
23202
|
}
|
|
22832
|
-
// ----------------
|
|
23203
|
+
// ---------------- scope-explicit sub-session API ----------------
|
|
22833
23204
|
//
|
|
22834
|
-
//
|
|
22835
|
-
//
|
|
22836
|
-
// -
|
|
22837
|
-
// -
|
|
22838
|
-
// - sessionId 由 PersonaManager 派生(personaId-<tokenHash>),不走自动 newSessionId
|
|
23205
|
+
// 区别于 owner / default 单 sessionId 入口,这些 API 接 SessionScope 参数 ——
|
|
23206
|
+
// 调用方(PersonaManager / observer 等)明确传 scope,由 storeFor 路由到正确目录:
|
|
23207
|
+
// - {kind:'persona', personaId, mode:'guest', capId} → guests/<capId>/,meta={idleKill:true, sandbox}
|
|
23208
|
+
// - {kind:'persona', personaId, mode:'owner'} → owner/,meta={idleKill:false}
|
|
22839
23209
|
//
|
|
22840
|
-
//
|
|
22841
|
-
// {kind:'persona', personaId, mode:'listener'},与 owner / default 分类隔离。
|
|
23210
|
+
// SubSessionMeta 由 scope → metaFromScope 派生,不进 SessionFile schema。
|
|
22842
23211
|
/** 按 scope 读取 SessionFile;不存在返回 null */
|
|
22843
23212
|
readForScope(sessionId, scope) {
|
|
22844
23213
|
return this.storeFor(scope).read(sessionId);
|
|
22845
23214
|
}
|
|
22846
|
-
/**
|
|
22847
|
-
* 列出指定 persona 在指定 mode 下的所有 SessionFile。
|
|
22848
|
-
* persona:listSubSessions RPC 入口(mode='listener');
|
|
22849
|
-
* 路径下还未写过任何 session 时 list() 返回空数组(SessionStore.list 内部 ENOENT 处理)
|
|
22850
|
-
*/
|
|
22851
|
-
listPersonaSessions(personaId, mode) {
|
|
22852
|
-
return this.storeFor({ kind: "persona", personaId, mode }).list();
|
|
22853
|
-
}
|
|
22854
23215
|
/**
|
|
22855
23216
|
* 创建 sub-session 的 SessionFile + 准备 runner(不 spawn)。
|
|
22856
23217
|
* SubSessionMeta 不进 SessionFile schema,运行时由 scope → metaFromScope 派生。
|
|
@@ -22995,7 +23356,7 @@ var SessionManager = class {
|
|
|
22995
23356
|
return { response: { ok: true }, broadcast };
|
|
22996
23357
|
}
|
|
22997
23358
|
/**
|
|
22998
|
-
* 改 SessionFile.label
|
|
23359
|
+
* 改 SessionFile.label(scope-explicit sub-session 重命名入口)。
|
|
22999
23360
|
* 不存在抛 SESSION_NOT_FOUND。
|
|
23000
23361
|
*/
|
|
23001
23362
|
renameForScope(args) {
|
|
@@ -23010,7 +23371,7 @@ var SessionManager = class {
|
|
|
23010
23371
|
store.write({ ...file, label: args.label });
|
|
23011
23372
|
}
|
|
23012
23373
|
/**
|
|
23013
|
-
* 删除
|
|
23374
|
+
* 删除 scope-explicit sub-session。
|
|
23014
23375
|
* - 若 runner 存在:走 reducer 'delete' command 收尾(SIGKILL + 文件 unlink + broadcast)
|
|
23015
23376
|
* - 若 runner 不存在:直接删 scope store 文件 + 广播 session:deleted
|
|
23016
23377
|
* 不存在抛 SESSION_NOT_FOUND。
|
|
@@ -23281,7 +23642,7 @@ var SessionManager = class {
|
|
|
23281
23642
|
|
|
23282
23643
|
// src/persona/store.ts
|
|
23283
23644
|
var fs6 = __toESM(require("fs"), 1);
|
|
23284
|
-
var
|
|
23645
|
+
var path8 = __toESM(require("path"), 1);
|
|
23285
23646
|
init_protocol();
|
|
23286
23647
|
var DEFAULT_SETTINGS = {
|
|
23287
23648
|
permissions: {
|
|
@@ -23310,13 +23671,13 @@ var PersonaStore = class {
|
|
|
23310
23671
|
}
|
|
23311
23672
|
root;
|
|
23312
23673
|
personaDir(personaId) {
|
|
23313
|
-
return
|
|
23674
|
+
return path8.join(this.root, safeFileName(personaId));
|
|
23314
23675
|
}
|
|
23315
23676
|
metaPath(personaId) {
|
|
23316
|
-
return
|
|
23677
|
+
return path8.join(this.personaDir(personaId), ".clawd", "persona.json");
|
|
23317
23678
|
}
|
|
23318
23679
|
claudeMdPath(personaId) {
|
|
23319
|
-
return
|
|
23680
|
+
return path8.join(this.personaDir(personaId), "CLAUDE.md");
|
|
23320
23681
|
}
|
|
23321
23682
|
/**
|
|
23322
23683
|
* Sandbox settings 落盘路径 —— 故意放在 `.clawd/` 而不是 `.claude/`,让 CC 的
|
|
@@ -23326,11 +23687,11 @@ var PersonaStore = class {
|
|
|
23326
23687
|
* 加载 persona 人格,只有 listener 多一层 OS sandbox。
|
|
23327
23688
|
*/
|
|
23328
23689
|
sandboxSettingsPath(personaId) {
|
|
23329
|
-
return
|
|
23690
|
+
return path8.join(this.personaDir(personaId), ".clawd", "sandbox-settings.json");
|
|
23330
23691
|
}
|
|
23331
23692
|
write(persona, personality) {
|
|
23332
23693
|
const dir = this.personaDir(persona.personaId);
|
|
23333
|
-
fs6.mkdirSync(
|
|
23694
|
+
fs6.mkdirSync(path8.join(dir, ".clawd"), { recursive: true });
|
|
23334
23695
|
this.atomicWrite(this.claudeMdPath(persona.personaId), personality);
|
|
23335
23696
|
this.atomicWrite(
|
|
23336
23697
|
this.sandboxSettingsPath(persona.personaId),
|
|
@@ -23348,6 +23709,10 @@ var PersonaStore = class {
|
|
|
23348
23709
|
const p2 = this.metaPath(personaId);
|
|
23349
23710
|
if (!fs6.existsSync(p2)) return null;
|
|
23350
23711
|
const raw = JSON.parse(fs6.readFileSync(p2, "utf8"));
|
|
23712
|
+
if (raw && typeof raw === "object" && "tokenMap" in raw) {
|
|
23713
|
+
delete raw.tokenMap;
|
|
23714
|
+
this.atomicWrite(p2, JSON.stringify(raw, null, 2));
|
|
23715
|
+
}
|
|
23351
23716
|
return PersonaFileSchema.parse(raw);
|
|
23352
23717
|
}
|
|
23353
23718
|
has(personaId) {
|
|
@@ -23373,14 +23738,14 @@ var PersonaStore = class {
|
|
|
23373
23738
|
}
|
|
23374
23739
|
/** Persona 私有 skills 目录路径:<personaDir>/.claude/skills */
|
|
23375
23740
|
skillsDir(personaId) {
|
|
23376
|
-
return
|
|
23741
|
+
return path8.join(this.personaDir(personaId), ".claude", "skills");
|
|
23377
23742
|
}
|
|
23378
23743
|
/**
|
|
23379
23744
|
* Claude Code 项目级 settings 路径:`<personaDir>/.claude/settings.json`。
|
|
23380
23745
|
* 这里只读 `enabledPlugins` 字段,由 owner 通过 CC `/plugin` 之类命令维护,daemon 不写。
|
|
23381
23746
|
*/
|
|
23382
23747
|
claudeSettingsPath(personaId) {
|
|
23383
|
-
return
|
|
23748
|
+
return path8.join(this.personaDir(personaId), ".claude", "settings.json");
|
|
23384
23749
|
}
|
|
23385
23750
|
/**
|
|
23386
23751
|
* 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
|
|
@@ -23414,7 +23779,7 @@ var PersonaStore = class {
|
|
|
23414
23779
|
list() {
|
|
23415
23780
|
if (!fs6.existsSync(this.root)) return [];
|
|
23416
23781
|
return fs6.readdirSync(this.root).filter((name) => {
|
|
23417
|
-
return fs6.existsSync(
|
|
23782
|
+
return fs6.existsSync(path8.join(this.root, name, ".clawd", "persona.json"));
|
|
23418
23783
|
});
|
|
23419
23784
|
}
|
|
23420
23785
|
remove(personaId) {
|
|
@@ -23439,12 +23804,19 @@ var PersonaRegistry = class {
|
|
|
23439
23804
|
}
|
|
23440
23805
|
store;
|
|
23441
23806
|
cache = /* @__PURE__ */ new Map();
|
|
23442
|
-
/**
|
|
23807
|
+
/**
|
|
23808
|
+
* 从 store 全量重建缓存(boot 时 + 测试 setup 时)。
|
|
23809
|
+
* 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
|
|
23810
|
+
* try/catch 隔离,让其它 persona 仍能加载。
|
|
23811
|
+
*/
|
|
23443
23812
|
reload() {
|
|
23444
23813
|
this.cache.clear();
|
|
23445
23814
|
for (const id of this.store.list()) {
|
|
23446
|
-
|
|
23447
|
-
|
|
23815
|
+
try {
|
|
23816
|
+
const p2 = this.store.read(id);
|
|
23817
|
+
if (p2) this.cache.set(p2.personaId, p2);
|
|
23818
|
+
} catch {
|
|
23819
|
+
}
|
|
23448
23820
|
}
|
|
23449
23821
|
}
|
|
23450
23822
|
get(personaId) {
|
|
@@ -23460,21 +23832,6 @@ var PersonaRegistry = class {
|
|
|
23460
23832
|
list() {
|
|
23461
23833
|
return Array.from(this.cache.values());
|
|
23462
23834
|
}
|
|
23463
|
-
/**
|
|
23464
|
-
* file-sharing HTTP auth-context 用的逆向查表:只有 Bearer token,没有 personaId。
|
|
23465
|
-
* 线性扫所有 persona.tokenMap 找到第一条 ok 命中的;revoked / non-public persona 跳过。
|
|
23466
|
-
* persona 数量通常 < 100,性能可忽略。
|
|
23467
|
-
*/
|
|
23468
|
-
findByToken(token) {
|
|
23469
|
-
if (!token) return null;
|
|
23470
|
-
for (const persona of this.cache.values()) {
|
|
23471
|
-
if (!persona.public) continue;
|
|
23472
|
-
const entry = persona.tokenMap?.[token];
|
|
23473
|
-
if (!entry || entry.revoked) continue;
|
|
23474
|
-
return { personaId: persona.personaId, label: entry.label };
|
|
23475
|
-
}
|
|
23476
|
-
return null;
|
|
23477
|
-
}
|
|
23478
23835
|
};
|
|
23479
23836
|
|
|
23480
23837
|
// src/persona/manager.ts
|
|
@@ -23768,9 +24125,6 @@ var PersonaManager = class {
|
|
|
23768
24125
|
model: args.model,
|
|
23769
24126
|
public: args.public ?? false,
|
|
23770
24127
|
iconKey: args.iconKey,
|
|
23771
|
-
// 初始化空 token 集合;后续由 issueToken / revokeToken 维护,
|
|
23772
|
-
// file-sharing HTTP Bearer 鉴权用 PersonaRegistry.findByToken 反查
|
|
23773
|
-
tokenMap: {},
|
|
23774
24128
|
createdAt: now,
|
|
23775
24129
|
updatedAt: now
|
|
23776
24130
|
};
|
|
@@ -23820,52 +24174,22 @@ var PersonaManager = class {
|
|
|
23820
24174
|
}
|
|
23821
24175
|
/**
|
|
23822
24176
|
* 删除 persona。
|
|
23823
|
-
* PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/
|
|
24177
|
+
* PersonaStore.remove(personaId) 已级联删除整个 <personaRoot>/<personaId>/ 目录,
|
|
24178
|
+
* Phase 2 capability platform 新布局下含 .clawd/sessions/owner/ +
|
|
24179
|
+
* .clawd/sessions/guests/<capId>/, rmSync recursive force 一锅清.
|
|
24180
|
+
*
|
|
24181
|
+
* ⚠️ TODO (Phase 3+ design gap, reviewer P1 flagged):
|
|
24182
|
+
* ~/.clawd/capabilities.json 里若有 cap.grants 含本 personaId, grant 会变成
|
|
24183
|
+
* 悬空引用 (guest 仍能持 token 接入, 但调依赖该 persona 的 RPC 会 fail).
|
|
24184
|
+
* 选项: (A) 这里扫所有 cap 过滤掉该 personaId 的 grant; (B) revoke 所有 grant
|
|
24185
|
+
* 含该 personaId 的 cap. 当前因 cleanupGuestSessionsForCapability 用 rmSync
|
|
24186
|
+
* force=true 吞 ENOENT 不会 crash, 但 UI CapabilityManagerDrawer 会展示
|
|
24187
|
+
* "无效的 persona" grant. Phase 3 加 cross-reference 矩阵后正式实现.
|
|
23824
24188
|
*/
|
|
23825
24189
|
delete(personaId) {
|
|
23826
24190
|
this.deps.store.remove(personaId);
|
|
23827
24191
|
this.deps.registry.remove(personaId);
|
|
23828
24192
|
}
|
|
23829
|
-
/**
|
|
23830
|
-
* 生成 32-char base64url token 并写入 tokenMap,给 file-sharing HTTP Bearer
|
|
23831
|
-
* 鉴权用(PersonaRegistry.findByToken 反查 personaId/label)。
|
|
23832
|
-
*/
|
|
23833
|
-
issueToken(personaId, label) {
|
|
23834
|
-
const existing = this.deps.registry.get(personaId);
|
|
23835
|
-
if (!existing) throw new Error(`persona not found: ${personaId}`);
|
|
23836
|
-
const token = import_node_crypto3.default.randomBytes(24).toString("base64url");
|
|
23837
|
-
const entry = {
|
|
23838
|
-
label,
|
|
23839
|
-
issuedAt: Date.now(),
|
|
23840
|
-
revoked: false
|
|
23841
|
-
};
|
|
23842
|
-
const updated = {
|
|
23843
|
-
...existing,
|
|
23844
|
-
tokenMap: { ...existing.tokenMap ?? {}, [token]: entry },
|
|
23845
|
-
updatedAt: Date.now()
|
|
23846
|
-
};
|
|
23847
|
-
this.deps.store.writeMeta(updated);
|
|
23848
|
-
this.deps.registry.set(updated);
|
|
23849
|
-
return { token, persona: updated };
|
|
23850
|
-
}
|
|
23851
|
-
/** 标记 token 为 revoked(保留条目用于审计);不存在的 token 抛错。 */
|
|
23852
|
-
revokeToken(personaId, token) {
|
|
23853
|
-
const existing = this.deps.registry.get(personaId);
|
|
23854
|
-
if (!existing) throw new Error(`persona not found: ${personaId}`);
|
|
23855
|
-
const tokenEntry = existing.tokenMap?.[token];
|
|
23856
|
-
if (!tokenEntry) throw new Error(`token not found in persona ${personaId}`);
|
|
23857
|
-
const updated = {
|
|
23858
|
-
...existing,
|
|
23859
|
-
tokenMap: {
|
|
23860
|
-
...existing.tokenMap ?? {},
|
|
23861
|
-
[token]: { ...tokenEntry, revoked: true }
|
|
23862
|
-
},
|
|
23863
|
-
updatedAt: Date.now()
|
|
23864
|
-
};
|
|
23865
|
-
this.deps.store.writeMeta(updated);
|
|
23866
|
-
this.deps.registry.set(updated);
|
|
23867
|
-
return updated;
|
|
23868
|
-
}
|
|
23869
24193
|
/**
|
|
23870
24194
|
* label 转 4-16 char slug。优先用 `persona-<slug>`;若 slug 已被占用,追加 4 char
|
|
23871
24195
|
* base64url 随机后缀直到不撞为止(最多 5 次,理论冲突 ≈ 2^-24,留个 panic 上限)。
|
|
@@ -23885,7 +24209,7 @@ var PersonaManager = class {
|
|
|
23885
24209
|
|
|
23886
24210
|
// src/persona/seed.ts
|
|
23887
24211
|
var fs8 = __toESM(require("fs"), 1);
|
|
23888
|
-
var
|
|
24212
|
+
var path10 = __toESM(require("path"), 1);
|
|
23889
24213
|
var import_node_url = require("url");
|
|
23890
24214
|
var import_meta = {};
|
|
23891
24215
|
var DEFAULT_PERSONAS = [
|
|
@@ -23921,14 +24245,14 @@ var DEFAULT_PERSONAS = [
|
|
|
23921
24245
|
function findDefaultsRoot() {
|
|
23922
24246
|
const candidates = [];
|
|
23923
24247
|
try {
|
|
23924
|
-
const here =
|
|
23925
|
-
candidates.push(
|
|
23926
|
-
candidates.push(
|
|
24248
|
+
const here = path10.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
24249
|
+
candidates.push(path10.resolve(here, "defaults"));
|
|
24250
|
+
candidates.push(path10.resolve(here, "persona-defaults"));
|
|
23927
24251
|
} catch {
|
|
23928
24252
|
}
|
|
23929
24253
|
if (process.argv[1]) {
|
|
23930
|
-
const argvDir =
|
|
23931
|
-
candidates.push(
|
|
24254
|
+
const argvDir = path10.dirname(process.argv[1]);
|
|
24255
|
+
candidates.push(path10.resolve(argvDir, "persona-defaults"));
|
|
23932
24256
|
}
|
|
23933
24257
|
for (const c of candidates) {
|
|
23934
24258
|
try {
|
|
@@ -23945,7 +24269,7 @@ function seedDefaultPersonas(args) {
|
|
|
23945
24269
|
args.logger.info("persona.seed.skip", { personaId: entry.personaId, reason: "exists" });
|
|
23946
24270
|
continue;
|
|
23947
24271
|
}
|
|
23948
|
-
const bundleDir =
|
|
24272
|
+
const bundleDir = path10.join(args.defaultsRoot, entry.personaId);
|
|
23949
24273
|
if (!fs8.existsSync(bundleDir)) {
|
|
23950
24274
|
args.logger.warn("persona.seed.skip", {
|
|
23951
24275
|
personaId: entry.personaId,
|
|
@@ -23954,7 +24278,7 @@ function seedDefaultPersonas(args) {
|
|
|
23954
24278
|
});
|
|
23955
24279
|
continue;
|
|
23956
24280
|
}
|
|
23957
|
-
const claudeMdPath =
|
|
24281
|
+
const claudeMdPath = path10.join(bundleDir, "CLAUDE.md");
|
|
23958
24282
|
if (!fs8.existsSync(claudeMdPath)) {
|
|
23959
24283
|
args.logger.warn("persona.seed.skip", {
|
|
23960
24284
|
personaId: entry.personaId,
|
|
@@ -23971,7 +24295,6 @@ function seedDefaultPersonas(args) {
|
|
|
23971
24295
|
model: entry.model,
|
|
23972
24296
|
public: entry.public,
|
|
23973
24297
|
iconKey: entry.iconKey,
|
|
23974
|
-
tokenMap: {},
|
|
23975
24298
|
createdAt: now,
|
|
23976
24299
|
updatedAt: now
|
|
23977
24300
|
};
|
|
@@ -23983,8 +24306,8 @@ function seedDefaultPersonas(args) {
|
|
|
23983
24306
|
function copyBundleExtras(srcDir, dstDir) {
|
|
23984
24307
|
for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
|
|
23985
24308
|
if (entry.name === "CLAUDE.md" || entry.name === ".clawd") continue;
|
|
23986
|
-
const srcPath =
|
|
23987
|
-
const dstPath =
|
|
24309
|
+
const srcPath = path10.join(srcDir, entry.name);
|
|
24310
|
+
const dstPath = path10.join(dstDir, entry.name);
|
|
23988
24311
|
if (entry.isDirectory()) {
|
|
23989
24312
|
fs8.cpSync(srcPath, dstPath, { recursive: true, dereference: true });
|
|
23990
24313
|
} else if (entry.isFile()) {
|
|
@@ -25325,6 +25648,7 @@ var import_sender = __toESM(require_sender(), 1);
|
|
|
25325
25648
|
var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
25326
25649
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25327
25650
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25651
|
+
var wrapper_default = import_websocket.default;
|
|
25328
25652
|
|
|
25329
25653
|
// src/transport/local-ws-server.ts
|
|
25330
25654
|
var import_node_http = __toESM(require("http"), 1);
|
|
@@ -25339,6 +25663,9 @@ var LocalWsServer = class {
|
|
|
25339
25663
|
httpServer = null;
|
|
25340
25664
|
frameHandler = null;
|
|
25341
25665
|
clients = /* @__PURE__ */ new Map();
|
|
25666
|
+
// Task 1.7 capability platform:capId → Set<clientId>,撤销时 O(1) 找到该 cap 的
|
|
25667
|
+
// 所有活跃连接做关闭级联(Task 1.10)。同一 cap 可能多端同时连(多设备 / 多 tab)。
|
|
25668
|
+
capabilityIdToClients = /* @__PURE__ */ new Map();
|
|
25342
25669
|
logger;
|
|
25343
25670
|
pingIntervalMs;
|
|
25344
25671
|
async start() {
|
|
@@ -25429,6 +25756,49 @@ var LocalWsServer = class {
|
|
|
25429
25756
|
this.safeSend(c.ws, frame);
|
|
25430
25757
|
}
|
|
25431
25758
|
}
|
|
25759
|
+
// Task 1.9 capability platform:仅广播给 owner 连接(跳过 guest ws)。
|
|
25760
|
+
// 用于 capability:tokenIssued / capability:tokenDeleted 等 owner-only push frame——
|
|
25761
|
+
// guest 没必要也无法消费这类管理帧。noAuth localhost ws 没有 ctx, 视作 owner.
|
|
25762
|
+
broadcastToOwners(frame) {
|
|
25763
|
+
const gate = this.opts.authGate;
|
|
25764
|
+
for (const c of this.clients.values()) {
|
|
25765
|
+
if (gate && !gate.isAuthed(c.handle.id)) continue;
|
|
25766
|
+
if (c.ctx && c.ctx.principal.kind !== "owner") continue;
|
|
25767
|
+
this.safeSend(c.ws, frame);
|
|
25768
|
+
}
|
|
25769
|
+
}
|
|
25770
|
+
// capability platform v3 inbox 重写: 推给某条 cap channel 的两端 ws.
|
|
25771
|
+
// - 所有 owner ws (owner 看自家所有 channel)
|
|
25772
|
+
// - 该 capabilityId 的所有 guest ws (该 cap 持有人的多端/多 tab)
|
|
25773
|
+
// 一次调用同时推两端, inbox:event push 帧由此走.
|
|
25774
|
+
broadcastToCapabilityChannel(capabilityId, frame) {
|
|
25775
|
+
this.broadcastToOwners(frame);
|
|
25776
|
+
const set = this.capabilityIdToClients.get(capabilityId);
|
|
25777
|
+
if (!set) return;
|
|
25778
|
+
for (const id of set) {
|
|
25779
|
+
const c = this.clients.get(id);
|
|
25780
|
+
if (!c) continue;
|
|
25781
|
+
this.safeSend(c.ws, frame);
|
|
25782
|
+
}
|
|
25783
|
+
}
|
|
25784
|
+
// Phase 4 capability platform DM: 广播到指定 principal 的所有活跃 ws.
|
|
25785
|
+
// principalId === ownerPrincipalId → 同 broadcastToOwners (所有 owner ws)
|
|
25786
|
+
// principalId === 'owner' sentinel → 同上(向后兼容遗留 caller / 协议层 sentinel)
|
|
25787
|
+
// principalId === 'cap_xxx' → 该 capability 的所有 guest ws (多端 / 多 tab)
|
|
25788
|
+
// 用于 DM inbox:event 路由: from + to 两侧各播一次, 让双方 UI 实时看到 thread 更新.
|
|
25789
|
+
broadcastToPrincipal(principalId, frame) {
|
|
25790
|
+
if (principalId === "owner" || principalId === this.opts.ownerPrincipalId) {
|
|
25791
|
+
this.broadcastToOwners(frame);
|
|
25792
|
+
return;
|
|
25793
|
+
}
|
|
25794
|
+
const set = this.capabilityIdToClients.get(principalId);
|
|
25795
|
+
if (!set) return;
|
|
25796
|
+
for (const id of set) {
|
|
25797
|
+
const c = this.clients.get(id);
|
|
25798
|
+
if (!c) continue;
|
|
25799
|
+
this.safeSend(c.ws, frame);
|
|
25800
|
+
}
|
|
25801
|
+
}
|
|
25432
25802
|
firstSubscriber(sessionId) {
|
|
25433
25803
|
for (const c of this.clients.values()) {
|
|
25434
25804
|
if (c.handle.subscribedSessions.has(sessionId)) return c.handle;
|
|
@@ -25450,6 +25820,40 @@ var LocalWsServer = class {
|
|
|
25450
25820
|
if (!c) return;
|
|
25451
25821
|
this.safeSend(c.ws, frame);
|
|
25452
25822
|
}
|
|
25823
|
+
// Task 1.7 capability platform:AuthGate.onAuthed 回调里调本方法,把 ConnectionContext
|
|
25824
|
+
// 绑到 client;guest 连接同时进 capId 索引(Task 1.10 撤销级联用)。
|
|
25825
|
+
attachClientContext(clientId, ctx) {
|
|
25826
|
+
const c = this.clients.get(clientId);
|
|
25827
|
+
if (!c) return;
|
|
25828
|
+
c.ctx = ctx;
|
|
25829
|
+
if (ctx.capabilityId) {
|
|
25830
|
+
let set = this.capabilityIdToClients.get(ctx.capabilityId);
|
|
25831
|
+
if (!set) {
|
|
25832
|
+
set = /* @__PURE__ */ new Set();
|
|
25833
|
+
this.capabilityIdToClients.set(ctx.capabilityId, set);
|
|
25834
|
+
}
|
|
25835
|
+
set.add(clientId);
|
|
25836
|
+
}
|
|
25837
|
+
}
|
|
25838
|
+
// 测试 / Task 1.8 dispatcher 用:拿 client 当前 ConnectionContext(null = 未鉴权 / noAuth)
|
|
25839
|
+
getClientContext(clientId) {
|
|
25840
|
+
return this.clients.get(clientId)?.ctx ?? null;
|
|
25841
|
+
}
|
|
25842
|
+
// Task 1.10 撤销级联:关闭某 capability 的所有活跃 ws 连接。close code 4401
|
|
25843
|
+
// 让客户端识别 'capability revoked' 而非普通断连。
|
|
25844
|
+
closeConnectionsByCapability(capabilityId, code = 4401, reason = "TOKEN_REVOKED") {
|
|
25845
|
+
const set = this.capabilityIdToClients.get(capabilityId);
|
|
25846
|
+
if (!set) return;
|
|
25847
|
+
const ids = [...set];
|
|
25848
|
+
for (const id of ids) {
|
|
25849
|
+
const c = this.clients.get(id);
|
|
25850
|
+
if (!c) continue;
|
|
25851
|
+
try {
|
|
25852
|
+
c.ws.close(code, reason);
|
|
25853
|
+
} catch {
|
|
25854
|
+
}
|
|
25855
|
+
}
|
|
25856
|
+
}
|
|
25453
25857
|
// URL path 路由:'/' → 主连接路径;其他 → close 4404
|
|
25454
25858
|
routeConnection(socket, req) {
|
|
25455
25859
|
const remoteAddress = req?.socket?.remoteAddress;
|
|
@@ -25484,7 +25888,7 @@ var LocalWsServer = class {
|
|
|
25484
25888
|
} catch {
|
|
25485
25889
|
}
|
|
25486
25890
|
}, this.pingIntervalMs);
|
|
25487
|
-
this.clients.set(id, { handle, ws: socket, pingTimer });
|
|
25891
|
+
this.clients.set(id, { handle, ws: socket, pingTimer, ctx: null });
|
|
25488
25892
|
this.logger?.info("client connected", { clientId: id, total: this.clients.size, remoteAddress });
|
|
25489
25893
|
const authGate = this.opts.authGate;
|
|
25490
25894
|
let authed = true;
|
|
@@ -25508,6 +25912,12 @@ var LocalWsServer = class {
|
|
|
25508
25912
|
socket.on("close", () => {
|
|
25509
25913
|
const c = this.clients.get(id);
|
|
25510
25914
|
if (c?.pingTimer) clearInterval(c.pingTimer);
|
|
25915
|
+
const capId = c?.ctx?.capabilityId;
|
|
25916
|
+
if (capId) {
|
|
25917
|
+
const set = this.capabilityIdToClients.get(capId);
|
|
25918
|
+
set?.delete(id);
|
|
25919
|
+
if (set && set.size === 0) this.capabilityIdToClients.delete(capId);
|
|
25920
|
+
}
|
|
25511
25921
|
this.clients.delete(id);
|
|
25512
25922
|
authGate?.unregister(id);
|
|
25513
25923
|
this.logger?.info("client disconnected", { clientId: id, remaining: this.clients.size });
|
|
@@ -25540,6 +25950,17 @@ var LocalWsServer = class {
|
|
|
25540
25950
|
return;
|
|
25541
25951
|
}
|
|
25542
25952
|
} else if (frame.type === "auth") {
|
|
25953
|
+
const token = typeof frame.token === "string" ? frame.token ?? "" : "";
|
|
25954
|
+
const selfPrincipalId = typeof frame.selfPrincipalId === "string" ? frame.selfPrincipalId ?? "" : "";
|
|
25955
|
+
const selfDisplayName = typeof frame.selfDisplayName === "string" ? frame.selfDisplayName ?? "" : "";
|
|
25956
|
+
if (token && this.opts.tryVerifyCapabilityToken) {
|
|
25957
|
+
const guestCtx = this.opts.tryVerifyCapabilityToken(
|
|
25958
|
+
token,
|
|
25959
|
+
selfPrincipalId || void 0,
|
|
25960
|
+
selfDisplayName || void 0
|
|
25961
|
+
);
|
|
25962
|
+
if (guestCtx) this.attachClientContext(client.id, guestCtx);
|
|
25963
|
+
}
|
|
25543
25964
|
this.safeSend(this.clients.get(client.id).ws, { type: "auth:ok" });
|
|
25544
25965
|
return;
|
|
25545
25966
|
}
|
|
@@ -25656,15 +26077,31 @@ var AuthGate = class {
|
|
|
25656
26077
|
this.markFailed(handle.id);
|
|
25657
26078
|
return frame.type === "auth" ? "consumed" : "reject";
|
|
25658
26079
|
}
|
|
25659
|
-
|
|
25660
|
-
|
|
25661
|
-
this.
|
|
25662
|
-
|
|
25663
|
-
|
|
25664
|
-
|
|
25665
|
-
|
|
25666
|
-
|
|
25667
|
-
|
|
26080
|
+
let ctx = null;
|
|
26081
|
+
if (this.opts.authenticate) {
|
|
26082
|
+
const r = this.opts.authenticate(
|
|
26083
|
+
parsed.data.token,
|
|
26084
|
+
parsed.data.selfPrincipalId,
|
|
26085
|
+
parsed.data.selfDisplayName
|
|
26086
|
+
);
|
|
26087
|
+
if (!r.ok) {
|
|
26088
|
+
this.opts.closeConnection(handle, 4401, r.code);
|
|
26089
|
+
this.markFailed(handle.id);
|
|
26090
|
+
return "consumed";
|
|
26091
|
+
}
|
|
26092
|
+
ctx = r.context;
|
|
26093
|
+
} else {
|
|
26094
|
+
if (!this.opts.expectedToken) {
|
|
26095
|
+
this.opts.closeConnection(handle, 1008, "auth not configured");
|
|
26096
|
+
this.markFailed(handle.id);
|
|
26097
|
+
return "consumed";
|
|
26098
|
+
}
|
|
26099
|
+
if (!constantTimeEqual(parsed.data.token, this.opts.expectedToken)) {
|
|
26100
|
+
this.opts.closeConnection(handle, 1008, "auth failed");
|
|
26101
|
+
this.markFailed(handle.id);
|
|
26102
|
+
return "consumed";
|
|
26103
|
+
}
|
|
26104
|
+
ctx = this.opts.buildOwnerContext?.() ?? null;
|
|
25668
26105
|
}
|
|
25669
26106
|
if (st.timer != null) {
|
|
25670
26107
|
const clear = this.opts.clearTimer ?? ((h) => clearTimeout(h));
|
|
@@ -25672,6 +26109,7 @@ var AuthGate = class {
|
|
|
25672
26109
|
}
|
|
25673
26110
|
st.authed = true;
|
|
25674
26111
|
st.timer = null;
|
|
26112
|
+
if (ctx && this.opts.onAuthed) this.opts.onAuthed(handle, ctx);
|
|
25675
26113
|
this.opts.sendOk(handle, { type: "auth:ok" });
|
|
25676
26114
|
return "consumed";
|
|
25677
26115
|
}
|
|
@@ -25707,7 +26145,7 @@ var AuthContextResolver = class {
|
|
|
25707
26145
|
}
|
|
25708
26146
|
opts;
|
|
25709
26147
|
/**
|
|
25710
|
-
* 从 `Authorization` 头解析。null = 无凭证 /
|
|
26148
|
+
* 从 `Authorization` 头解析。null = 无凭证 / 不识别,调用方一律 401。
|
|
25711
26149
|
* remoteAddress 用于 isLoopback 判定(loopback = 127.0.0.1 / ::1 / ::ffff:127.0.0.1)。
|
|
25712
26150
|
*/
|
|
25713
26151
|
resolveFromHeader(authHeader, remoteAddress) {
|
|
@@ -25717,15 +26155,6 @@ var AuthContextResolver = class {
|
|
|
25717
26155
|
if (this.opts.ownerToken && constantTimeEqual2(token, this.opts.ownerToken)) {
|
|
25718
26156
|
return { role: "owner", isLoopback };
|
|
25719
26157
|
}
|
|
25720
|
-
const personalHit = this.opts.personaRegistry.findByToken(token);
|
|
25721
|
-
if (personalHit) {
|
|
25722
|
-
return {
|
|
25723
|
-
role: "personal",
|
|
25724
|
-
personaId: personalHit.personaId,
|
|
25725
|
-
label: personalHit.label,
|
|
25726
|
-
isLoopback
|
|
25727
|
-
};
|
|
25728
|
-
}
|
|
25729
26158
|
return null;
|
|
25730
26159
|
}
|
|
25731
26160
|
};
|
|
@@ -25747,206 +26176,753 @@ function constantTimeEqual2(a, b2) {
|
|
|
25747
26176
|
return diff2 === 0;
|
|
25748
26177
|
}
|
|
25749
26178
|
|
|
25750
|
-
// src/
|
|
25751
|
-
|
|
25752
|
-
var import_node_path16 = __toESM(require("path"), 1);
|
|
26179
|
+
// ../protocol/src/index.ts
|
|
26180
|
+
init_runtime();
|
|
25753
26181
|
|
|
25754
|
-
// src/
|
|
25755
|
-
|
|
25756
|
-
|
|
25757
|
-
|
|
25758
|
-
|
|
25759
|
-
|
|
26182
|
+
// src/transport/connection-context.ts
|
|
26183
|
+
function ownerContext(ownerPrincipalId, displayName) {
|
|
26184
|
+
return {
|
|
26185
|
+
principal: makeOwnerPrincipal(ownerPrincipalId, displayName),
|
|
26186
|
+
grants: [{ resource: { type: "*" }, actions: ["admin"] }]
|
|
26187
|
+
};
|
|
26188
|
+
}
|
|
26189
|
+
function guestContext(cap) {
|
|
26190
|
+
return {
|
|
26191
|
+
principal: { id: cap.id, kind: "guest", displayName: cap.displayName },
|
|
26192
|
+
grants: cap.grants,
|
|
26193
|
+
capabilityId: cap.id
|
|
26194
|
+
};
|
|
26195
|
+
}
|
|
26196
|
+
function authenticate(token, deps) {
|
|
26197
|
+
if (!token) return { ok: false, code: "NO_TOKEN" };
|
|
26198
|
+
if (deps.isOwnerToken(token)) {
|
|
26199
|
+
return { ok: true, context: ownerContext(deps.ownerPrincipalId, deps.ownerDisplayName) };
|
|
26200
|
+
}
|
|
26201
|
+
if (!deps.capabilityRegistry) return { ok: false, code: "BAD_TOKEN" };
|
|
26202
|
+
const v2 = deps.capabilityRegistry.verifyToken(token);
|
|
26203
|
+
if (!v2.ok) {
|
|
26204
|
+
if (v2.code === "TOKEN_INVALID") return { ok: false, code: "BAD_TOKEN" };
|
|
26205
|
+
return { ok: false, code: v2.code };
|
|
26206
|
+
}
|
|
26207
|
+
if (deps.selfPrincipalId && deps.selfDisplayName && deps.onGuestHello) {
|
|
26208
|
+
try {
|
|
26209
|
+
deps.onGuestHello(v2.capability.id, deps.selfPrincipalId, deps.selfDisplayName);
|
|
26210
|
+
} catch (e) {
|
|
26211
|
+
if (e.message === "TOKEN_SHARED_LEAK") {
|
|
26212
|
+
return { ok: false, code: "TOKEN_REVOKED" };
|
|
26213
|
+
}
|
|
26214
|
+
throw e;
|
|
26215
|
+
}
|
|
26216
|
+
}
|
|
26217
|
+
return { ok: true, context: guestContext(v2.capability) };
|
|
26218
|
+
}
|
|
26219
|
+
|
|
26220
|
+
// src/permission/capability-store.ts
|
|
26221
|
+
var fs15 = __toESM(require("fs"), 1);
|
|
26222
|
+
var path18 = __toESM(require("path"), 1);
|
|
26223
|
+
var CAPABILITIES_FILE_NAME = "capabilities.json";
|
|
26224
|
+
var FILE_VERSION = 1;
|
|
26225
|
+
var CapabilityStore = class {
|
|
26226
|
+
constructor(dataDir) {
|
|
26227
|
+
this.dataDir = dataDir;
|
|
26228
|
+
fs15.mkdirSync(dataDir, { recursive: true });
|
|
26229
|
+
this.cache = this.readFromDisk();
|
|
26230
|
+
}
|
|
25760
26231
|
dataDir;
|
|
25761
|
-
|
|
25762
|
-
|
|
25763
|
-
|
|
25764
|
-
this.dataDir = opts.dataDir;
|
|
25765
|
-
this.logger = opts.logger;
|
|
26232
|
+
cache;
|
|
26233
|
+
list() {
|
|
26234
|
+
return [...this.cache];
|
|
25766
26235
|
}
|
|
25767
|
-
|
|
25768
|
-
|
|
26236
|
+
upsert(cap) {
|
|
26237
|
+
const idx = this.cache.findIndex((c) => c.id === cap.id);
|
|
26238
|
+
if (idx >= 0) {
|
|
26239
|
+
this.cache[idx] = cap;
|
|
26240
|
+
} else {
|
|
26241
|
+
this.cache.push(cap);
|
|
26242
|
+
}
|
|
26243
|
+
this.flush();
|
|
25769
26244
|
}
|
|
25770
|
-
|
|
25771
|
-
|
|
25772
|
-
|
|
26245
|
+
remove(id) {
|
|
26246
|
+
const next = this.cache.filter((c) => c.id !== id);
|
|
26247
|
+
if (next.length === this.cache.length) return;
|
|
26248
|
+
this.cache = next;
|
|
26249
|
+
this.flush();
|
|
25773
26250
|
}
|
|
25774
|
-
|
|
25775
|
-
return
|
|
26251
|
+
filePath() {
|
|
26252
|
+
return path18.join(this.dataDir, CAPABILITIES_FILE_NAME);
|
|
25776
26253
|
}
|
|
25777
|
-
|
|
25778
|
-
|
|
25779
|
-
|
|
26254
|
+
readFromDisk() {
|
|
26255
|
+
const file = this.filePath();
|
|
26256
|
+
let raw;
|
|
25780
26257
|
try {
|
|
25781
|
-
|
|
25782
|
-
const parsed = JSON.parse(raw);
|
|
25783
|
-
if (!Array.isArray(parsed)) {
|
|
25784
|
-
this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
|
|
25785
|
-
file
|
|
25786
|
-
});
|
|
25787
|
-
return [];
|
|
25788
|
-
}
|
|
25789
|
-
const out = [];
|
|
25790
|
-
for (const entry of parsed) {
|
|
25791
|
-
const r = GroupFileEntrySchema.safeParse(entry);
|
|
25792
|
-
if (r.success) out.push(r.data);
|
|
25793
|
-
}
|
|
25794
|
-
return out;
|
|
26258
|
+
raw = fs15.readFileSync(file, "utf8");
|
|
25795
26259
|
} catch (err) {
|
|
25796
|
-
|
|
25797
|
-
if (code === "ENOENT") return [];
|
|
25798
|
-
this.logger?.warn("GroupFileStore.readFile failed", {
|
|
25799
|
-
file,
|
|
25800
|
-
err: err.message
|
|
25801
|
-
});
|
|
26260
|
+
if (err?.code === "ENOENT") return [];
|
|
25802
26261
|
return [];
|
|
25803
26262
|
}
|
|
26263
|
+
if (!raw.trim()) return [];
|
|
26264
|
+
let parsed;
|
|
26265
|
+
try {
|
|
26266
|
+
parsed = JSON.parse(raw);
|
|
26267
|
+
} catch {
|
|
26268
|
+
return [];
|
|
26269
|
+
}
|
|
26270
|
+
if (!parsed || typeof parsed !== "object") return [];
|
|
26271
|
+
const arr = parsed.capabilities;
|
|
26272
|
+
if (!Array.isArray(arr)) return [];
|
|
26273
|
+
const out = [];
|
|
26274
|
+
for (const item of arr) {
|
|
26275
|
+
const r = CapabilitySchema.safeParse(item);
|
|
26276
|
+
if (r.success) out.push(r.data);
|
|
26277
|
+
}
|
|
26278
|
+
return out;
|
|
25804
26279
|
}
|
|
25805
|
-
|
|
25806
|
-
const
|
|
25807
|
-
|
|
25808
|
-
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
25809
|
-
import_node_fs13.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
25810
|
-
import_node_fs13.default.renameSync(tmp, file);
|
|
26280
|
+
flush() {
|
|
26281
|
+
const content = { version: FILE_VERSION, capabilities: this.cache };
|
|
26282
|
+
this.atomicWrite(this.filePath(), JSON.stringify(content, null, 2));
|
|
25811
26283
|
}
|
|
25812
|
-
|
|
25813
|
-
|
|
25814
|
-
|
|
25815
|
-
|
|
25816
|
-
|
|
25817
|
-
|
|
25818
|
-
|
|
25819
|
-
|
|
26284
|
+
atomicWrite(file, content) {
|
|
26285
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
26286
|
+
fs15.writeFileSync(tmp, content, { mode: 384 });
|
|
26287
|
+
fs15.renameSync(tmp, file);
|
|
26288
|
+
try {
|
|
26289
|
+
fs15.chmodSync(file, 384);
|
|
26290
|
+
} catch {
|
|
26291
|
+
}
|
|
26292
|
+
}
|
|
26293
|
+
};
|
|
26294
|
+
|
|
26295
|
+
// src/permission/capability-registry.ts
|
|
26296
|
+
var crypto4 = __toESM(require("crypto"), 1);
|
|
26297
|
+
var CapabilityRegistry = class {
|
|
26298
|
+
constructor(store, now = () => Date.now()) {
|
|
26299
|
+
this.store = store;
|
|
26300
|
+
this.now = now;
|
|
26301
|
+
for (const cap of store.list()) {
|
|
26302
|
+
this.bySecretHash.set(cap.secretHash, cap);
|
|
26303
|
+
}
|
|
26304
|
+
}
|
|
26305
|
+
store;
|
|
26306
|
+
now;
|
|
26307
|
+
// sha256(token) → Capability
|
|
26308
|
+
bySecretHash = /* @__PURE__ */ new Map();
|
|
26309
|
+
list() {
|
|
26310
|
+
return this.store.list();
|
|
26311
|
+
}
|
|
26312
|
+
verifyToken(token) {
|
|
26313
|
+
if (!token) return { ok: false, code: "TOKEN_INVALID" };
|
|
26314
|
+
const hash = sha256Hex(token);
|
|
26315
|
+
const cap = this.bySecretHash.get(hash);
|
|
26316
|
+
if (!cap) return { ok: false, code: "TOKEN_INVALID" };
|
|
26317
|
+
if (cap.expiresAt && this.now() >= cap.expiresAt) {
|
|
26318
|
+
return { ok: false, code: "TOKEN_EXPIRED" };
|
|
26319
|
+
}
|
|
26320
|
+
if (cap.maxUses && cap.usedCount >= cap.maxUses) {
|
|
26321
|
+
return { ok: false, code: "TOKEN_EXHAUSTED" };
|
|
26322
|
+
}
|
|
26323
|
+
const updated = { ...cap, usedCount: cap.usedCount + 1 };
|
|
26324
|
+
this.bySecretHash.set(hash, updated);
|
|
26325
|
+
this.store.upsert(updated);
|
|
26326
|
+
return { ok: true, capability: updated };
|
|
26327
|
+
}
|
|
26328
|
+
upsertCapability(cap) {
|
|
26329
|
+
this.bySecretHash.set(cap.secretHash, cap);
|
|
26330
|
+
this.store.upsert(cap);
|
|
25820
26331
|
}
|
|
25821
26332
|
/**
|
|
25822
|
-
*
|
|
25823
|
-
*
|
|
25824
|
-
*
|
|
25825
|
-
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
25826
|
-
*
|
|
25827
|
-
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
26333
|
+
* Hard delete:从 store 物理移除 + 从 Map 清掉 secretHash 索引。
|
|
26334
|
+
* 后续 verifyToken 该 token 走 'TOKEN_INVALID' 分支(secretHash 已不在 Map)。
|
|
26335
|
+
* idempotent:不存在 → null。
|
|
25828
26336
|
*/
|
|
25829
|
-
|
|
25830
|
-
const
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
|
|
25834
|
-
|
|
25835
|
-
|
|
25836
|
-
|
|
25837
|
-
|
|
25838
|
-
|
|
25839
|
-
|
|
25840
|
-
|
|
25841
|
-
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
|
|
25846
|
-
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
25850
|
-
|
|
25851
|
-
|
|
25852
|
-
|
|
25853
|
-
|
|
25854
|
-
|
|
25855
|
-
|
|
26337
|
+
delete(id) {
|
|
26338
|
+
const current = this.store.list().find((c) => c.id === id);
|
|
26339
|
+
if (!current) return null;
|
|
26340
|
+
this.bySecretHash.delete(current.secretHash);
|
|
26341
|
+
this.store.remove(id);
|
|
26342
|
+
return current;
|
|
26343
|
+
}
|
|
26344
|
+
findById(id) {
|
|
26345
|
+
return this.store.list().find((c) => c.id === id) ?? null;
|
|
26346
|
+
}
|
|
26347
|
+
};
|
|
26348
|
+
function sha256Hex(s) {
|
|
26349
|
+
return crypto4.createHash("sha256").update(s).digest("hex");
|
|
26350
|
+
}
|
|
26351
|
+
|
|
26352
|
+
// src/permission/capability-manager.ts
|
|
26353
|
+
var crypto5 = __toESM(require("crypto"), 1);
|
|
26354
|
+
var CapabilityManager = class {
|
|
26355
|
+
constructor(registry2, hooks = {}) {
|
|
26356
|
+
this.registry = registry2;
|
|
26357
|
+
this.hooks = hooks;
|
|
26358
|
+
}
|
|
26359
|
+
registry;
|
|
26360
|
+
hooks;
|
|
26361
|
+
list() {
|
|
26362
|
+
return this.registry.list();
|
|
26363
|
+
}
|
|
26364
|
+
// whoami handler 用:根据 capabilityId 反查 caller 的 capability 实体。
|
|
26365
|
+
// 返回 Capability 持久化形态;handler 调 stripSecretHash 后再上 wire。
|
|
26366
|
+
findById(id) {
|
|
26367
|
+
return this.registry.findById(id);
|
|
26368
|
+
}
|
|
26369
|
+
issue(args) {
|
|
26370
|
+
if (!args.displayName.trim()) {
|
|
26371
|
+
throw new Error("CapabilityManager.issue: displayName must be non-empty");
|
|
25856
26372
|
}
|
|
25857
|
-
this.
|
|
25858
|
-
this.
|
|
25859
|
-
|
|
26373
|
+
const token = (this.hooks.generateToken ?? defaultGenerateToken)();
|
|
26374
|
+
const id = (this.hooks.generateId ?? defaultGenerateId)();
|
|
26375
|
+
const now = (this.hooks.now ?? Date.now)();
|
|
26376
|
+
const cap = {
|
|
26377
|
+
id,
|
|
26378
|
+
secretHash: sha256Hex2(token),
|
|
26379
|
+
displayName: args.displayName,
|
|
26380
|
+
grants: args.grants,
|
|
26381
|
+
issuedAt: now,
|
|
26382
|
+
usedCount: 0,
|
|
26383
|
+
...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
|
|
26384
|
+
...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
|
|
26385
|
+
};
|
|
26386
|
+
this.registry.upsertCapability(cap);
|
|
26387
|
+
this.hooks.onIssued?.(cap, token);
|
|
26388
|
+
return { token, capability: cap };
|
|
25860
26389
|
}
|
|
25861
26390
|
/**
|
|
25862
|
-
*
|
|
25863
|
-
*
|
|
25864
|
-
*
|
|
25865
|
-
*
|
|
25866
|
-
*
|
|
26391
|
+
* guest 端 auth 帧捎带 selfPrincipalId/selfDisplayName 时,AuthGate.authenticate
|
|
26392
|
+
* 校验 cap token 通过后调本方法把 peer 字段写回 cap:
|
|
26393
|
+
* - 首次(cap.firstUsedByPeerAt 未设)→ 写盘 + 触发 onIssued 复用 push 频道
|
|
26394
|
+
* 让 owner UI 通过 capability:updated 实时拿到 peer 字段
|
|
26395
|
+
* - 重复(同 peerOwnerId)→ noop
|
|
26396
|
+
* - 冲突(不同 peerOwnerId)→ throw TOKEN_SHARED_LEAK,AuthGate 转 close 4401
|
|
25867
26397
|
*/
|
|
25868
|
-
|
|
25869
|
-
const
|
|
25870
|
-
|
|
25871
|
-
if (
|
|
25872
|
-
|
|
25873
|
-
|
|
25874
|
-
|
|
25875
|
-
|
|
26398
|
+
recordPeerHello(capId, args) {
|
|
26399
|
+
const cap = this.registry.findById(capId);
|
|
26400
|
+
if (!cap) return { changed: false };
|
|
26401
|
+
if (cap.firstUsedByPeerAt != null) {
|
|
26402
|
+
if (cap.peerOwnerId !== args.ownerPrincipalId) {
|
|
26403
|
+
throw new Error("TOKEN_SHARED_LEAK");
|
|
26404
|
+
}
|
|
26405
|
+
return { changed: false };
|
|
26406
|
+
}
|
|
26407
|
+
const updated = {
|
|
26408
|
+
...cap,
|
|
26409
|
+
peerOwnerId: args.ownerPrincipalId,
|
|
26410
|
+
firstUsedByPeerAt: args.now,
|
|
26411
|
+
peerDisplayName: args.displayName
|
|
26412
|
+
};
|
|
26413
|
+
this.registry.upsertCapability(updated);
|
|
26414
|
+
this.hooks.onIssued?.(updated, "__peer_hello__");
|
|
26415
|
+
return { changed: true };
|
|
25876
26416
|
}
|
|
25877
26417
|
/**
|
|
25878
|
-
*
|
|
25879
|
-
*
|
|
25880
|
-
*
|
|
25881
|
-
*
|
|
25882
|
-
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
26418
|
+
* Hard delete:从 registry / store 物理移除 capability。
|
|
26419
|
+
* idempotent:不存在 → null(handler 翻译成 VALIDATION_ERROR)
|
|
26420
|
+
* onDeleted hook 在 daemon/index.ts wire 负责 close 连接 + cleanup 文件 + 广播
|
|
25883
26421
|
*/
|
|
25884
|
-
|
|
25885
|
-
const
|
|
25886
|
-
|
|
25887
|
-
|
|
25888
|
-
|
|
25889
|
-
this.writeFile(scope, sessionId, entries);
|
|
25890
|
-
this.cache.set(this.cacheKey(scope, sessionId), { entries, scope });
|
|
25891
|
-
return true;
|
|
26422
|
+
delete(id) {
|
|
26423
|
+
const removed = this.registry.delete(id);
|
|
26424
|
+
if (!removed) return null;
|
|
26425
|
+
this.hooks.onDeleted?.(removed);
|
|
26426
|
+
return { capability: removed };
|
|
25892
26427
|
}
|
|
26428
|
+
};
|
|
26429
|
+
function defaultGenerateToken() {
|
|
26430
|
+
return crypto5.randomBytes(24).toString("base64url");
|
|
26431
|
+
}
|
|
26432
|
+
function defaultGenerateId() {
|
|
26433
|
+
return "cap_" + crypto5.randomBytes(6).toString("base64url");
|
|
26434
|
+
}
|
|
26435
|
+
function sha256Hex2(s) {
|
|
26436
|
+
return crypto5.createHash("sha256").update(s).digest("hex");
|
|
26437
|
+
}
|
|
26438
|
+
|
|
26439
|
+
// src/permission/cleanup.ts
|
|
26440
|
+
var fs16 = __toESM(require("fs"), 1);
|
|
26441
|
+
function cleanupGuestSessionsForCapability(cap, factory) {
|
|
26442
|
+
const removed = [];
|
|
26443
|
+
for (const g2 of cap.grants) {
|
|
26444
|
+
if (g2.resource.type !== "persona") continue;
|
|
26445
|
+
const dir = factory.vmGuestRoot(g2.resource.id, cap.id);
|
|
26446
|
+
try {
|
|
26447
|
+
fs16.rmSync(dir, { recursive: true, force: true });
|
|
26448
|
+
removed.push(dir);
|
|
26449
|
+
} catch {
|
|
26450
|
+
}
|
|
26451
|
+
}
|
|
26452
|
+
return { removed };
|
|
26453
|
+
}
|
|
26454
|
+
|
|
26455
|
+
// src/inbox/inbox-store.ts
|
|
26456
|
+
var fs17 = __toESM(require("fs"), 1);
|
|
26457
|
+
var path19 = __toESM(require("path"), 1);
|
|
26458
|
+
var INBOX_SUBDIR = "inbox";
|
|
26459
|
+
var InboxStore = class {
|
|
26460
|
+
constructor(dataDir) {
|
|
26461
|
+
this.dataDir = dataDir;
|
|
26462
|
+
fs17.mkdirSync(this.dirPath(), { recursive: true });
|
|
26463
|
+
}
|
|
26464
|
+
dataDir;
|
|
25893
26465
|
/**
|
|
25894
|
-
*
|
|
25895
|
-
*
|
|
25896
|
-
*
|
|
25897
|
-
|
|
26466
|
+
* 列出某 peer 的消息, 按 createdAt 升序.
|
|
26467
|
+
* sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
|
|
26468
|
+
* 文件不存在时返回 [] (不抛, 不创建).
|
|
26469
|
+
*/
|
|
26470
|
+
list(peerOwnerId, sinceCreatedAt) {
|
|
26471
|
+
const file = this.filePath(peerOwnerId);
|
|
26472
|
+
let raw;
|
|
26473
|
+
try {
|
|
26474
|
+
raw = fs17.readFileSync(file, "utf8");
|
|
26475
|
+
} catch (err) {
|
|
26476
|
+
if (err?.code === "ENOENT") return [];
|
|
26477
|
+
return [];
|
|
26478
|
+
}
|
|
26479
|
+
const all = parseAllLines(raw);
|
|
26480
|
+
if (sinceCreatedAt === void 0) return all;
|
|
26481
|
+
return all.filter((m2) => m2.createdAt > sinceCreatedAt);
|
|
26482
|
+
}
|
|
26483
|
+
/**
|
|
26484
|
+
* 列出所有 peer 的 peerOwnerId (扫目录所有 *.jsonl 文件名).
|
|
26485
|
+
*/
|
|
26486
|
+
listAllPeerOwnerIds() {
|
|
26487
|
+
const dir = this.dirPath();
|
|
26488
|
+
let entries;
|
|
26489
|
+
try {
|
|
26490
|
+
entries = fs17.readdirSync(dir);
|
|
26491
|
+
} catch (err) {
|
|
26492
|
+
if (err?.code === "ENOENT") return [];
|
|
26493
|
+
return [];
|
|
26494
|
+
}
|
|
26495
|
+
return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
|
|
26496
|
+
}
|
|
26497
|
+
/**
|
|
26498
|
+
* 追加一条消息,按 message.id 幂等去重。
|
|
26499
|
+
* 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
|
|
26500
|
+
*/
|
|
26501
|
+
append(message) {
|
|
26502
|
+
const existing = this.list(message.peerOwnerId);
|
|
26503
|
+
if (existing.some((m2) => m2.id === message.id)) return;
|
|
26504
|
+
const file = this.filePath(message.peerOwnerId);
|
|
26505
|
+
const line = JSON.stringify(message) + "\n";
|
|
26506
|
+
fs17.appendFileSync(file, line, { mode: 384 });
|
|
26507
|
+
try {
|
|
26508
|
+
fs17.chmodSync(file, 384);
|
|
26509
|
+
} catch {
|
|
26510
|
+
}
|
|
26511
|
+
}
|
|
26512
|
+
/**
|
|
26513
|
+
* 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=principalId 的消息
|
|
26514
|
+
* readBy[principalId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
|
|
26515
|
+
* 返写入了几条.
|
|
26516
|
+
*/
|
|
26517
|
+
markRead(peerOwnerId, principalId, upToCreatedAt) {
|
|
26518
|
+
const messages = this.list(peerOwnerId);
|
|
26519
|
+
let changed = 0;
|
|
26520
|
+
const next = messages.map((m2) => {
|
|
26521
|
+
if (m2.senderPrincipalId === principalId) return m2;
|
|
26522
|
+
if (m2.createdAt > upToCreatedAt) return m2;
|
|
26523
|
+
const cur = m2.readBy[principalId];
|
|
26524
|
+
if (cur !== void 0 && cur >= upToCreatedAt) return m2;
|
|
26525
|
+
changed++;
|
|
26526
|
+
return { ...m2, readBy: { ...m2.readBy, [principalId]: upToCreatedAt } };
|
|
26527
|
+
});
|
|
26528
|
+
if (changed === 0) return 0;
|
|
26529
|
+
this.rewriteFile(peerOwnerId, next);
|
|
26530
|
+
return changed;
|
|
26531
|
+
}
|
|
26532
|
+
/**
|
|
26533
|
+
* 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
|
|
26534
|
+
*/
|
|
26535
|
+
removeByPeerOwnerId(peerOwnerId) {
|
|
26536
|
+
const file = this.filePath(peerOwnerId);
|
|
26537
|
+
try {
|
|
26538
|
+
fs17.unlinkSync(file);
|
|
26539
|
+
} catch (err) {
|
|
26540
|
+
if (err?.code === "ENOENT") return;
|
|
26541
|
+
}
|
|
26542
|
+
}
|
|
26543
|
+
rewriteFile(peerOwnerId, messages) {
|
|
26544
|
+
const file = this.filePath(peerOwnerId);
|
|
26545
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
26546
|
+
const content = messages.map((m2) => JSON.stringify(m2)).join("\n") + (messages.length > 0 ? "\n" : "");
|
|
26547
|
+
fs17.writeFileSync(tmp, content, { mode: 384 });
|
|
26548
|
+
fs17.renameSync(tmp, file);
|
|
26549
|
+
try {
|
|
26550
|
+
fs17.chmodSync(file, 384);
|
|
26551
|
+
} catch {
|
|
26552
|
+
}
|
|
26553
|
+
}
|
|
26554
|
+
dirPath() {
|
|
26555
|
+
return path19.join(this.dataDir, INBOX_SUBDIR);
|
|
26556
|
+
}
|
|
26557
|
+
filePath(peerOwnerId) {
|
|
26558
|
+
return path19.join(this.dirPath(), `${peerOwnerId}.jsonl`);
|
|
26559
|
+
}
|
|
26560
|
+
};
|
|
26561
|
+
function parseAllLines(raw) {
|
|
26562
|
+
const out = [];
|
|
26563
|
+
for (const line of raw.split("\n")) {
|
|
26564
|
+
const trimmed = line.trim();
|
|
26565
|
+
if (!trimmed) continue;
|
|
26566
|
+
let parsed;
|
|
26567
|
+
try {
|
|
26568
|
+
parsed = JSON.parse(trimmed);
|
|
26569
|
+
} catch {
|
|
26570
|
+
continue;
|
|
26571
|
+
}
|
|
26572
|
+
const r = InboxMessageSchema.safeParse(parsed);
|
|
26573
|
+
if (r.success) out.push(r.data);
|
|
26574
|
+
}
|
|
26575
|
+
return out;
|
|
26576
|
+
}
|
|
26577
|
+
|
|
26578
|
+
// src/inbox/inbox-manager.ts
|
|
26579
|
+
var InboxManager = class {
|
|
26580
|
+
constructor(store, broadcast) {
|
|
26581
|
+
this.store = store;
|
|
26582
|
+
this.broadcast = broadcast;
|
|
26583
|
+
}
|
|
26584
|
+
store;
|
|
26585
|
+
broadcast;
|
|
26586
|
+
/**
|
|
26587
|
+
* 写一条消息到 peer.jsonl + broadcast 给本机所有 owner ws。
|
|
26588
|
+
* 按 id 幂等:同 id 已落盘则 no-op(不广播、不重写)。
|
|
25898
26589
|
*
|
|
25899
|
-
*
|
|
26590
|
+
* caller 必须已通过 handler 鉴权
|
|
26591
|
+
* (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
|
|
25900
26592
|
*/
|
|
25901
|
-
|
|
25902
|
-
const
|
|
25903
|
-
|
|
25904
|
-
|
|
25905
|
-
|
|
25906
|
-
|
|
26593
|
+
postMessage(args) {
|
|
26594
|
+
const beforeLen = this.store.list(args.peerOwnerId).length;
|
|
26595
|
+
const msg = {
|
|
26596
|
+
id: args.id,
|
|
26597
|
+
peerOwnerId: args.peerOwnerId,
|
|
26598
|
+
senderPrincipalId: args.senderPrincipalId,
|
|
26599
|
+
text: args.text,
|
|
26600
|
+
createdAt: args.createdAt,
|
|
26601
|
+
readBy: {}
|
|
26602
|
+
};
|
|
26603
|
+
this.store.append(msg);
|
|
26604
|
+
const afterLen = this.store.list(args.peerOwnerId).length;
|
|
26605
|
+
if (afterLen === beforeLen) return msg;
|
|
26606
|
+
this.broadcast(args.peerOwnerId, {
|
|
26607
|
+
type: "inbox:event",
|
|
26608
|
+
peerOwnerId: args.peerOwnerId,
|
|
26609
|
+
message: msg
|
|
26610
|
+
});
|
|
26611
|
+
return msg;
|
|
26612
|
+
}
|
|
26613
|
+
list(peerOwnerId, sinceCreatedAt) {
|
|
26614
|
+
return this.store.list(peerOwnerId, sinceCreatedAt);
|
|
26615
|
+
}
|
|
26616
|
+
/**
|
|
26617
|
+
* 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
|
|
26618
|
+
* broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
|
|
26619
|
+
*/
|
|
26620
|
+
markRead(args) {
|
|
26621
|
+
const before = this.store.list(args.peerOwnerId);
|
|
26622
|
+
const changed = this.store.markRead(args.peerOwnerId, args.principalId, args.upToCreatedAt);
|
|
26623
|
+
if (changed === 0) return 0;
|
|
26624
|
+
const after = this.store.list(args.peerOwnerId);
|
|
26625
|
+
const beforeById = new Map(before.map((m2) => [m2.id, m2]));
|
|
26626
|
+
for (const m2 of after) {
|
|
26627
|
+
const prev = beforeById.get(m2.id);
|
|
26628
|
+
if (!prev) continue;
|
|
26629
|
+
if (prev.readBy[args.principalId] === m2.readBy[args.principalId]) continue;
|
|
26630
|
+
this.broadcast(args.peerOwnerId, {
|
|
26631
|
+
type: "inbox:event",
|
|
26632
|
+
peerOwnerId: args.peerOwnerId,
|
|
26633
|
+
message: m2
|
|
26634
|
+
});
|
|
26635
|
+
}
|
|
26636
|
+
return changed;
|
|
26637
|
+
}
|
|
26638
|
+
/** peer:remove cascade. 删整个 peer 的 jsonl. */
|
|
26639
|
+
removeChannel(peerOwnerId) {
|
|
26640
|
+
this.store.removeByPeerOwnerId(peerOwnerId);
|
|
26641
|
+
}
|
|
26642
|
+
};
|
|
26643
|
+
|
|
26644
|
+
// src/state/received-capability-store.ts
|
|
26645
|
+
var fs18 = __toESM(require("fs"), 1);
|
|
26646
|
+
var path20 = __toESM(require("path"), 1);
|
|
26647
|
+
var FILE_NAME = "received-capabilities.json";
|
|
26648
|
+
var ReceivedCapabilityStore = class {
|
|
26649
|
+
constructor(dataDir) {
|
|
26650
|
+
this.dataDir = dataDir;
|
|
26651
|
+
}
|
|
26652
|
+
dataDir;
|
|
26653
|
+
caps = /* @__PURE__ */ new Map();
|
|
26654
|
+
load() {
|
|
26655
|
+
this.caps.clear();
|
|
26656
|
+
const file = path20.join(this.dataDir, FILE_NAME);
|
|
26657
|
+
let raw;
|
|
26658
|
+
try {
|
|
26659
|
+
raw = fs18.readFileSync(file, "utf8");
|
|
26660
|
+
} catch (err) {
|
|
26661
|
+
if (err?.code !== "ENOENT") this.renameBak(file);
|
|
26662
|
+
return;
|
|
26663
|
+
}
|
|
26664
|
+
let parsed;
|
|
26665
|
+
try {
|
|
26666
|
+
parsed = JSON.parse(raw);
|
|
26667
|
+
} catch {
|
|
26668
|
+
this.renameBak(file);
|
|
26669
|
+
return;
|
|
26670
|
+
}
|
|
26671
|
+
const arr = parsed?.receivedCaps;
|
|
26672
|
+
if (!Array.isArray(arr)) return;
|
|
26673
|
+
for (const entry of arr) {
|
|
26674
|
+
const r = ReceivedCapabilitySchema.safeParse(entry);
|
|
26675
|
+
if (r.success) this.caps.set(r.data.ownerPrincipalId, r.data);
|
|
26676
|
+
}
|
|
26677
|
+
}
|
|
26678
|
+
list() {
|
|
26679
|
+
return Array.from(this.caps.values());
|
|
26680
|
+
}
|
|
26681
|
+
get(ownerPrincipalId) {
|
|
26682
|
+
return this.caps.get(ownerPrincipalId) ?? null;
|
|
26683
|
+
}
|
|
26684
|
+
hasOwner(ownerPrincipalId) {
|
|
26685
|
+
return this.caps.has(ownerPrincipalId);
|
|
26686
|
+
}
|
|
26687
|
+
upsert(cap) {
|
|
26688
|
+
this.caps.set(cap.ownerPrincipalId, cap);
|
|
26689
|
+
this.flush();
|
|
26690
|
+
}
|
|
26691
|
+
remove(ownerPrincipalId) {
|
|
26692
|
+
if (!this.caps.delete(ownerPrincipalId)) return;
|
|
26693
|
+
this.flush();
|
|
26694
|
+
}
|
|
26695
|
+
flush() {
|
|
26696
|
+
const file = path20.join(this.dataDir, FILE_NAME);
|
|
26697
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
26698
|
+
const content = JSON.stringify(
|
|
26699
|
+
{ receivedCaps: Array.from(this.caps.values()) },
|
|
26700
|
+
null,
|
|
26701
|
+
2
|
|
26702
|
+
);
|
|
26703
|
+
fs18.mkdirSync(this.dataDir, { recursive: true });
|
|
26704
|
+
fs18.writeFileSync(tmp, content, { mode: 384 });
|
|
26705
|
+
fs18.renameSync(tmp, file);
|
|
26706
|
+
}
|
|
26707
|
+
renameBak(file) {
|
|
26708
|
+
try {
|
|
26709
|
+
fs18.renameSync(file, `${file}.bak`);
|
|
26710
|
+
} catch {
|
|
26711
|
+
}
|
|
26712
|
+
}
|
|
26713
|
+
};
|
|
26714
|
+
|
|
26715
|
+
// src/state/legacy-migration.ts
|
|
26716
|
+
var fs19 = __toESM(require("fs"), 1);
|
|
26717
|
+
var path21 = __toESM(require("path"), 1);
|
|
26718
|
+
var TARGETS = [
|
|
26719
|
+
{ src: "capabilities.json", bak: "capabilities.legacy.bak" },
|
|
26720
|
+
{ src: "remote-personas", bak: "remote-personas.legacy.bak" },
|
|
26721
|
+
{ src: "peers.json", bak: "peers.legacy.bak" }
|
|
26722
|
+
];
|
|
26723
|
+
function migrateLegacyFiles(dataDir) {
|
|
26724
|
+
for (const t of TARGETS) {
|
|
26725
|
+
const srcPath = path21.join(dataDir, t.src);
|
|
26726
|
+
const bakPath = path21.join(dataDir, t.bak);
|
|
26727
|
+
if (!fs19.existsSync(srcPath)) continue;
|
|
26728
|
+
if (fs19.existsSync(bakPath)) continue;
|
|
26729
|
+
try {
|
|
26730
|
+
fs19.renameSync(srcPath, bakPath);
|
|
26731
|
+
} catch {
|
|
26732
|
+
}
|
|
26733
|
+
}
|
|
26734
|
+
}
|
|
26735
|
+
|
|
26736
|
+
// src/received-capability/connect-remote.ts
|
|
26737
|
+
var crypto6 = __toESM(require("crypto"), 1);
|
|
26738
|
+
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
26739
|
+
var RPC_TIMEOUT_MS = 5e3;
|
|
26740
|
+
async function connectRemote(args) {
|
|
26741
|
+
const ws = new wrapper_default(args.url);
|
|
26742
|
+
await new Promise((resolve2, reject) => {
|
|
26743
|
+
const onError = (e) => reject(e);
|
|
26744
|
+
ws.once("open", () => {
|
|
26745
|
+
ws.off("error", onError);
|
|
26746
|
+
resolve2();
|
|
26747
|
+
});
|
|
26748
|
+
ws.once("error", onError);
|
|
26749
|
+
});
|
|
26750
|
+
ws.send(
|
|
26751
|
+
JSON.stringify({
|
|
26752
|
+
type: "auth",
|
|
26753
|
+
token: args.token,
|
|
26754
|
+
selfPrincipalId: args.selfPrincipalId,
|
|
26755
|
+
selfDisplayName: args.selfDisplayName
|
|
26756
|
+
})
|
|
26757
|
+
);
|
|
26758
|
+
await new Promise((resolve2, reject) => {
|
|
26759
|
+
const onMessage = (raw) => {
|
|
26760
|
+
let f;
|
|
25907
26761
|
try {
|
|
25908
|
-
|
|
25909
|
-
} catch
|
|
25910
|
-
|
|
25911
|
-
|
|
25912
|
-
|
|
26762
|
+
f = JSON.parse(raw.toString());
|
|
26763
|
+
} catch {
|
|
26764
|
+
return;
|
|
26765
|
+
}
|
|
26766
|
+
if (f.type === "auth:ok" || f.type === "ready") {
|
|
26767
|
+
ws.off("message", onMessage);
|
|
26768
|
+
resolve2();
|
|
26769
|
+
return;
|
|
25913
26770
|
}
|
|
25914
|
-
|
|
25915
|
-
|
|
25916
|
-
|
|
25917
|
-
|
|
25918
|
-
|
|
25919
|
-
|
|
26771
|
+
if (f.type === "auth:error" || f.code === 4401) {
|
|
26772
|
+
ws.off("message", onMessage);
|
|
26773
|
+
reject(new Error(`auth failed: ${f.message ?? f.type ?? "unknown"}`));
|
|
26774
|
+
}
|
|
26775
|
+
};
|
|
26776
|
+
ws.on("message", onMessage);
|
|
26777
|
+
setTimeout(() => {
|
|
26778
|
+
ws.off("message", onMessage);
|
|
26779
|
+
reject(new Error("auth handshake timeout"));
|
|
26780
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
26781
|
+
});
|
|
26782
|
+
function call(method, payload) {
|
|
26783
|
+
const requestId = crypto6.randomUUID();
|
|
26784
|
+
return new Promise((resolve2, reject) => {
|
|
26785
|
+
const onMessage = (raw) => {
|
|
26786
|
+
let f;
|
|
26787
|
+
try {
|
|
26788
|
+
f = JSON.parse(raw.toString());
|
|
26789
|
+
} catch {
|
|
26790
|
+
return;
|
|
26791
|
+
}
|
|
26792
|
+
if (f.requestId !== requestId) return;
|
|
26793
|
+
ws.off("message", onMessage);
|
|
26794
|
+
if (f.error || f.type?.endsWith(":error")) {
|
|
26795
|
+
reject(new Error(f.message ?? f.error ?? "rpc error"));
|
|
26796
|
+
} else {
|
|
26797
|
+
resolve2(f);
|
|
26798
|
+
}
|
|
26799
|
+
};
|
|
26800
|
+
ws.on("message", onMessage);
|
|
26801
|
+
ws.send(JSON.stringify({ type: method, requestId, ...payload }));
|
|
26802
|
+
setTimeout(() => {
|
|
26803
|
+
ws.off("message", onMessage);
|
|
26804
|
+
reject(new Error(`${method} timeout`));
|
|
26805
|
+
}, RPC_TIMEOUT_MS);
|
|
26806
|
+
});
|
|
26807
|
+
}
|
|
26808
|
+
return {
|
|
26809
|
+
whoami: async () => {
|
|
26810
|
+
const f = await call("whoami", {});
|
|
26811
|
+
return {
|
|
26812
|
+
ownerId: f.owner.id,
|
|
26813
|
+
displayName: f.owner.displayName,
|
|
26814
|
+
capabilityId: f.capability.id,
|
|
26815
|
+
grants: f.capability.grants
|
|
26816
|
+
};
|
|
26817
|
+
},
|
|
26818
|
+
close: () => {
|
|
26819
|
+
try {
|
|
26820
|
+
ws.close();
|
|
26821
|
+
} catch {
|
|
25920
26822
|
}
|
|
25921
26823
|
}
|
|
25922
|
-
|
|
26824
|
+
};
|
|
26825
|
+
}
|
|
26826
|
+
|
|
26827
|
+
// src/migrations/2026-05-20-flatten-sessions.ts
|
|
26828
|
+
var fs20 = __toESM(require("fs"), 1);
|
|
26829
|
+
var path22 = __toESM(require("path"), 1);
|
|
26830
|
+
var MIGRATION_FLAG_NAME = ".migration.v1.done";
|
|
26831
|
+
function migrateFlattenSessions(opts) {
|
|
26832
|
+
const dataDir = opts.dataDir;
|
|
26833
|
+
const now = opts.now ?? Date.now;
|
|
26834
|
+
const sessionsDir = path22.join(dataDir, "sessions");
|
|
26835
|
+
const flagPath = path22.join(sessionsDir, MIGRATION_FLAG_NAME);
|
|
26836
|
+
if (existsSync4(flagPath)) {
|
|
26837
|
+
return { skipped: true, flagWritten: false, movedBare: 0, movedVmOwner: 0, archivedListener: 0 };
|
|
26838
|
+
}
|
|
26839
|
+
let movedBare = 0;
|
|
26840
|
+
let movedVmOwner = 0;
|
|
26841
|
+
let archivedListener = 0;
|
|
26842
|
+
const defaultDir = path22.join(sessionsDir, "default");
|
|
26843
|
+
if (existsSync4(defaultDir)) {
|
|
26844
|
+
for (const entry of readdirSafe(defaultDir)) {
|
|
26845
|
+
if (!entry.endsWith(".json")) continue;
|
|
26846
|
+
const src = path22.join(defaultDir, entry);
|
|
26847
|
+
const dst = path22.join(sessionsDir, entry);
|
|
26848
|
+
fs20.renameSync(src, dst);
|
|
26849
|
+
movedBare += 1;
|
|
26850
|
+
}
|
|
26851
|
+
rmdirIfEmpty(defaultDir);
|
|
26852
|
+
}
|
|
26853
|
+
for (const pid of readdirSafe(sessionsDir)) {
|
|
26854
|
+
const personaDir = path22.join(sessionsDir, pid);
|
|
26855
|
+
if (!isDir(personaDir)) continue;
|
|
26856
|
+
if (pid === "default") continue;
|
|
26857
|
+
const ownerSrc = path22.join(personaDir, "owner");
|
|
26858
|
+
if (existsSync4(ownerSrc) && isDir(ownerSrc)) {
|
|
26859
|
+
const ownerDst = path22.join(dataDir, "personas", pid, ".clawd", "sessions", "owner");
|
|
26860
|
+
fs20.mkdirSync(ownerDst, { recursive: true });
|
|
26861
|
+
for (const file of readdirSafe(ownerSrc)) {
|
|
26862
|
+
if (!file.endsWith(".json")) continue;
|
|
26863
|
+
fs20.renameSync(path22.join(ownerSrc, file), path22.join(ownerDst, file));
|
|
26864
|
+
movedVmOwner += 1;
|
|
26865
|
+
}
|
|
26866
|
+
rmdirIfEmpty(ownerSrc);
|
|
26867
|
+
}
|
|
26868
|
+
const listenerSrc = path22.join(personaDir, "listener");
|
|
26869
|
+
if (existsSync4(listenerSrc) && isDir(listenerSrc)) {
|
|
26870
|
+
const archiveDst = path22.join(dataDir, ".legacy", `listener-${pid}`);
|
|
26871
|
+
fs20.mkdirSync(archiveDst, { recursive: true });
|
|
26872
|
+
for (const file of readdirSafe(listenerSrc)) {
|
|
26873
|
+
if (!file.endsWith(".json")) continue;
|
|
26874
|
+
fs20.renameSync(path22.join(listenerSrc, file), path22.join(archiveDst, file));
|
|
26875
|
+
archivedListener += 1;
|
|
26876
|
+
}
|
|
26877
|
+
rmdirIfEmpty(listenerSrc);
|
|
26878
|
+
}
|
|
26879
|
+
rmdirIfEmpty(personaDir);
|
|
26880
|
+
}
|
|
26881
|
+
fs20.mkdirSync(sessionsDir, { recursive: true });
|
|
26882
|
+
fs20.writeFileSync(flagPath, JSON.stringify({ migratedAt: now() }, null, 2));
|
|
26883
|
+
return {
|
|
26884
|
+
skipped: false,
|
|
26885
|
+
flagWritten: true,
|
|
26886
|
+
movedBare,
|
|
26887
|
+
movedVmOwner,
|
|
26888
|
+
archivedListener
|
|
26889
|
+
};
|
|
26890
|
+
}
|
|
26891
|
+
function existsSync4(p2) {
|
|
26892
|
+
try {
|
|
26893
|
+
fs20.statSync(p2);
|
|
26894
|
+
return true;
|
|
26895
|
+
} catch {
|
|
26896
|
+
return false;
|
|
25923
26897
|
}
|
|
25924
|
-
}
|
|
25925
|
-
function
|
|
25926
|
-
|
|
25927
|
-
|
|
26898
|
+
}
|
|
26899
|
+
function isDir(p2) {
|
|
26900
|
+
try {
|
|
26901
|
+
return fs20.statSync(p2).isDirectory();
|
|
26902
|
+
} catch {
|
|
25928
26903
|
return false;
|
|
25929
26904
|
}
|
|
25930
|
-
|
|
25931
|
-
|
|
25932
|
-
|
|
25933
|
-
|
|
25934
|
-
|
|
25935
|
-
|
|
25936
|
-
}
|
|
26905
|
+
}
|
|
26906
|
+
function readdirSafe(p2) {
|
|
26907
|
+
try {
|
|
26908
|
+
return fs20.readdirSync(p2);
|
|
26909
|
+
} catch {
|
|
26910
|
+
return [];
|
|
25937
26911
|
}
|
|
25938
|
-
return false;
|
|
25939
26912
|
}
|
|
25940
|
-
function
|
|
26913
|
+
function rmdirIfEmpty(p2) {
|
|
25941
26914
|
try {
|
|
25942
|
-
|
|
26915
|
+
fs20.rmdirSync(p2);
|
|
25943
26916
|
} catch {
|
|
25944
|
-
return null;
|
|
25945
26917
|
}
|
|
25946
26918
|
}
|
|
25947
26919
|
|
|
25948
|
-
// src/
|
|
26920
|
+
// src/transport/http-router.ts
|
|
26921
|
+
var import_node_fs13 = __toESM(require("fs"), 1);
|
|
25949
26922
|
var import_node_path15 = __toESM(require("path"), 1);
|
|
26923
|
+
|
|
26924
|
+
// src/attachment/mime.ts
|
|
26925
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
25950
26926
|
var TEXT_PLAIN = "text/plain; charset=utf-8";
|
|
25951
26927
|
var EXT_TO_NATIVE_MIME = {
|
|
25952
26928
|
// 图片
|
|
@@ -26053,14 +27029,14 @@ var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
26053
27029
|
".mk"
|
|
26054
27030
|
]);
|
|
26055
27031
|
function lookupMime(filePathOrName) {
|
|
26056
|
-
const ext =
|
|
27032
|
+
const ext = import_node_path14.default.extname(filePathOrName).toLowerCase();
|
|
26057
27033
|
if (EXT_TO_NATIVE_MIME[ext]) return EXT_TO_NATIVE_MIME[ext];
|
|
26058
27034
|
if (TEXT_EXTENSIONS.has(ext)) return TEXT_PLAIN;
|
|
26059
27035
|
return "application/octet-stream";
|
|
26060
27036
|
}
|
|
26061
27037
|
|
|
26062
27038
|
// src/attachment/sign-url.ts
|
|
26063
|
-
var
|
|
27039
|
+
var import_node_crypto4 = __toESM(require("crypto"), 1);
|
|
26064
27040
|
var HMAC_ALGO = "sha256";
|
|
26065
27041
|
function base64urlEncode(buf) {
|
|
26066
27042
|
const b2 = typeof buf === "string" ? Buffer.from(buf, "utf8") : buf;
|
|
@@ -26077,7 +27053,7 @@ function decodeAbsPathFromUrl(encoded) {
|
|
|
26077
27053
|
}
|
|
26078
27054
|
function computeSig(secret, absPath, e) {
|
|
26079
27055
|
const msg = e === null ? absPath : `${absPath}|${e}`;
|
|
26080
|
-
return
|
|
27056
|
+
return import_node_crypto4.default.createHmac(HMAC_ALGO, secret).update(msg).digest();
|
|
26081
27057
|
}
|
|
26082
27058
|
function signUrlParts(secret, absPath, ttlSeconds, now = Date.now) {
|
|
26083
27059
|
const e = ttlSeconds === null ? null : Math.floor(now() / 1e3) + ttlSeconds;
|
|
@@ -26112,7 +27088,7 @@ function verifySignedUrl(secret, absPath, eRaw, s, now = Date.now) {
|
|
|
26112
27088
|
if (provided.length !== expected.length) {
|
|
26113
27089
|
return { ok: false, code: "BAD_SIG" };
|
|
26114
27090
|
}
|
|
26115
|
-
if (!
|
|
27091
|
+
if (!import_node_crypto4.default.timingSafeEqual(provided, expected)) {
|
|
26116
27092
|
return { ok: false, code: "BAD_SIG" };
|
|
26117
27093
|
}
|
|
26118
27094
|
if (e !== null && now() / 1e3 > e) {
|
|
@@ -26133,7 +27109,7 @@ function createHttpRouter(deps) {
|
|
|
26133
27109
|
sendJson(res, 200, { ok: true, version: deps.daemonVersion });
|
|
26134
27110
|
return true;
|
|
26135
27111
|
}
|
|
26136
|
-
if (!url.pathname.startsWith("/
|
|
27112
|
+
if (!url.pathname.startsWith("/session/") && !url.pathname.startsWith("/files/")) {
|
|
26137
27113
|
return false;
|
|
26138
27114
|
}
|
|
26139
27115
|
if (url.pathname.startsWith("/files/") && req.method === "GET") {
|
|
@@ -26173,43 +27149,8 @@ function createHttpRouter(deps) {
|
|
|
26173
27149
|
sendJson(res, 401, { code: "UNAUTHORIZED", message: "missing or invalid bearer token" });
|
|
26174
27150
|
return true;
|
|
26175
27151
|
}
|
|
26176
|
-
const personaFilesMatch = url.pathname.match(/^\/persona\/([^/]+)\/files$/);
|
|
26177
|
-
if (personaFilesMatch && req.method === "GET") {
|
|
26178
|
-
const pid = personaFilesMatch[1];
|
|
26179
|
-
const pathParam = url.searchParams.get("path");
|
|
26180
|
-
if (!pathParam) {
|
|
26181
|
-
sendJson(res, 400, { code: "INVALID_PARAM", message: "missing `path` query" });
|
|
26182
|
-
return true;
|
|
26183
|
-
}
|
|
26184
|
-
if (!deps.personaStore || !deps.groupFileStore) {
|
|
26185
|
-
sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "files endpoint not wired" }));
|
|
26186
|
-
return true;
|
|
26187
|
-
}
|
|
26188
|
-
const personaDir = deps.personaStore.personaDirPath(pid);
|
|
26189
|
-
const absPath = import_node_path16.default.isAbsolute(pathParam) ? pathParam : import_node_path16.default.join(personaDir, pathParam);
|
|
26190
|
-
if (!import_node_path16.default.isAbsolute(pathParam) && !isContainedIn(absPath, personaDir)) {
|
|
26191
|
-
sendJson(res, 400, { code: "PATH_TRAVERSAL", message: "rel path escapes personaDir" });
|
|
26192
|
-
return true;
|
|
26193
|
-
}
|
|
26194
|
-
if (ctx.role === "personal") {
|
|
26195
|
-
if (ctx.personaId !== pid) {
|
|
26196
|
-
sendJson(res, 403, { code: "FORBIDDEN", message: "personal token bound to other persona" });
|
|
26197
|
-
return true;
|
|
26198
|
-
}
|
|
26199
|
-
if (!personalViewable(deps.groupFileStore, personaDir, pid, absPath)) {
|
|
26200
|
-
sendJson(res, 403, { code: "FORBIDDEN", message: "path not in personal viewable scope" });
|
|
26201
|
-
return true;
|
|
26202
|
-
}
|
|
26203
|
-
}
|
|
26204
|
-
streamFile(res, absPath, deps.logger);
|
|
26205
|
-
return true;
|
|
26206
|
-
}
|
|
26207
27152
|
const sessionFilesMatch = url.pathname.match(/^\/session\/([^/]+)\/files$/);
|
|
26208
27153
|
if (sessionFilesMatch && req.method === "GET") {
|
|
26209
|
-
if (ctx.role !== "owner") {
|
|
26210
|
-
sendJson(res, 403, { code: "FORBIDDEN", message: "direct session files are owner-only" });
|
|
26211
|
-
return true;
|
|
26212
|
-
}
|
|
26213
27154
|
const sid = sessionFilesMatch[1];
|
|
26214
27155
|
const pathParam = url.searchParams.get("path");
|
|
26215
27156
|
if (!pathParam) {
|
|
@@ -26217,7 +27158,7 @@ function createHttpRouter(deps) {
|
|
|
26217
27158
|
return true;
|
|
26218
27159
|
}
|
|
26219
27160
|
let absPath;
|
|
26220
|
-
if (
|
|
27161
|
+
if (import_node_path15.default.isAbsolute(pathParam)) {
|
|
26221
27162
|
absPath = pathParam;
|
|
26222
27163
|
} else if (deps.sessionStore) {
|
|
26223
27164
|
const file = deps.sessionStore.read(sid);
|
|
@@ -26225,7 +27166,7 @@ function createHttpRouter(deps) {
|
|
|
26225
27166
|
sendJson(res, 404, { code: "NOT_FOUND", message: `session ${sid} not found` });
|
|
26226
27167
|
return true;
|
|
26227
27168
|
}
|
|
26228
|
-
absPath =
|
|
27169
|
+
absPath = import_node_path15.default.join(file.cwd, pathParam);
|
|
26229
27170
|
} else {
|
|
26230
27171
|
sendJson(res, 501, withCtx(ctx, { code: "NOT_IMPLEMENTED", message: "sessionStore not wired" }));
|
|
26231
27172
|
return true;
|
|
@@ -26250,18 +27191,12 @@ function sendJson(res, status, body) {
|
|
|
26250
27191
|
res.end(JSON.stringify(body));
|
|
26251
27192
|
}
|
|
26252
27193
|
function withCtx(ctx, body) {
|
|
26253
|
-
return { ...body, role: ctx.role
|
|
26254
|
-
}
|
|
26255
|
-
function isContainedIn(abs, root) {
|
|
26256
|
-
const normalized = import_node_path16.default.resolve(abs);
|
|
26257
|
-
const normalizedRoot = import_node_path16.default.resolve(root);
|
|
26258
|
-
if (normalized === normalizedRoot) return true;
|
|
26259
|
-
return normalized.startsWith(normalizedRoot + import_node_path16.default.sep);
|
|
27194
|
+
return { ...body, role: ctx.role };
|
|
26260
27195
|
}
|
|
26261
27196
|
function streamFile(res, absPath, logger) {
|
|
26262
27197
|
let stat;
|
|
26263
27198
|
try {
|
|
26264
|
-
stat =
|
|
27199
|
+
stat = import_node_fs13.default.statSync(absPath);
|
|
26265
27200
|
} catch (err) {
|
|
26266
27201
|
const code = err?.code;
|
|
26267
27202
|
if (code === "ENOENT") {
|
|
@@ -26271,26 +27206,167 @@ function streamFile(res, absPath, logger) {
|
|
|
26271
27206
|
}
|
|
26272
27207
|
return;
|
|
26273
27208
|
}
|
|
26274
|
-
if (!stat.isFile()) {
|
|
26275
|
-
sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
|
|
26276
|
-
return;
|
|
27209
|
+
if (!stat.isFile()) {
|
|
27210
|
+
sendJson(res, 400, { code: "NOT_A_FILE", message: "path is not a regular file" });
|
|
27211
|
+
return;
|
|
27212
|
+
}
|
|
27213
|
+
const mime = lookupMime(absPath);
|
|
27214
|
+
const basename = import_node_path15.default.basename(absPath);
|
|
27215
|
+
res.writeHead(200, {
|
|
27216
|
+
"Content-Type": mime,
|
|
27217
|
+
"Content-Length": String(stat.size),
|
|
27218
|
+
"Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
|
|
27219
|
+
// 防止浏览器把任意 mime 当 html 渲染
|
|
27220
|
+
"X-Content-Type-Options": "nosniff"
|
|
27221
|
+
});
|
|
27222
|
+
const stream = import_node_fs13.default.createReadStream(absPath);
|
|
27223
|
+
stream.on("error", (err) => {
|
|
27224
|
+
logger?.warn("streamFile read error", { absPath, err: err.message });
|
|
27225
|
+
res.destroy();
|
|
27226
|
+
});
|
|
27227
|
+
stream.pipe(res);
|
|
27228
|
+
}
|
|
27229
|
+
|
|
27230
|
+
// src/attachment/group.ts
|
|
27231
|
+
var import_node_fs14 = __toESM(require("fs"), 1);
|
|
27232
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
27233
|
+
var import_node_crypto5 = __toESM(require("crypto"), 1);
|
|
27234
|
+
init_protocol();
|
|
27235
|
+
var GroupFileStore = class {
|
|
27236
|
+
dataDir;
|
|
27237
|
+
logger;
|
|
27238
|
+
cache = /* @__PURE__ */ new Map();
|
|
27239
|
+
constructor(opts) {
|
|
27240
|
+
this.dataDir = opts.dataDir;
|
|
27241
|
+
this.logger = opts.logger;
|
|
27242
|
+
}
|
|
27243
|
+
rootForScope(scope) {
|
|
27244
|
+
return import_node_path16.default.join(this.dataDir, "sessions", ...scopeSubPath(scope).map(safeFileName));
|
|
27245
|
+
}
|
|
27246
|
+
/** 与 SessionStore.filePath 平级,扩展名 .group-files.json */
|
|
27247
|
+
filePath(scope, sessionId) {
|
|
27248
|
+
return import_node_path16.default.join(this.rootForScope(scope), `${safeFileName(sessionId)}.group-files.json`);
|
|
27249
|
+
}
|
|
27250
|
+
cacheKey(scope, sessionId) {
|
|
27251
|
+
return scope.kind === "default" ? `default::${sessionId}` : `persona:${scope.personaId}:${scope.mode}::${sessionId}`;
|
|
27252
|
+
}
|
|
27253
|
+
/** 从磁盘读一份;不存在 → 空数组;schema 不匹配的条目 → 跳过(防腐) */
|
|
27254
|
+
readFile(scope, sessionId) {
|
|
27255
|
+
const file = this.filePath(scope, sessionId);
|
|
27256
|
+
try {
|
|
27257
|
+
const raw = import_node_fs14.default.readFileSync(file, "utf8");
|
|
27258
|
+
const parsed = JSON.parse(raw);
|
|
27259
|
+
if (!Array.isArray(parsed)) {
|
|
27260
|
+
this.logger?.warn("GroupFileStore.readFile: not an array; resetting session entries", {
|
|
27261
|
+
file
|
|
27262
|
+
});
|
|
27263
|
+
return [];
|
|
27264
|
+
}
|
|
27265
|
+
const out = [];
|
|
27266
|
+
for (const entry of parsed) {
|
|
27267
|
+
const r = GroupFileEntrySchema.safeParse(entry);
|
|
27268
|
+
if (r.success) out.push(r.data);
|
|
27269
|
+
}
|
|
27270
|
+
return out;
|
|
27271
|
+
} catch (err) {
|
|
27272
|
+
const code = err?.code;
|
|
27273
|
+
if (code === "ENOENT") return [];
|
|
27274
|
+
this.logger?.warn("GroupFileStore.readFile failed", {
|
|
27275
|
+
file,
|
|
27276
|
+
err: err.message
|
|
27277
|
+
});
|
|
27278
|
+
return [];
|
|
27279
|
+
}
|
|
27280
|
+
}
|
|
27281
|
+
writeFile(scope, sessionId, entries) {
|
|
27282
|
+
const file = this.filePath(scope, sessionId);
|
|
27283
|
+
import_node_fs14.default.mkdirSync(import_node_path16.default.dirname(file), { recursive: true });
|
|
27284
|
+
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
27285
|
+
import_node_fs14.default.writeFileSync(tmp, JSON.stringify(entries, null, 2), { mode: 384 });
|
|
27286
|
+
import_node_fs14.default.renameSync(tmp, file);
|
|
27287
|
+
}
|
|
27288
|
+
/** 拉一份当前 session 的清单。读盘 → cache;之后调用复用 cache */
|
|
27289
|
+
list(scope, sessionId) {
|
|
27290
|
+
const key = this.cacheKey(scope, sessionId);
|
|
27291
|
+
const cached = this.cache.get(key);
|
|
27292
|
+
if (cached) return cached.entries;
|
|
27293
|
+
const entries = this.readFile(scope, sessionId);
|
|
27294
|
+
this.cache.set(key, { entries });
|
|
27295
|
+
return entries;
|
|
27296
|
+
}
|
|
27297
|
+
/**
|
|
27298
|
+
* upsert:
|
|
27299
|
+
* - 同 relPath 已存在 → 更新 lastEditedAt + clear stale;不动 from / addedAt / id
|
|
27300
|
+
* (保留首次入群人 / 入群时刻)
|
|
27301
|
+
* - 不存在 → append,id 派生稳定 uuid,addedAt = now
|
|
27302
|
+
*
|
|
27303
|
+
* 返回最新 entry(caller 可用来 broadcast 通知)。
|
|
27304
|
+
*/
|
|
27305
|
+
upsert(scope, sessionId, input, now = Date.now()) {
|
|
27306
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27307
|
+
const idx = entries.findIndex((e) => e.relPath === input.relPath);
|
|
27308
|
+
let next;
|
|
27309
|
+
if (idx >= 0) {
|
|
27310
|
+
const prev = entries[idx];
|
|
27311
|
+
next = {
|
|
27312
|
+
...prev,
|
|
27313
|
+
size: input.size,
|
|
27314
|
+
mime: input.mime,
|
|
27315
|
+
lastEditedAt: now,
|
|
27316
|
+
stale: false
|
|
27317
|
+
// label 不在 upsert 路径覆盖(owner +Add 走另一条路径)
|
|
27318
|
+
};
|
|
27319
|
+
entries[idx] = next;
|
|
27320
|
+
} else {
|
|
27321
|
+
next = {
|
|
27322
|
+
id: `gf-${import_node_crypto5.default.randomBytes(6).toString("base64url")}`,
|
|
27323
|
+
relPath: input.relPath,
|
|
27324
|
+
from: input.from,
|
|
27325
|
+
label: input.label,
|
|
27326
|
+
size: input.size,
|
|
27327
|
+
mime: input.mime,
|
|
27328
|
+
addedAt: now
|
|
27329
|
+
// agent 第一次 upsert 时不写 lastEditedAt(语义上 = 首次"编辑" = addedAt)
|
|
27330
|
+
};
|
|
27331
|
+
entries.push(next);
|
|
27332
|
+
}
|
|
27333
|
+
this.writeFile(scope, sessionId, entries);
|
|
27334
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27335
|
+
return next;
|
|
27336
|
+
}
|
|
27337
|
+
/**
|
|
27338
|
+
* 标记一个 relPath stale(agent rm / mv 后文件不在)。
|
|
27339
|
+
* - 命中 → stale=true,UI 灰显
|
|
27340
|
+
* - 未命中 → noop(不需要为不在群里的文件创建 stale 条目)
|
|
27341
|
+
*
|
|
27342
|
+
* 注:spec §6 "Bash 命令是 rm 形态"启发式不强求 — runner 暂不调,留给后续优化
|
|
27343
|
+
*/
|
|
27344
|
+
markStale(scope, sessionId, relPath) {
|
|
27345
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27346
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
27347
|
+
if (idx < 0) return;
|
|
27348
|
+
if (entries[idx].stale) return;
|
|
27349
|
+
entries[idx] = { ...entries[idx], stale: true };
|
|
27350
|
+
this.writeFile(scope, sessionId, entries);
|
|
27351
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27352
|
+
}
|
|
27353
|
+
/**
|
|
27354
|
+
* 真删一条群文件条目(用于 owner 撤销自己 +Add 的入群操作)。
|
|
27355
|
+
* agent 自动入群的不应走这条 —— 用 markStale 表达"文件不在了"语义;
|
|
27356
|
+
* owner 手动加错了,应该能彻底从列表移除而不是留个 stale 占位。
|
|
27357
|
+
*
|
|
27358
|
+
* 返回值:true=命中并删除;false=relPath 不在群里。
|
|
27359
|
+
*/
|
|
27360
|
+
remove(scope, sessionId, relPath) {
|
|
27361
|
+
const entries = this.list(scope, sessionId).slice();
|
|
27362
|
+
const idx = entries.findIndex((e) => e.relPath === relPath);
|
|
27363
|
+
if (idx < 0) return false;
|
|
27364
|
+
entries.splice(idx, 1);
|
|
27365
|
+
this.writeFile(scope, sessionId, entries);
|
|
27366
|
+
this.cache.set(this.cacheKey(scope, sessionId), { entries });
|
|
27367
|
+
return true;
|
|
26277
27368
|
}
|
|
26278
|
-
|
|
26279
|
-
const basename = import_node_path16.default.basename(absPath);
|
|
26280
|
-
res.writeHead(200, {
|
|
26281
|
-
"Content-Type": mime,
|
|
26282
|
-
"Content-Length": String(stat.size),
|
|
26283
|
-
"Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(basename)}`,
|
|
26284
|
-
// 防止浏览器把任意 mime 当 html 渲染
|
|
26285
|
-
"X-Content-Type-Options": "nosniff"
|
|
26286
|
-
});
|
|
26287
|
-
const stream = import_node_fs14.default.createReadStream(absPath);
|
|
26288
|
-
stream.on("error", (err) => {
|
|
26289
|
-
logger?.warn("streamFile read error", { absPath, err: err.message });
|
|
26290
|
-
res.destroy();
|
|
26291
|
-
});
|
|
26292
|
-
stream.pipe(res);
|
|
26293
|
-
}
|
|
27369
|
+
};
|
|
26294
27370
|
|
|
26295
27371
|
// src/discovery/state-file.ts
|
|
26296
27372
|
var import_node_fs15 = __toESM(require("fs"), 1);
|
|
@@ -26970,26 +28046,32 @@ function authFilePath(dataDir) {
|
|
|
26970
28046
|
}
|
|
26971
28047
|
function loadOrCreateAuthFile(opts) {
|
|
26972
28048
|
const file = authFilePath(opts.dataDir);
|
|
26973
|
-
const generate = opts.generate ??
|
|
28049
|
+
const generate = opts.generate ?? defaultGenerateToken2;
|
|
28050
|
+
const genId = opts.genOwnerPrincipalId ?? defaultGenerateOwnerPrincipalId;
|
|
26974
28051
|
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
26975
28052
|
const existing = readAuthFile(file);
|
|
26976
|
-
if (existing && existing.token && existing.signSecret) {
|
|
28053
|
+
if (existing && existing.token && existing.signSecret && existing.ownerPrincipalId) {
|
|
26977
28054
|
return {
|
|
26978
28055
|
token: existing.token,
|
|
26979
28056
|
signSecret: existing.signSecret,
|
|
28057
|
+
ownerPrincipalId: existing.ownerPrincipalId,
|
|
26980
28058
|
createdAt: existing.createdAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
|
|
26981
28059
|
};
|
|
26982
28060
|
}
|
|
26983
28061
|
const token = existing?.token || generate();
|
|
26984
28062
|
const signSecret = existing?.signSecret || generate();
|
|
28063
|
+
const ownerPrincipalId = existing?.ownerPrincipalId || genId();
|
|
26985
28064
|
const createdAt = existing?.createdAt || now().toISOString();
|
|
26986
|
-
const next = { token, signSecret, createdAt };
|
|
28065
|
+
const next = { token, signSecret, ownerPrincipalId, createdAt };
|
|
26987
28066
|
writeAuthFile(file, next);
|
|
26988
28067
|
return next;
|
|
26989
28068
|
}
|
|
26990
|
-
function
|
|
28069
|
+
function defaultGenerateToken2() {
|
|
26991
28070
|
return import_node_crypto8.default.randomBytes(32).toString("base64url");
|
|
26992
28071
|
}
|
|
28072
|
+
function defaultGenerateOwnerPrincipalId() {
|
|
28073
|
+
return `owner-${import_node_crypto8.default.randomUUID()}`;
|
|
28074
|
+
}
|
|
26993
28075
|
function readAuthFile(file) {
|
|
26994
28076
|
try {
|
|
26995
28077
|
const raw = import_node_fs20.default.readFileSync(file, "utf8");
|
|
@@ -27000,6 +28082,7 @@ function readAuthFile(file) {
|
|
|
27000
28082
|
return {
|
|
27001
28083
|
token: parsed.token,
|
|
27002
28084
|
signSecret: typeof parsed.signSecret === "string" && parsed.signSecret.length > 0 ? parsed.signSecret : void 0,
|
|
28085
|
+
ownerPrincipalId: typeof parsed.ownerPrincipalId === "string" && parsed.ownerPrincipalId.length > 0 ? parsed.ownerPrincipalId : void 0,
|
|
27003
28086
|
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : void 0
|
|
27004
28087
|
};
|
|
27005
28088
|
} catch (err) {
|
|
@@ -27113,10 +28196,63 @@ function forkSession(input) {
|
|
|
27113
28196
|
return { forkedToolSessionId, forkedFilePath };
|
|
27114
28197
|
}
|
|
27115
28198
|
|
|
28199
|
+
// src/permission/capability.ts
|
|
28200
|
+
function matchResource(grant, target) {
|
|
28201
|
+
if (grant.type === "*") return true;
|
|
28202
|
+
if (grant.type !== target.type) return false;
|
|
28203
|
+
if (grant.id === "*") return true;
|
|
28204
|
+
return grant.id === target.id;
|
|
28205
|
+
}
|
|
28206
|
+
function assertGrant(grants, resource, action) {
|
|
28207
|
+
for (const g2 of grants) {
|
|
28208
|
+
if (!matchResource(g2.resource, resource)) continue;
|
|
28209
|
+
if (g2.actions.includes(action)) return true;
|
|
28210
|
+
if (g2.actions.includes("admin")) return true;
|
|
28211
|
+
}
|
|
28212
|
+
return false;
|
|
28213
|
+
}
|
|
28214
|
+
|
|
28215
|
+
// src/permission/session-access.ts
|
|
28216
|
+
function canAccessSession(ctx, sessionId, action, deps) {
|
|
28217
|
+
if (ctx.principal.kind === "owner") return true;
|
|
28218
|
+
const file = deps.readSession(sessionId);
|
|
28219
|
+
if (!file) return true;
|
|
28220
|
+
return canAccessPersona(ctx.grants, file.ownerPersonaId, action);
|
|
28221
|
+
}
|
|
28222
|
+
function canAccessPersona(grants, personaId, action) {
|
|
28223
|
+
if (!personaId) return false;
|
|
28224
|
+
const resource = { type: "persona", id: personaId };
|
|
28225
|
+
return assertGrant(grants, resource, action);
|
|
28226
|
+
}
|
|
28227
|
+
|
|
27116
28228
|
// src/handlers/session.ts
|
|
28229
|
+
init_protocol();
|
|
27117
28230
|
function buildSessionHandlers(deps) {
|
|
27118
|
-
const { manager, observer, getAdapter: getAdapter2 } = deps;
|
|
27119
|
-
const
|
|
28231
|
+
const { manager, observer, getAdapter: getAdapter2, store, ownerPrincipalId } = deps;
|
|
28232
|
+
const ensureSessionAccess = (ctx, sessionId, action) => {
|
|
28233
|
+
if (!ctx) return;
|
|
28234
|
+
const ok = canAccessSession(ctx, sessionId, action, {
|
|
28235
|
+
readSession: (sid) => store.read(sid)
|
|
28236
|
+
});
|
|
28237
|
+
if (!ok) {
|
|
28238
|
+
throw new ClawdError(
|
|
28239
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28240
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} session ${sessionId}`
|
|
28241
|
+
);
|
|
28242
|
+
}
|
|
28243
|
+
};
|
|
28244
|
+
const ensurePersonaAccess = (ctx, personaId, action) => {
|
|
28245
|
+
if (!ctx) return;
|
|
28246
|
+
if (ctx.principal.kind === "owner") return;
|
|
28247
|
+
const ok = canAccessPersona(ctx.grants, personaId, action);
|
|
28248
|
+
if (!ok) {
|
|
28249
|
+
throw new ClawdError(
|
|
28250
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28251
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${action} on persona:${personaId ?? "<none>"}`
|
|
28252
|
+
);
|
|
28253
|
+
}
|
|
28254
|
+
};
|
|
28255
|
+
const create = async (frame, _client, ctx) => {
|
|
27120
28256
|
const args = SessionCreateArgs.parse(frame);
|
|
27121
28257
|
if (args.ownerPersonaId) {
|
|
27122
28258
|
const persona = deps.personaRegistry.get(args.ownerPersonaId);
|
|
@@ -27124,58 +28260,75 @@ function buildSessionHandlers(deps) {
|
|
|
27124
28260
|
throw new Error(`persona not found: ${args.ownerPersonaId}`);
|
|
27125
28261
|
}
|
|
27126
28262
|
}
|
|
27127
|
-
|
|
28263
|
+
ensurePersonaAccess(ctx, args.ownerPersonaId, "send");
|
|
28264
|
+
const creatorPrincipalId = ctx?.principal.id ?? ownerPrincipalId;
|
|
28265
|
+
const { response, broadcast } = manager.create(args, creatorPrincipalId);
|
|
27128
28266
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27129
28267
|
};
|
|
27130
|
-
const list = async () => {
|
|
28268
|
+
const list = async (_frame, _client, ctx) => {
|
|
27131
28269
|
const { response } = manager.list();
|
|
28270
|
+
if (ctx && ctx.principal.kind === "guest") {
|
|
28271
|
+
const sessions = response.sessions ?? [];
|
|
28272
|
+
const capId = ctx.capabilityId;
|
|
28273
|
+
const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === capId);
|
|
28274
|
+
return { response: { type: "session:list", sessions: filtered } };
|
|
28275
|
+
}
|
|
27132
28276
|
return { response: { type: "session:list", ...response } };
|
|
27133
28277
|
};
|
|
27134
|
-
const get = async (frame) => {
|
|
28278
|
+
const get = async (frame, _client, ctx) => {
|
|
27135
28279
|
const args = SessionIdArgs.parse(frame);
|
|
28280
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27136
28281
|
const { response } = manager.get(args);
|
|
27137
28282
|
return { response: { type: "session:info", ...response } };
|
|
27138
28283
|
};
|
|
27139
|
-
const update = async (frame) => {
|
|
28284
|
+
const update = async (frame, _client, ctx) => {
|
|
27140
28285
|
const args = SessionUpdateArgs.parse(frame);
|
|
28286
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27141
28287
|
const { response, broadcast } = manager.update(args);
|
|
27142
28288
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27143
28289
|
};
|
|
27144
|
-
const del = async (frame) => {
|
|
28290
|
+
const del = async (frame, _client, ctx) => {
|
|
27145
28291
|
const args = SessionIdArgs.parse(frame);
|
|
28292
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27146
28293
|
const { broadcast } = manager.delete(args);
|
|
27147
28294
|
return {
|
|
27148
28295
|
response: { type: "session:deleted", sessionId: args.sessionId },
|
|
27149
28296
|
broadcast
|
|
27150
28297
|
};
|
|
27151
28298
|
};
|
|
27152
|
-
const send = async (frame) => {
|
|
28299
|
+
const send = async (frame, _client, ctx) => {
|
|
27153
28300
|
const args = SessionSendArgs.parse(frame);
|
|
28301
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27154
28302
|
const { broadcast } = manager.send(args);
|
|
27155
28303
|
return { response: { type: "session:send", ok: true }, broadcast };
|
|
27156
28304
|
};
|
|
27157
|
-
const stop = async (frame) => {
|
|
28305
|
+
const stop = async (frame, _client, ctx) => {
|
|
27158
28306
|
const args = SessionIdArgs.parse(frame);
|
|
28307
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27159
28308
|
const { broadcast } = await manager.stop(args);
|
|
27160
28309
|
return { response: { type: "session:stop", ok: true }, broadcast };
|
|
27161
28310
|
};
|
|
27162
|
-
const interrupt = async (frame) => {
|
|
28311
|
+
const interrupt = async (frame, _client, ctx) => {
|
|
27163
28312
|
const args = SessionIdArgs.parse(frame);
|
|
28313
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27164
28314
|
const { broadcast } = await manager.interrupt(args);
|
|
27165
28315
|
return { response: { type: "session:interrupt", ok: true }, broadcast };
|
|
27166
28316
|
};
|
|
27167
|
-
const rewind = async (frame) => {
|
|
28317
|
+
const rewind = async (frame, _client, ctx) => {
|
|
27168
28318
|
const args = SessionRewindArgs.parse(frame);
|
|
28319
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27169
28320
|
const { response, broadcast } = await manager.rewind(args);
|
|
27170
28321
|
return { response: { type: "session:rewind", ...response }, broadcast };
|
|
27171
28322
|
};
|
|
27172
|
-
const rewindDiff = async (frame) => {
|
|
28323
|
+
const rewindDiff = async (frame, _client, ctx) => {
|
|
27173
28324
|
const args = SessionRewindDiffArgs.parse(frame);
|
|
28325
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27174
28326
|
const { response } = await manager.rewindDiff(args);
|
|
27175
28327
|
return { response: { type: "session:rewind-diff", ...response } };
|
|
27176
28328
|
};
|
|
27177
|
-
const rewindableMessageIds = async (frame) => {
|
|
28329
|
+
const rewindableMessageIds = async (frame, _client, ctx) => {
|
|
27178
28330
|
const args = SessionRewindableMessageIdsArgs.parse(frame);
|
|
28331
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27179
28332
|
const { response } = manager.rewindableMessageIds(args);
|
|
27180
28333
|
return {
|
|
27181
28334
|
response: { type: "session:rewindable-message-ids", ...response }
|
|
@@ -27186,19 +28339,22 @@ function buildSessionHandlers(deps) {
|
|
|
27186
28339
|
const result = forkSession(args);
|
|
27187
28340
|
return { response: { type: "session:fork", ...result } };
|
|
27188
28341
|
};
|
|
27189
|
-
const newSession = async (frame) => {
|
|
28342
|
+
const newSession = async (frame, _client, ctx) => {
|
|
27190
28343
|
const args = SessionIdArgs.parse(frame);
|
|
28344
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27191
28345
|
observer.stop(args.sessionId);
|
|
27192
28346
|
const { response, broadcast } = manager.newSession(args);
|
|
27193
28347
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27194
28348
|
};
|
|
27195
|
-
const resume = async (frame) => {
|
|
28349
|
+
const resume = async (frame, _client, ctx) => {
|
|
27196
28350
|
const args = SessionResumeArgs.parse(frame);
|
|
28351
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27197
28352
|
const { response, broadcast } = manager.resume(args);
|
|
27198
28353
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27199
28354
|
};
|
|
27200
|
-
const observe = async (frame) => {
|
|
28355
|
+
const observe = async (frame, _client, ctx) => {
|
|
27201
28356
|
const args = SessionObserveArgs.parse(frame);
|
|
28357
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27202
28358
|
const { response: file } = manager.get({ sessionId: args.sessionId });
|
|
27203
28359
|
const sessionFile = file;
|
|
27204
28360
|
manager.ensureSession(sessionFile);
|
|
@@ -27212,14 +28368,17 @@ function buildSessionHandlers(deps) {
|
|
|
27212
28368
|
});
|
|
27213
28369
|
return { response: { type: "session:observe", ok: true } };
|
|
27214
28370
|
};
|
|
27215
|
-
const events = async (frame) => {
|
|
28371
|
+
const events = async (frame, _client, ctx) => {
|
|
27216
28372
|
const args = SessionEventsArgs.parse(frame);
|
|
28373
|
+
ensureSessionAccess(ctx, args.sessionId, "read");
|
|
27217
28374
|
const { response } = manager.getEvents(args);
|
|
27218
28375
|
return { response: { type: "session:events", ...response } };
|
|
27219
28376
|
};
|
|
27220
|
-
const subscribe = async (frame, client) => {
|
|
27221
|
-
if (typeof frame.sessionId === "string")
|
|
28377
|
+
const subscribe = async (frame, client, ctx) => {
|
|
28378
|
+
if (typeof frame.sessionId === "string") {
|
|
28379
|
+
ensureSessionAccess(ctx, frame.sessionId, "read");
|
|
27222
28380
|
addSubscription(client, frame.sessionId);
|
|
28381
|
+
}
|
|
27223
28382
|
return {
|
|
27224
28383
|
response: { type: "subscribed", sessionId: frame.sessionId }
|
|
27225
28384
|
};
|
|
@@ -27231,8 +28390,9 @@ function buildSessionHandlers(deps) {
|
|
|
27231
28390
|
response: { type: "unsubscribed", sessionId: frame.sessionId }
|
|
27232
28391
|
};
|
|
27233
28392
|
};
|
|
27234
|
-
const pin = async (frame) => {
|
|
28393
|
+
const pin = async (frame, _client, ctx) => {
|
|
27235
28394
|
const args = SessionPinArgs.parse(frame);
|
|
28395
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27236
28396
|
const { response, broadcast } = manager.pin(args);
|
|
27237
28397
|
return { response: { type: "session:info", ...response }, broadcast };
|
|
27238
28398
|
};
|
|
@@ -27241,13 +28401,15 @@ function buildSessionHandlers(deps) {
|
|
|
27241
28401
|
const { response, broadcast } = manager.reorderPins(args);
|
|
27242
28402
|
return { response: { type: "session:reorderPins", ...response }, broadcast };
|
|
27243
28403
|
};
|
|
27244
|
-
const answerQuestion = async (frame) => {
|
|
28404
|
+
const answerQuestion = async (frame, _client, ctx) => {
|
|
27245
28405
|
const args = AnswerQuestionArgs.parse(frame);
|
|
28406
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27246
28407
|
const { response, broadcast } = manager.answerQuestion(args);
|
|
27247
28408
|
return { response: { type: "session:answerQuestion", ...response }, broadcast };
|
|
27248
28409
|
};
|
|
27249
|
-
const cancelQuestion = async (frame) => {
|
|
28410
|
+
const cancelQuestion = async (frame, _client, ctx) => {
|
|
27250
28411
|
const args = CancelQuestionArgs.parse(frame);
|
|
28412
|
+
ensureSessionAccess(ctx, args.sessionId, "send");
|
|
27251
28413
|
const { response, broadcast } = await manager.cancelQuestion(args);
|
|
27252
28414
|
return { response: { type: "session:cancelQuestion", ...response }, broadcast };
|
|
27253
28415
|
};
|
|
@@ -27342,6 +28504,7 @@ function listRecentDirs(store, limit = 50) {
|
|
|
27342
28504
|
}
|
|
27343
28505
|
|
|
27344
28506
|
// src/handlers/history.ts
|
|
28507
|
+
init_protocol();
|
|
27345
28508
|
function buildHistoryHandlers(deps) {
|
|
27346
28509
|
const { manager, history, store } = deps;
|
|
27347
28510
|
const projects = async () => {
|
|
@@ -27353,17 +28516,27 @@ function buildHistoryHandlers(deps) {
|
|
|
27353
28516
|
const sessions = await history.listSessions(args);
|
|
27354
28517
|
return { response: { type: "history:list", sessions } };
|
|
27355
28518
|
};
|
|
27356
|
-
const read = async (frame) => {
|
|
28519
|
+
const read = async (frame, _client, ctx) => {
|
|
27357
28520
|
const args = HistoryReadArgs.parse(frame);
|
|
27358
28521
|
const { response: file } = manager.get({ sessionId: args.sessionId });
|
|
27359
|
-
|
|
28522
|
+
const f = file;
|
|
28523
|
+
if (ctx && ctx.principal.kind === "guest") {
|
|
28524
|
+
const ok = canAccessPersona(ctx.grants, f.ownerPersonaId, "read") && f.creatorPrincipalId === ctx.capabilityId;
|
|
28525
|
+
if (!ok) {
|
|
28526
|
+
throw new ClawdError(
|
|
28527
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28528
|
+
`principal guest:${ctx.principal.id} cannot read history of session ${args.sessionId}`
|
|
28529
|
+
);
|
|
28530
|
+
}
|
|
28531
|
+
}
|
|
28532
|
+
if (!f.toolSessionId) {
|
|
27360
28533
|
return {
|
|
27361
28534
|
response: { type: "history:read", messages: [], total: 0, offset: 0 }
|
|
27362
28535
|
};
|
|
27363
28536
|
}
|
|
27364
28537
|
const res = await history.read({
|
|
27365
|
-
cwd:
|
|
27366
|
-
toolSessionId:
|
|
28538
|
+
cwd: f.cwd,
|
|
28539
|
+
toolSessionId: f.toolSessionId,
|
|
27367
28540
|
limit: args.limit,
|
|
27368
28541
|
offset: args.offset
|
|
27369
28542
|
});
|
|
@@ -27545,6 +28718,345 @@ function buildCapabilitiesHandlers(deps) {
|
|
|
27545
28718
|
};
|
|
27546
28719
|
}
|
|
27547
28720
|
|
|
28721
|
+
// src/handlers/capability.ts
|
|
28722
|
+
init_zod();
|
|
28723
|
+
init_protocol();
|
|
28724
|
+
var DeleteArgsSchema = external_exports.object({
|
|
28725
|
+
capabilityId: external_exports.string().min(1)
|
|
28726
|
+
}).strict();
|
|
28727
|
+
function buildCapabilityHandlers(deps) {
|
|
28728
|
+
const { manager, getShareBaseUrl } = deps;
|
|
28729
|
+
const issue = async (frame) => {
|
|
28730
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28731
|
+
const args = CapabilityIssueArgsSchema.parse(rest);
|
|
28732
|
+
const { token, capability } = manager.issue({
|
|
28733
|
+
displayName: args.displayName,
|
|
28734
|
+
grants: args.grants,
|
|
28735
|
+
...args.expiresAt !== void 0 ? { expiresAt: args.expiresAt } : {},
|
|
28736
|
+
...args.maxUses !== void 0 ? { maxUses: args.maxUses } : {}
|
|
28737
|
+
});
|
|
28738
|
+
const base = getShareBaseUrl().replace(/\/$/, "");
|
|
28739
|
+
const shareUrl = `${base}/?token=${encodeURIComponent(token)}`;
|
|
28740
|
+
return {
|
|
28741
|
+
response: {
|
|
28742
|
+
type: "capability:issued",
|
|
28743
|
+
token,
|
|
28744
|
+
shareUrl,
|
|
28745
|
+
capability: stripSecretHash(capability)
|
|
28746
|
+
}
|
|
28747
|
+
};
|
|
28748
|
+
};
|
|
28749
|
+
const list = async () => {
|
|
28750
|
+
return {
|
|
28751
|
+
response: {
|
|
28752
|
+
type: "capability:list",
|
|
28753
|
+
capabilities: manager.list().map(stripSecretHash)
|
|
28754
|
+
}
|
|
28755
|
+
};
|
|
28756
|
+
};
|
|
28757
|
+
const del = async (frame) => {
|
|
28758
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28759
|
+
const args = DeleteArgsSchema.parse(rest);
|
|
28760
|
+
const result = manager.delete(args.capabilityId);
|
|
28761
|
+
if (!result) {
|
|
28762
|
+
throw new ClawdError(
|
|
28763
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28764
|
+
`capability not found: ${args.capabilityId}`
|
|
28765
|
+
);
|
|
28766
|
+
}
|
|
28767
|
+
return {
|
|
28768
|
+
response: {
|
|
28769
|
+
type: "capability:deleted",
|
|
28770
|
+
capabilityId: args.capabilityId
|
|
28771
|
+
}
|
|
28772
|
+
};
|
|
28773
|
+
};
|
|
28774
|
+
const bindPeer = async (frame) => {
|
|
28775
|
+
const { type: _type, requestId: _requestId, ...rest } = frame;
|
|
28776
|
+
const args = CapabilityBindPeerArgsSchema.parse(rest);
|
|
28777
|
+
try {
|
|
28778
|
+
manager.recordPeerHello(args.capabilityId, {
|
|
28779
|
+
ownerPrincipalId: args.peerOwnerId,
|
|
28780
|
+
displayName: args.peerDisplayName,
|
|
28781
|
+
now: Date.now()
|
|
28782
|
+
});
|
|
28783
|
+
} catch (e) {
|
|
28784
|
+
throw new ClawdError(
|
|
28785
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28786
|
+
`capability:bindPeer: ${e.message}`
|
|
28787
|
+
);
|
|
28788
|
+
}
|
|
28789
|
+
return {
|
|
28790
|
+
response: { type: "capability:bindPeer:ok", capabilityId: args.capabilityId }
|
|
28791
|
+
};
|
|
28792
|
+
};
|
|
28793
|
+
return {
|
|
28794
|
+
"capability:issue": issue,
|
|
28795
|
+
"capability:list": list,
|
|
28796
|
+
"capability:delete": del,
|
|
28797
|
+
"capability:bindPeer": bindPeer
|
|
28798
|
+
};
|
|
28799
|
+
}
|
|
28800
|
+
|
|
28801
|
+
// src/handlers/inbox.ts
|
|
28802
|
+
init_protocol();
|
|
28803
|
+
function resolvePeerOwnerId(ctx, argsPeerOwnerId, capManager) {
|
|
28804
|
+
if (ctx.principal.kind === "owner") {
|
|
28805
|
+
if (!argsPeerOwnerId) {
|
|
28806
|
+
throw new ClawdError(
|
|
28807
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28808
|
+
"inbox: owner ctx requires peerOwnerId"
|
|
28809
|
+
);
|
|
28810
|
+
}
|
|
28811
|
+
return argsPeerOwnerId;
|
|
28812
|
+
}
|
|
28813
|
+
if (!ctx.capabilityId) {
|
|
28814
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "inbox: guest ctx missing capabilityId");
|
|
28815
|
+
}
|
|
28816
|
+
const cap = capManager.findById(ctx.capabilityId);
|
|
28817
|
+
if (!cap?.peerOwnerId) {
|
|
28818
|
+
throw new ClawdError(
|
|
28819
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28820
|
+
`inbox: guest cap ${ctx.capabilityId} has no peerOwnerId (recordPeerHello not yet)`
|
|
28821
|
+
);
|
|
28822
|
+
}
|
|
28823
|
+
if (argsPeerOwnerId && argsPeerOwnerId !== cap.peerOwnerId) {
|
|
28824
|
+
throw new ClawdError(
|
|
28825
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28826
|
+
`inbox: guest args.peerOwnerId mismatch (cap.peerOwnerId=${cap.peerOwnerId}, args=${argsPeerOwnerId})`
|
|
28827
|
+
);
|
|
28828
|
+
}
|
|
28829
|
+
return cap.peerOwnerId;
|
|
28830
|
+
}
|
|
28831
|
+
function buildInboxHandlers(deps) {
|
|
28832
|
+
const { manager, capManager } = deps;
|
|
28833
|
+
const postMessage = async (frame, _client, ctx) => {
|
|
28834
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28835
|
+
const args = InboxPostMessageArgsSchema.parse(rest);
|
|
28836
|
+
if (!ctx) {
|
|
28837
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:postMessage: missing ConnectionContext");
|
|
28838
|
+
}
|
|
28839
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28840
|
+
const message = manager.postMessage({
|
|
28841
|
+
peerOwnerId,
|
|
28842
|
+
senderPrincipalId: ctx.principal.id,
|
|
28843
|
+
text: args.text,
|
|
28844
|
+
id: args.id,
|
|
28845
|
+
createdAt: args.createdAt
|
|
28846
|
+
});
|
|
28847
|
+
return {
|
|
28848
|
+
response: { type: "inbox:postMessage:ok", message }
|
|
28849
|
+
};
|
|
28850
|
+
};
|
|
28851
|
+
const list = async (frame, _client, ctx) => {
|
|
28852
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28853
|
+
const args = InboxListArgsSchema.parse(rest);
|
|
28854
|
+
if (!ctx) {
|
|
28855
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:list: missing ConnectionContext");
|
|
28856
|
+
}
|
|
28857
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28858
|
+
const messages = manager.list(peerOwnerId, args.sinceCreatedAt);
|
|
28859
|
+
return {
|
|
28860
|
+
response: { type: "inbox:list:ok", peerOwnerId, messages }
|
|
28861
|
+
};
|
|
28862
|
+
};
|
|
28863
|
+
const markRead = async (frame, _client, ctx) => {
|
|
28864
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28865
|
+
const args = InboxMarkReadArgsSchema.parse(rest);
|
|
28866
|
+
if (!ctx) {
|
|
28867
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "inbox:markRead: missing ConnectionContext");
|
|
28868
|
+
}
|
|
28869
|
+
const peerOwnerId = resolvePeerOwnerId(ctx, args.peerOwnerId, capManager);
|
|
28870
|
+
const updated = manager.markRead({
|
|
28871
|
+
peerOwnerId,
|
|
28872
|
+
principalId: ctx.principal.id,
|
|
28873
|
+
upToCreatedAt: args.upToCreatedAt
|
|
28874
|
+
});
|
|
28875
|
+
return {
|
|
28876
|
+
response: {
|
|
28877
|
+
type: "inbox:markRead:ok",
|
|
28878
|
+
peerOwnerId,
|
|
28879
|
+
upToCreatedAt: args.upToCreatedAt,
|
|
28880
|
+
updated
|
|
28881
|
+
}
|
|
28882
|
+
};
|
|
28883
|
+
};
|
|
28884
|
+
return {
|
|
28885
|
+
"inbox:postMessage": postMessage,
|
|
28886
|
+
"inbox:list": list,
|
|
28887
|
+
"inbox:markRead": markRead
|
|
28888
|
+
};
|
|
28889
|
+
}
|
|
28890
|
+
|
|
28891
|
+
// src/handlers/received-capability.ts
|
|
28892
|
+
init_protocol();
|
|
28893
|
+
function ensureOwner(ctx) {
|
|
28894
|
+
if (!ctx || ctx.principal.kind !== "owner") {
|
|
28895
|
+
throw new ClawdError(
|
|
28896
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28897
|
+
"UNAUTHORIZED: received-capability:* requires owner ctx"
|
|
28898
|
+
);
|
|
28899
|
+
}
|
|
28900
|
+
}
|
|
28901
|
+
function ensureGuestCtx(ctx) {
|
|
28902
|
+
if (!ctx || ctx.principal.kind !== "guest" || !ctx.capabilityId) {
|
|
28903
|
+
throw new ClawdError(
|
|
28904
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
28905
|
+
"UNAUTHORIZED: received-capability:autoAttach requires guest cap ctx"
|
|
28906
|
+
);
|
|
28907
|
+
}
|
|
28908
|
+
}
|
|
28909
|
+
function buildReceivedCapabilityHandlers(deps) {
|
|
28910
|
+
const now = deps.now ?? Date.now;
|
|
28911
|
+
const list = async (_frame, _client, ctx) => {
|
|
28912
|
+
ensureOwner(ctx);
|
|
28913
|
+
return {
|
|
28914
|
+
response: {
|
|
28915
|
+
type: "received-capability:list:ok",
|
|
28916
|
+
receivedCaps: deps.store.list()
|
|
28917
|
+
}
|
|
28918
|
+
};
|
|
28919
|
+
};
|
|
28920
|
+
const add = async (frame, _client, ctx) => {
|
|
28921
|
+
ensureOwner(ctx);
|
|
28922
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28923
|
+
const args = ReceivedCapabilityAddArgsSchema.parse(rest);
|
|
28924
|
+
let conn;
|
|
28925
|
+
try {
|
|
28926
|
+
conn = await deps.connectRemote({
|
|
28927
|
+
url: args.remoteUrl,
|
|
28928
|
+
token: args.token,
|
|
28929
|
+
selfPrincipalId: deps.selfPrincipalId(),
|
|
28930
|
+
selfDisplayName: deps.selfDisplayName()
|
|
28931
|
+
});
|
|
28932
|
+
} catch (e) {
|
|
28933
|
+
throw new ClawdError(
|
|
28934
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
28935
|
+
`INVITE_REMOTE_UNREACHABLE: ${e.message}`
|
|
28936
|
+
);
|
|
28937
|
+
}
|
|
28938
|
+
try {
|
|
28939
|
+
const wh = await conn.whoami();
|
|
28940
|
+
const rc = {
|
|
28941
|
+
ownerPrincipalId: wh.ownerId,
|
|
28942
|
+
ownerDisplayName: wh.displayName,
|
|
28943
|
+
remoteUrl: args.remoteUrl,
|
|
28944
|
+
capabilityId: wh.capabilityId,
|
|
28945
|
+
capabilityToken: args.token,
|
|
28946
|
+
grants: wh.grants,
|
|
28947
|
+
receivedAt: now()
|
|
28948
|
+
};
|
|
28949
|
+
deps.store.upsert(rc);
|
|
28950
|
+
deps.broadcast({ type: "received-capability:added", receivedCap: rc });
|
|
28951
|
+
return {
|
|
28952
|
+
response: { type: "received-capability:add:ok", receivedCap: rc }
|
|
28953
|
+
};
|
|
28954
|
+
} finally {
|
|
28955
|
+
try {
|
|
28956
|
+
conn.close();
|
|
28957
|
+
} catch {
|
|
28958
|
+
}
|
|
28959
|
+
}
|
|
28960
|
+
};
|
|
28961
|
+
const remove = async (frame, _client, ctx) => {
|
|
28962
|
+
ensureOwner(ctx);
|
|
28963
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28964
|
+
const args = ReceivedCapabilityRemoveArgsSchema.parse(rest);
|
|
28965
|
+
deps.store.remove(args.ownerPrincipalId);
|
|
28966
|
+
deps.broadcast({
|
|
28967
|
+
type: "received-capability:removed",
|
|
28968
|
+
ownerPrincipalId: args.ownerPrincipalId
|
|
28969
|
+
});
|
|
28970
|
+
return {
|
|
28971
|
+
response: {
|
|
28972
|
+
type: "received-capability:remove:ok",
|
|
28973
|
+
ownerPrincipalId: args.ownerPrincipalId
|
|
28974
|
+
}
|
|
28975
|
+
};
|
|
28976
|
+
};
|
|
28977
|
+
const autoAttach = async (frame, _client, ctx) => {
|
|
28978
|
+
ensureGuestCtx(ctx);
|
|
28979
|
+
const { type: _t, requestId: _r, ...rest } = frame;
|
|
28980
|
+
const args = ReceivedCapabilityAutoAttachArgsSchema.parse(rest);
|
|
28981
|
+
const rc = {
|
|
28982
|
+
ownerPrincipalId: args.ownerPrincipalId,
|
|
28983
|
+
ownerDisplayName: args.ownerDisplayName,
|
|
28984
|
+
remoteUrl: args.remoteUrl,
|
|
28985
|
+
capabilityId: args.capabilityId,
|
|
28986
|
+
capabilityToken: args.token,
|
|
28987
|
+
grants: args.grants,
|
|
28988
|
+
receivedAt: now()
|
|
28989
|
+
};
|
|
28990
|
+
deps.store.upsert(rc);
|
|
28991
|
+
deps.broadcast({ type: "received-capability:added", receivedCap: rc });
|
|
28992
|
+
return {
|
|
28993
|
+
response: { type: "received-capability:autoAttach:ok" }
|
|
28994
|
+
};
|
|
28995
|
+
};
|
|
28996
|
+
return {
|
|
28997
|
+
"received-capability:list": list,
|
|
28998
|
+
"received-capability:add": add,
|
|
28999
|
+
"received-capability:remove": remove,
|
|
29000
|
+
"received-capability:autoAttach": autoAttach
|
|
29001
|
+
};
|
|
29002
|
+
}
|
|
29003
|
+
|
|
29004
|
+
// src/handlers/whoami.ts
|
|
29005
|
+
init_protocol();
|
|
29006
|
+
function buildWhoamiHandler(deps) {
|
|
29007
|
+
return async (_frame, _client, ctx) => {
|
|
29008
|
+
if (!ctx) {
|
|
29009
|
+
throw new ClawdError(ERROR_CODES.INTERNAL, "whoami: missing ConnectionContext");
|
|
29010
|
+
}
|
|
29011
|
+
const owner = makeOwnerPrincipal(deps.ownerPrincipalId, deps.ownerDisplayName);
|
|
29012
|
+
let capability;
|
|
29013
|
+
if (ctx.principal.kind === "owner") {
|
|
29014
|
+
capability = {
|
|
29015
|
+
id: deps.ownerPrincipalId,
|
|
29016
|
+
displayName: deps.ownerDisplayName,
|
|
29017
|
+
grants: ctx.grants,
|
|
29018
|
+
issuedAt: 0,
|
|
29019
|
+
usedCount: 0
|
|
29020
|
+
};
|
|
29021
|
+
} else {
|
|
29022
|
+
if (!ctx.capabilityId) {
|
|
29023
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, "whoami: guest ctx without capabilityId");
|
|
29024
|
+
}
|
|
29025
|
+
const cap = deps.capabilityManager.findById(ctx.capabilityId);
|
|
29026
|
+
if (!cap) {
|
|
29027
|
+
throw new ClawdError(ERROR_CODES.UNAUTHORIZED, `whoami: capability not found: ${ctx.capabilityId}`);
|
|
29028
|
+
}
|
|
29029
|
+
capability = stripSecretHash(cap);
|
|
29030
|
+
}
|
|
29031
|
+
const grantedPersonas = [];
|
|
29032
|
+
const isGuest = ctx.principal.kind === "guest";
|
|
29033
|
+
const hasWildcard = capability.grants.some((g2) => g2.resource.type === "*");
|
|
29034
|
+
if (hasWildcard) {
|
|
29035
|
+
for (const id of deps.personaStore.list()) {
|
|
29036
|
+
const file = deps.personaStore.read(id);
|
|
29037
|
+
if (!file) continue;
|
|
29038
|
+
if (isGuest && !file.public) continue;
|
|
29039
|
+
grantedPersonas.push({ id: file.personaId, displayName: file.label });
|
|
29040
|
+
}
|
|
29041
|
+
} else {
|
|
29042
|
+
for (const g2 of capability.grants) {
|
|
29043
|
+
if (g2.resource.type !== "persona") continue;
|
|
29044
|
+
const file = deps.personaStore.read(g2.resource.id);
|
|
29045
|
+
if (!file) continue;
|
|
29046
|
+
grantedPersonas.push({ id: file.personaId, displayName: file.label });
|
|
29047
|
+
}
|
|
29048
|
+
}
|
|
29049
|
+
return {
|
|
29050
|
+
response: {
|
|
29051
|
+
type: "whoami:ok",
|
|
29052
|
+
owner,
|
|
29053
|
+
capability,
|
|
29054
|
+
grantedPersonas
|
|
29055
|
+
}
|
|
29056
|
+
};
|
|
29057
|
+
};
|
|
29058
|
+
}
|
|
29059
|
+
|
|
27548
29060
|
// src/handlers/meta.ts
|
|
27549
29061
|
var import_node_os13 = __toESM(require("os"), 1);
|
|
27550
29062
|
init_protocol();
|
|
@@ -27608,9 +29120,14 @@ function buildPersonaHandlers(deps) {
|
|
|
27608
29120
|
const persona = personaManager.create(args);
|
|
27609
29121
|
return { response: { type: "persona:info", ...persona } };
|
|
27610
29122
|
};
|
|
27611
|
-
const list = async () => {
|
|
29123
|
+
const list = async (_frame, _client, ctx) => {
|
|
29124
|
+
const all = personaRegistry.list();
|
|
29125
|
+
const isGuest = ctx?.principal.kind === "guest";
|
|
29126
|
+
const personas = isGuest ? all.filter(
|
|
29127
|
+
(p2) => p2.public && assertGrant(ctx.grants, { type: "persona", id: p2.personaId }, "read")
|
|
29128
|
+
) : all;
|
|
27612
29129
|
return {
|
|
27613
|
-
response: { type: "persona:list", personas
|
|
29130
|
+
response: { type: "persona:list", personas }
|
|
27614
29131
|
};
|
|
27615
29132
|
};
|
|
27616
29133
|
const get = async (frame) => {
|
|
@@ -27648,26 +29165,12 @@ function buildPersonaHandlers(deps) {
|
|
|
27648
29165
|
response: { type: "persona:deleted", personaId: args.personaId }
|
|
27649
29166
|
};
|
|
27650
29167
|
};
|
|
27651
|
-
const issueToken = async (frame) => {
|
|
27652
|
-
const args = PersonaIssueTokenArgsSchema.parse(frame);
|
|
27653
|
-
const { token, persona } = personaManager.issueToken(args.personaId, args.label);
|
|
27654
|
-
return {
|
|
27655
|
-
response: { type: "persona:tokenIssued", token, persona }
|
|
27656
|
-
};
|
|
27657
|
-
};
|
|
27658
|
-
const revokeToken = async (frame) => {
|
|
27659
|
-
const args = PersonaRevokeTokenArgsSchema.parse(frame);
|
|
27660
|
-
const persona = personaManager.revokeToken(args.personaId, args.token);
|
|
27661
|
-
return { response: { type: "persona:info", ...persona } };
|
|
27662
|
-
};
|
|
27663
29168
|
return {
|
|
27664
29169
|
"persona:create": create,
|
|
27665
29170
|
"persona:list": list,
|
|
27666
29171
|
"persona:get": get,
|
|
27667
29172
|
"persona:update": update,
|
|
27668
|
-
"persona:delete": del
|
|
27669
|
-
"persona:issueToken": issueToken,
|
|
27670
|
-
"persona:revokeToken": revokeToken
|
|
29173
|
+
"persona:delete": del
|
|
27671
29174
|
};
|
|
27672
29175
|
}
|
|
27673
29176
|
|
|
@@ -27798,23 +29301,11 @@ function buildAttachmentHandlers(deps) {
|
|
|
27798
29301
|
const entries = deps.groupFileStore.list(scope, parsed.data.sessionId);
|
|
27799
29302
|
return { response: { type: "attachment.groupList", entries } };
|
|
27800
29303
|
};
|
|
27801
|
-
const groupListPersona = async (frame) => {
|
|
27802
|
-
if (!deps.groupFileStore) {
|
|
27803
|
-
throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, "groupFileStore not wired");
|
|
27804
|
-
}
|
|
27805
|
-
const parsed = AttachmentGroupListPersonaArgs.safeParse(frame);
|
|
27806
|
-
if (!parsed.success) {
|
|
27807
|
-
throw new ClawdError(ERROR_CODES.VALIDATION_ERROR, parsed.error.message);
|
|
27808
|
-
}
|
|
27809
|
-
const perSession = deps.groupFileStore.listByPersona(parsed.data.personaId);
|
|
27810
|
-
return { response: { type: "attachment.groupListPersona", perSession } };
|
|
27811
|
-
};
|
|
27812
29304
|
return {
|
|
27813
29305
|
"attachment.signUrl": signUrl,
|
|
27814
29306
|
"attachment.groupAdd": groupAdd,
|
|
27815
29307
|
"attachment.groupRemove": groupRemove,
|
|
27816
|
-
"attachment.groupList": groupList
|
|
27817
|
-
"attachment.groupListPersona": groupListPersona
|
|
29308
|
+
"attachment.groupList": groupList
|
|
27818
29309
|
};
|
|
27819
29310
|
}
|
|
27820
29311
|
|
|
@@ -27832,10 +29323,136 @@ function buildMethodHandlers(deps) {
|
|
|
27832
29323
|
personaManager: deps.personaManager,
|
|
27833
29324
|
personaRegistry: deps.personaRegistry
|
|
27834
29325
|
}),
|
|
29326
|
+
...buildCapabilityHandlers({
|
|
29327
|
+
manager: deps.capabilityManager,
|
|
29328
|
+
getShareBaseUrl: deps.getShareBaseUrl
|
|
29329
|
+
}),
|
|
29330
|
+
...buildInboxHandlers({
|
|
29331
|
+
manager: deps.inboxManager,
|
|
29332
|
+
capManager: deps.capabilityManager
|
|
29333
|
+
}),
|
|
29334
|
+
...buildReceivedCapabilityHandlers({
|
|
29335
|
+
store: deps.receivedCapabilityStore,
|
|
29336
|
+
connectRemote: deps.connectRemote,
|
|
29337
|
+
broadcast: deps.broadcastToOwners,
|
|
29338
|
+
selfPrincipalId: () => deps.ownerPrincipalId,
|
|
29339
|
+
selfDisplayName: () => deps.ownerDisplayName
|
|
29340
|
+
}),
|
|
29341
|
+
whoami: buildWhoamiHandler({
|
|
29342
|
+
ownerDisplayName: deps.ownerDisplayName,
|
|
29343
|
+
ownerPrincipalId: deps.ownerPrincipalId,
|
|
29344
|
+
personaStore: deps.personaStore,
|
|
29345
|
+
capabilityManager: deps.capabilityManager
|
|
29346
|
+
}),
|
|
27835
29347
|
...deps.attachment ? buildAttachmentHandlers(deps.attachment) : {}
|
|
27836
29348
|
};
|
|
27837
29349
|
}
|
|
27838
29350
|
|
|
29351
|
+
// src/handlers/method-grants.ts
|
|
29352
|
+
var ADMIN_ANY = {
|
|
29353
|
+
kind: "fixed",
|
|
29354
|
+
resource: { type: "*" },
|
|
29355
|
+
action: "admin"
|
|
29356
|
+
};
|
|
29357
|
+
var CAPABILITY_SCOPED = { kind: "capability-scoped" };
|
|
29358
|
+
var METHOD_GRANT_MAP = {
|
|
29359
|
+
// ---- public(meta-only,guest 也能调) ----
|
|
29360
|
+
"info": { kind: "public" },
|
|
29361
|
+
"ping": { kind: "public" },
|
|
29362
|
+
// v2 Phase 6: whoami 是身份反查,任何 authed connection (owner / guest) 都能调;
|
|
29363
|
+
// handler 内部按 ctx.principal.kind 决定返回 "合成 owner self capability" 还是
|
|
29364
|
+
// "查 capabilityManager 拿 caller 的 guest capability"。
|
|
29365
|
+
"whoami": { kind: "public" },
|
|
29366
|
+
// ---- capability platform(admin-only,本 PR 新增) ----
|
|
29367
|
+
"capability:issue": ADMIN_ANY,
|
|
29368
|
+
"capability:list": ADMIN_ANY,
|
|
29369
|
+
"capability:delete": ADMIN_ANY,
|
|
29370
|
+
"capability:bindPeer": ADMIN_ANY,
|
|
29371
|
+
// ---- inbox: channel-based IM (capability platform v3) ----
|
|
29372
|
+
// 三条都 'public' — capability 本身就是授权凭证. handler 内额外校验
|
|
29373
|
+
// guest ctx.capabilityId === args.capabilityId, 防 guest 越权操作别的 channel.
|
|
29374
|
+
"inbox:list": { kind: "public" },
|
|
29375
|
+
"inbox:markRead": { kind: "public" },
|
|
29376
|
+
"inbox:postMessage": { kind: "public" },
|
|
29377
|
+
// ---- received-capability:* (bidirectional cap 视角 B 2026-05-23) ----
|
|
29378
|
+
// owner-only: list / add / remove;guest-only: autoAttach (capability 本身是凭证)
|
|
29379
|
+
"received-capability:list": ADMIN_ANY,
|
|
29380
|
+
"received-capability:add": ADMIN_ANY,
|
|
29381
|
+
"received-capability:remove": ADMIN_ANY,
|
|
29382
|
+
"received-capability:autoAttach": { kind: "public" },
|
|
29383
|
+
// ---- session:* / chat:* 业务方法(v2 Phase 8 两层模型)----
|
|
29384
|
+
// dispatcher 不验资源,handler 内按 ctx + frame.args 反查 ownerPersonaId 调 assertGrant。
|
|
29385
|
+
// owner 自动通过(ctx 自带 '*':'admin' grant 一切 match);guest 在被授权 persona 内可调。
|
|
29386
|
+
"session:create": CAPABILITY_SCOPED,
|
|
29387
|
+
"session:list": CAPABILITY_SCOPED,
|
|
29388
|
+
"session:get": CAPABILITY_SCOPED,
|
|
29389
|
+
"session:update": CAPABILITY_SCOPED,
|
|
29390
|
+
"session:delete": CAPABILITY_SCOPED,
|
|
29391
|
+
"session:send": CAPABILITY_SCOPED,
|
|
29392
|
+
"session:stop": CAPABILITY_SCOPED,
|
|
29393
|
+
"session:interrupt": CAPABILITY_SCOPED,
|
|
29394
|
+
"session:rewind": CAPABILITY_SCOPED,
|
|
29395
|
+
"session:rewind-diff": CAPABILITY_SCOPED,
|
|
29396
|
+
"session:rewindable-message-ids": CAPABILITY_SCOPED,
|
|
29397
|
+
"session:fork": CAPABILITY_SCOPED,
|
|
29398
|
+
"session:new": CAPABILITY_SCOPED,
|
|
29399
|
+
"session:resume": CAPABILITY_SCOPED,
|
|
29400
|
+
"session:observe": CAPABILITY_SCOPED,
|
|
29401
|
+
"session:events": CAPABILITY_SCOPED,
|
|
29402
|
+
"session:subscribe": CAPABILITY_SCOPED,
|
|
29403
|
+
"session:unsubscribe": CAPABILITY_SCOPED,
|
|
29404
|
+
"session:pin": CAPABILITY_SCOPED,
|
|
29405
|
+
"session:reorderPins": ADMIN_ANY,
|
|
29406
|
+
// owner 全局操作,无 personaId 维度
|
|
29407
|
+
"permission:respond": CAPABILITY_SCOPED,
|
|
29408
|
+
"session:answerQuestion": CAPABILITY_SCOPED,
|
|
29409
|
+
"session:cancelQuestion": CAPABILITY_SCOPED,
|
|
29410
|
+
"history:projects": ADMIN_ANY,
|
|
29411
|
+
"history:list": ADMIN_ANY,
|
|
29412
|
+
// history:read 取一条 session 的 CC JSONL 历史。guest 进 persona VM 后必须能读自己
|
|
29413
|
+
// 创建的 session 历史(chat view 加载历史消息走这条),所以下沉到 capability-scoped;
|
|
29414
|
+
// handler 内按 ctx.capabilityId === file.creatorPrincipalId 拦越权。
|
|
29415
|
+
"history:read": CAPABILITY_SCOPED,
|
|
29416
|
+
"history:subagents": ADMIN_ANY,
|
|
29417
|
+
"history:subagent-read": ADMIN_ANY,
|
|
29418
|
+
"history:recentDirs": ADMIN_ANY,
|
|
29419
|
+
"workspace:list": ADMIN_ANY,
|
|
29420
|
+
"workspace:read": ADMIN_ANY,
|
|
29421
|
+
"skills:list": ADMIN_ANY,
|
|
29422
|
+
"agents:list": ADMIN_ANY,
|
|
29423
|
+
"git:root": ADMIN_ANY,
|
|
29424
|
+
"git:branch": ADMIN_ANY,
|
|
29425
|
+
"git:branches": ADMIN_ANY,
|
|
29426
|
+
"capabilities:get": ADMIN_ANY,
|
|
29427
|
+
"persona:create": ADMIN_ANY,
|
|
29428
|
+
// persona:list 下沉到 CAPABILITY_SCOPED:guest 调用合法(用于 People 面板展开
|
|
29429
|
+
// 对方分享的 personas),handler 端按 ctx + persona.public + assertGrant 过滤。
|
|
29430
|
+
"persona:list": CAPABILITY_SCOPED,
|
|
29431
|
+
"persona:get": ADMIN_ANY,
|
|
29432
|
+
"persona:update": ADMIN_ANY,
|
|
29433
|
+
"persona:delete": ADMIN_ANY,
|
|
29434
|
+
"session:pty:input": ADMIN_ANY,
|
|
29435
|
+
"session:pty:resize": ADMIN_ANY,
|
|
29436
|
+
// file-sharing attachment.* RPC:dispatcher 用 admin-only 兜底(wire-level 拦),
|
|
29437
|
+
// 实际只有 owner 能调(personal token 链路 2026-05-21 删除,HTTP Bearer 只识别 owner)
|
|
29438
|
+
"attachment.signUrl": ADMIN_ANY,
|
|
29439
|
+
"attachment.groupAdd": ADMIN_ANY,
|
|
29440
|
+
"attachment.groupRemove": ADMIN_ANY,
|
|
29441
|
+
"attachment.groupList": ADMIN_ANY
|
|
29442
|
+
};
|
|
29443
|
+
function computeGrantForFrame(method, frame) {
|
|
29444
|
+
const rule = METHOD_GRANT_MAP[method];
|
|
29445
|
+
if (!rule) return { kind: "public" };
|
|
29446
|
+
if (rule.kind === "public") return { kind: "public" };
|
|
29447
|
+
if (rule.kind === "capability-scoped") return { kind: "public" };
|
|
29448
|
+
if (rule.kind === "fixed") {
|
|
29449
|
+
return { kind: "check", resource: rule.resource, action: rule.action };
|
|
29450
|
+
}
|
|
29451
|
+
const picked = rule.pick(frame);
|
|
29452
|
+
if (!picked) return { kind: "public" };
|
|
29453
|
+
return { kind: "check", resource: picked.resource, action: picked.action };
|
|
29454
|
+
}
|
|
29455
|
+
|
|
27839
29456
|
// src/index.ts
|
|
27840
29457
|
async function startDaemon(config) {
|
|
27841
29458
|
const logger = createLogger({
|
|
@@ -27851,24 +29468,90 @@ async function startDaemon(config) {
|
|
|
27851
29468
|
if (pre.status === "stale") {
|
|
27852
29469
|
logger.warn("stale state file detected, overwriting", { pid: pre.existing.pid });
|
|
27853
29470
|
}
|
|
29471
|
+
const authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
|
|
27854
29472
|
let resolvedAuthToken = null;
|
|
27855
|
-
let authFile = null;
|
|
27856
29473
|
if (config.authToken && config.authToken.trim()) {
|
|
27857
29474
|
resolvedAuthToken = config.authToken.trim();
|
|
27858
29475
|
} else if (config.tunnel) {
|
|
27859
|
-
authFile = loadOrCreateAuthFile({ dataDir: config.dataDir });
|
|
27860
29476
|
resolvedAuthToken = authFile.token;
|
|
27861
29477
|
}
|
|
29478
|
+
const ownerPrincipalId = authFile.ownerPrincipalId;
|
|
29479
|
+
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
27862
29480
|
const authMode = resolvedAuthToken == null ? "none" : "first-message";
|
|
29481
|
+
migrateLegacyFiles(config.dataDir);
|
|
29482
|
+
const inboxStore = new InboxStore(config.dataDir);
|
|
29483
|
+
const inboxManager = new InboxManager(inboxStore, (_peerOwnerId, frame) => {
|
|
29484
|
+
wsServer?.broadcastToOwners(frame);
|
|
29485
|
+
});
|
|
29486
|
+
const receivedCapabilityStore = new ReceivedCapabilityStore(config.dataDir);
|
|
29487
|
+
receivedCapabilityStore.load();
|
|
29488
|
+
const capabilityStore = new CapabilityStore(config.dataDir);
|
|
29489
|
+
const capabilityRegistry = new CapabilityRegistry(capabilityStore);
|
|
29490
|
+
const capabilityManager = new CapabilityManager(capabilityRegistry, {
|
|
29491
|
+
onIssued: (cap, token) => {
|
|
29492
|
+
wsServer?.broadcastToOwners({
|
|
29493
|
+
type: "capability:tokenIssued",
|
|
29494
|
+
capability: stripSecretHash(cap),
|
|
29495
|
+
token
|
|
29496
|
+
});
|
|
29497
|
+
},
|
|
29498
|
+
onDeleted: (cap) => {
|
|
29499
|
+
const deletedAt = Date.now();
|
|
29500
|
+
wsServer?.broadcastToOwners({
|
|
29501
|
+
type: "capability:tokenDeleted",
|
|
29502
|
+
capabilityId: cap.id,
|
|
29503
|
+
deletedAt
|
|
29504
|
+
});
|
|
29505
|
+
wsServer?.closeConnectionsByCapability(cap.id);
|
|
29506
|
+
const cleanup = cleanupGuestSessionsForCapability(cap, sessionStoreFactory);
|
|
29507
|
+
if (cleanup.removed.length > 0) {
|
|
29508
|
+
logger.info("capability delete cascade: guest sessions removed", {
|
|
29509
|
+
capabilityId: cap.id,
|
|
29510
|
+
removedDirs: cleanup.removed
|
|
29511
|
+
});
|
|
29512
|
+
}
|
|
29513
|
+
}
|
|
29514
|
+
});
|
|
29515
|
+
migrateDropPersonsDir(config.dataDir);
|
|
27863
29516
|
let wsServer = null;
|
|
27864
29517
|
const authGate = authMode === "first-message" ? new AuthGate({
|
|
27865
29518
|
shouldEnforce: buildShouldEnforce({ tunnel: config.tunnel }),
|
|
29519
|
+
// Task 1.7:authenticate 注入路径替代 expectedToken 单 token 比对。
|
|
29520
|
+
// owner 路径 constantTimeEqual 防侧信道;guest 路径走 capabilityRegistry.
|
|
27866
29521
|
expectedToken: resolvedAuthToken,
|
|
29522
|
+
authenticate: (t, selfPrincipalId, selfDisplayName) => {
|
|
29523
|
+
return authenticate(t, {
|
|
29524
|
+
isOwnerToken: (x) => resolvedAuthToken != null && constantTimeEqual(x, resolvedAuthToken),
|
|
29525
|
+
ownerPrincipalId,
|
|
29526
|
+
ownerDisplayName,
|
|
29527
|
+
capabilityRegistry,
|
|
29528
|
+
selfPrincipalId,
|
|
29529
|
+
selfDisplayName,
|
|
29530
|
+
onGuestHello: (capId, ownerId, name) => {
|
|
29531
|
+
capabilityManager.recordPeerHello(capId, {
|
|
29532
|
+
ownerPrincipalId: ownerId,
|
|
29533
|
+
displayName: name,
|
|
29534
|
+
now: Date.now()
|
|
29535
|
+
});
|
|
29536
|
+
}
|
|
29537
|
+
});
|
|
29538
|
+
},
|
|
29539
|
+
onAuthed: (h, ctx) => wsServer?.attachClientContext(h.id, ctx),
|
|
29540
|
+
buildOwnerContext: () => ownerContext(ownerPrincipalId, ownerDisplayName),
|
|
27867
29541
|
closeConnection: (h, code, reason) => wsServer?.closeClient(h.id, code, reason),
|
|
27868
29542
|
sendOk: (h, payload) => wsServer?.sendToClient(h.id, payload)
|
|
27869
29543
|
}) : null;
|
|
27870
29544
|
resetRegistry();
|
|
27871
|
-
const
|
|
29545
|
+
const migrateResult = migrateFlattenSessions({ dataDir: config.dataDir });
|
|
29546
|
+
if (!migrateResult.skipped && (migrateResult.movedBare || migrateResult.movedVmOwner || migrateResult.archivedListener)) {
|
|
29547
|
+
logger.info("sessions migration applied", {
|
|
29548
|
+
movedBare: migrateResult.movedBare,
|
|
29549
|
+
movedVmOwner: migrateResult.movedVmOwner,
|
|
29550
|
+
archivedListener: migrateResult.archivedListener
|
|
29551
|
+
});
|
|
29552
|
+
}
|
|
29553
|
+
const sessionStoreFactory = new SessionStoreFactory({ dataDir: config.dataDir });
|
|
29554
|
+
const store = sessionStoreFactory.forBare();
|
|
27872
29555
|
const workspace = new WorkspaceBrowser();
|
|
27873
29556
|
const skills = new SkillsScanner();
|
|
27874
29557
|
const agents = new AgentsScanner();
|
|
@@ -27881,10 +29564,12 @@ async function startDaemon(config) {
|
|
|
27881
29564
|
} else {
|
|
27882
29565
|
logger.warn("persona.seed.skip", { reason: "defaults-root-not-found" });
|
|
27883
29566
|
}
|
|
27884
|
-
const ownerDisplayName = loadOwnerDisplayName(config.dataDir);
|
|
27885
29567
|
const groupFileStore = new GroupFileStore({ dataDir: config.dataDir, logger });
|
|
27886
29568
|
const manager = new SessionManager({
|
|
27887
29569
|
store,
|
|
29570
|
+
// Phase 2 (capability platform plan §1): factory 注入后 manager.storeFor 走
|
|
29571
|
+
// 新布局派生 (sessions/* + personas/<pid>/.clawd/sessions/owner/*)
|
|
29572
|
+
storeFactory: sessionStoreFactory,
|
|
27888
29573
|
logger,
|
|
27889
29574
|
getAdapter,
|
|
27890
29575
|
historyReader: history,
|
|
@@ -27892,6 +29577,7 @@ async function startDaemon(config) {
|
|
|
27892
29577
|
personaRoot: import_node_path27.default.join(config.dataDir, "personas"),
|
|
27893
29578
|
personaStore,
|
|
27894
29579
|
ownerDisplayName,
|
|
29580
|
+
ownerPrincipalId,
|
|
27895
29581
|
mode: config.mode,
|
|
27896
29582
|
broadcastFrame: (frame, target) => {
|
|
27897
29583
|
if (target === "all") {
|
|
@@ -28027,18 +29713,37 @@ async function startDaemon(config) {
|
|
|
28027
29713
|
// group RPC + sign 都用:根据 sessionId 反查 scope。owner-mode persona session 走
|
|
28028
29714
|
// 'persona/<pid>/owner',default 走 'default'。
|
|
28029
29715
|
getSessionScope: (sid) => manager.findOwnedSessionScope(sid)
|
|
28030
|
-
}
|
|
29716
|
+
},
|
|
29717
|
+
// Task 1.9: capability:issue/list/revoke handler 依赖
|
|
29718
|
+
capabilityManager,
|
|
29719
|
+
// v2 Phase 5: capability:issue 返回的 shareUrl 用此 base URL 拼接。
|
|
29720
|
+
// tunnel 拉起后切到反代 wss://... 地址;否则本机 ws://host:port。
|
|
29721
|
+
getShareBaseUrl: () => currentTunnelUrl ?? `ws://${config.host}:${config.port}`,
|
|
29722
|
+
// v2 Phase 6: whoami handler 装在 owner principal.displayName + persona 解析
|
|
29723
|
+
ownerDisplayName,
|
|
29724
|
+
// owner-id stabilization: whoami 用稳定 ownerPrincipalId 替代 'owner' 字面量
|
|
29725
|
+
ownerPrincipalId,
|
|
29726
|
+
personaStore,
|
|
29727
|
+
// capability handler 也用 (capability:issue 走 registry / capability:list)
|
|
29728
|
+
capabilityRegistry,
|
|
29729
|
+
// capability platform v3 inbox: handler 接 manager (post/list/markRead),
|
|
29730
|
+
// cascade 接 store (list 统计 deletedInboxEvents).
|
|
29731
|
+
inboxManager,
|
|
29732
|
+
inboxStore,
|
|
29733
|
+
// bidirectional cap 视角 B: 对方颁给我的 cap 走这个 store
|
|
29734
|
+
receivedCapabilityStore,
|
|
29735
|
+
// received-capability:added / removed broadcast;复用 capability:tokenIssued 同款通路
|
|
29736
|
+
broadcastToOwners: (frame) => wsServer?.broadcastToOwners(frame),
|
|
29737
|
+
// received-capability:add 临时 ws 连远端跑 whoami — 直连 ws (不走 transport listener)
|
|
29738
|
+
connectRemote
|
|
28031
29739
|
});
|
|
28032
29740
|
const authResolver = new AuthContextResolver({
|
|
28033
|
-
ownerToken: resolvedAuthToken
|
|
28034
|
-
personaRegistry
|
|
29741
|
+
ownerToken: resolvedAuthToken
|
|
28035
29742
|
});
|
|
28036
29743
|
const httpRouter = createHttpRouter({
|
|
28037
29744
|
authResolver,
|
|
28038
29745
|
daemonVersion: version,
|
|
28039
29746
|
logger,
|
|
28040
|
-
personaStore,
|
|
28041
|
-
groupFileStore,
|
|
28042
29747
|
sessionStore: store,
|
|
28043
29748
|
// /files HMAC verify 用 auth.json 的 signSecret 字段(与 attachment.signUrl 同源)。
|
|
28044
29749
|
// --auth-token CLI 模式没 signSecret → 路由返 501,sign URL 功能整体禁用。
|
|
@@ -28048,6 +29753,8 @@ async function startDaemon(config) {
|
|
|
28048
29753
|
host: config.host,
|
|
28049
29754
|
port: config.port,
|
|
28050
29755
|
logger,
|
|
29756
|
+
// broadcastToPrincipal 用此 id 路由 owner-targeted 帧(替代字面量 'owner' sentinel)
|
|
29757
|
+
ownerPrincipalId,
|
|
28051
29758
|
readyFrameBuilder: (ctx) => buildReadyFrame(
|
|
28052
29759
|
{
|
|
28053
29760
|
manager,
|
|
@@ -28063,6 +29770,29 @@ async function startDaemon(config) {
|
|
|
28063
29770
|
),
|
|
28064
29771
|
protocolVersion: PROTOCOL_VERSION,
|
|
28065
29772
|
authGate: authGate ?? void 0,
|
|
29773
|
+
// noAuth 模式下仍验 capability token: 修 capability platform 漏洞 — 远端 client
|
|
29774
|
+
// 用 cap token 连 noAuth daemon 时若无此 hook 会 fallback owner ctx, whoami 错返
|
|
29775
|
+
// capability.id = ownerPrincipalId, 导致 myCapabilityId 写错 → inbox channel 写错.
|
|
29776
|
+
// 命中 cap → attach guest ctx; 空 token / 未命中 → 保持 noAuth 行为 (handler fallback owner).
|
|
29777
|
+
tryVerifyCapabilityToken: (token, selfPrincipalId, selfDisplayName) => {
|
|
29778
|
+
const v2 = capabilityRegistry.verifyToken(token);
|
|
29779
|
+
if (!v2.ok) return null;
|
|
29780
|
+
if (selfPrincipalId && selfDisplayName) {
|
|
29781
|
+
try {
|
|
29782
|
+
capabilityManager.recordPeerHello(v2.capability.id, {
|
|
29783
|
+
ownerPrincipalId: selfPrincipalId,
|
|
29784
|
+
displayName: selfDisplayName,
|
|
29785
|
+
now: Date.now()
|
|
29786
|
+
});
|
|
29787
|
+
} catch {
|
|
29788
|
+
}
|
|
29789
|
+
}
|
|
29790
|
+
return {
|
|
29791
|
+
principal: { id: v2.capability.id, kind: "guest", displayName: v2.capability.displayName },
|
|
29792
|
+
grants: v2.capability.grants,
|
|
29793
|
+
capabilityId: v2.capability.id
|
|
29794
|
+
};
|
|
29795
|
+
},
|
|
28066
29796
|
// file-sharing HTTP 路由复用 daemon 同端口(spec §5 第 3 条);router 自己处理 auth + 404
|
|
28067
29797
|
httpRequestHandler: httpRouter,
|
|
28068
29798
|
// 订阅成功后给该 client 重放 in-flight pendingQuestions(plan: clawd-question-server-truth)。
|
|
@@ -28115,7 +29845,18 @@ async function startDaemon(config) {
|
|
|
28115
29845
|
const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
|
|
28116
29846
|
const handler = handlers[type];
|
|
28117
29847
|
if (!handler) throw new ClawdError(ERROR_CODES.METHOD_NOT_IMPLEMENTED, `not implemented: ${type}`);
|
|
28118
|
-
const
|
|
29848
|
+
const ctx = wsServer.getClientContext(client.id) ?? ownerContext(ownerPrincipalId, ownerDisplayName);
|
|
29849
|
+
const verdict = computeGrantForFrame(type, frame);
|
|
29850
|
+
if (verdict.kind === "check") {
|
|
29851
|
+
const ok = assertGrant(ctx.grants, verdict.resource, verdict.action);
|
|
29852
|
+
if (!ok) {
|
|
29853
|
+
throw new ClawdError(
|
|
29854
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
29855
|
+
`principal ${ctx.principal.kind}:${ctx.principal.id} cannot ${verdict.action} on ${verdict.resource.type}${"id" in verdict.resource ? ":" + verdict.resource.id : ""}`
|
|
29856
|
+
);
|
|
29857
|
+
}
|
|
29858
|
+
}
|
|
29859
|
+
const result = await handler(frame, client, ctx);
|
|
28119
29860
|
if (requestId && result.response) {
|
|
28120
29861
|
client.send({ ...result.response, requestId });
|
|
28121
29862
|
}
|
|
@@ -28238,6 +29979,13 @@ ${bar}
|
|
|
28238
29979
|
manager
|
|
28239
29980
|
};
|
|
28240
29981
|
}
|
|
29982
|
+
function migrateDropPersonsDir(dataDir) {
|
|
29983
|
+
const dir = import_node_path27.default.join(dataDir, "persons");
|
|
29984
|
+
try {
|
|
29985
|
+
import_node_fs24.default.rmSync(dir, { recursive: true, force: true });
|
|
29986
|
+
} catch {
|
|
29987
|
+
}
|
|
29988
|
+
}
|
|
28241
29989
|
|
|
28242
29990
|
// src/cli.ts
|
|
28243
29991
|
async function main() {
|