@topolo/cli 0.13.8 → 0.13.10
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/README.md +34 -5
- package/agent-packs/README.md +98 -0
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/src/topolo_agent_pack/approvals.py +22 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +37 -4
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +749 -0
- package/agent-packs/src/topolo_agent_pack/live.py +914 -16
- package/agent-packs/src/topolo_agent_pack/policy.py +6 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +200 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +42 -3
- package/dist/claude-pack-backend-RMSOPHVQ.js +2380 -0
- package/dist/index.js +270 -66
- package/package.json +5 -5
- package/dist/claude-pack-backend-3YJNKMYD.js +0 -1207
|
@@ -0,0 +1,2380 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/claude-pack-backend.ts
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
5
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
6
|
+
import * as filesystem from "fs";
|
|
7
|
+
import * as cryptography from "crypto";
|
|
8
|
+
import * as paths from "path";
|
|
9
|
+
import { spawn, spawnSync } from "child_process";
|
|
10
|
+
import { dirname, resolve, sep } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { createConnection } from "net";
|
|
13
|
+
import { WebSocket } from "undici";
|
|
14
|
+
|
|
15
|
+
// src/lib/claude-native-pack.ts
|
|
16
|
+
async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoint, verifyContext = () => {
|
|
17
|
+
}) {
|
|
18
|
+
const fenceKey = /* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1");
|
|
19
|
+
const previous = manager[fenceKey];
|
|
20
|
+
const operationId = plan.operationId ?? "legacy-configuration";
|
|
21
|
+
if (apply && previous) throw new Error("Native operation is unresolved; reconcile its journal before another mutation.");
|
|
22
|
+
if (plan.mode === "recover" && previous && (previous.operationId !== operationId || previous.running))
|
|
23
|
+
throw new Error("Native recovery identity differs or the operation is still running.");
|
|
24
|
+
if (plan.noEffects && (plan.mode !== "recover" || previous?.effectAttempted)) throw new Error("No-effect recovery is not qualified.");
|
|
25
|
+
if (apply) manager[fenceKey] = { operationId, running: true, effectAttempted: false };
|
|
26
|
+
try {
|
|
27
|
+
const incremental = (plan.mode ?? "configure") === "configure";
|
|
28
|
+
verifyContext(incremental ? [] : void 0);
|
|
29
|
+
const checkDeadline = () => {
|
|
30
|
+
if (Date.now() >= plan.deadline) throw new Error("Native operation deadline expired; reconcile journal before retry.");
|
|
31
|
+
};
|
|
32
|
+
const command = async (value) => {
|
|
33
|
+
checkDeadline();
|
|
34
|
+
const result = await sidebar(value);
|
|
35
|
+
if (!result.ok) throw new Error(result.error || "Native sidebar acknowledgement refused.");
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
if (!["configure", "switch", "recover"].includes(plan.mode ?? "configure") || plan.mode === "recover" && apply || plan.outgoing?.length && !["switch", "recover"].includes(plan.mode ?? "configure"))
|
|
39
|
+
throw new Error("Unqualified native operation mode.");
|
|
40
|
+
if (plan.mode === "switch" || plan.mode === "recover") {
|
|
41
|
+
if (typeof manager.ensureArchivedSessionsLoaded !== "function") throw new Error("Archived ownership inventory is unavailable.");
|
|
42
|
+
await manager.ensureArchivedSessionsLoaded("ownership");
|
|
43
|
+
}
|
|
44
|
+
if (!plan.members.length || plan.members.length > 100 || (plan.outgoing?.length ?? 0) > 100 || new Set(plan.members.map((m) => m.sessionId)).size !== plan.members.length || new Set(plan.members.map((m) => m.key)).size !== plan.members.length)
|
|
45
|
+
throw new Error("A unique owned roster of 1\u2013100 sessions is required.");
|
|
46
|
+
const models = manager.getAvailableCodeModels();
|
|
47
|
+
const groups = manager.listCustomGroups();
|
|
48
|
+
if (!groups) throw new Error("Native sidebar groups have not been reported by the running window.");
|
|
49
|
+
for (const member of plan.members) {
|
|
50
|
+
if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(member.sessionId) || !plan.noEffects && (!["claude-opus-5-5", "claude-sonnet-5-5"].includes(member.model) || !["medium", "high"].includes(member.effort) || !["default", "acceptEdits", "bypassPermissions"].includes(member.permissionMode) || member.permissionMode === "bypassPermissions" && (!member.permissionConsent?.contextKey.startsWith("org:") || !member.permissionConsent.intakeKey)))
|
|
51
|
+
throw new Error("Unqualified session settings in plan.");
|
|
52
|
+
if (!plan.noEffects && !models.includes(member.model)) throw new Error("Requested model is absent from the native account catalog: " + member.model);
|
|
53
|
+
if (!plan.noEffects && member.group !== null && groups.filter((g) => g.name === member.group).length > 1)
|
|
54
|
+
throw new Error("Ambiguous native sidebar group: " + member.group);
|
|
55
|
+
}
|
|
56
|
+
const nativeDescriptor = async (member) => {
|
|
57
|
+
if (typeof manager.getSessionWhenLoaded !== "function") throw new Error("Qualified native session descriptor API is unavailable.");
|
|
58
|
+
const descriptor = await manager.getSessionWhenLoaded(member.sessionId, "topolo-agent-pack", { warnOnMiss: false });
|
|
59
|
+
checkDeadline();
|
|
60
|
+
if (!descriptor || descriptor.sessionId !== member.sessionId || descriptor.cwd !== member.cwd || descriptor.title !== member.title)
|
|
61
|
+
throw new Error("Owned native session descriptor identity changed: " + member.key);
|
|
62
|
+
return descriptor;
|
|
63
|
+
};
|
|
64
|
+
const snapshot = async (members = plan.members) => {
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const member of members) {
|
|
67
|
+
checkDeadline();
|
|
68
|
+
let session = await manager.getSession(member.sessionId);
|
|
69
|
+
if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || session.isArchived && !["switch", "recover"].includes(plan.mode ?? "configure"))
|
|
70
|
+
throw new Error("Owned session identity or active membership changed: " + member.key);
|
|
71
|
+
let descriptor;
|
|
72
|
+
if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length))
|
|
73
|
+
throw new Error("Session has live or pending work; no changes permitted: " + member.key);
|
|
74
|
+
if (!plan.noEffects) {
|
|
75
|
+
descriptor = await nativeDescriptor(member);
|
|
76
|
+
session = await manager.getSession(member.sessionId);
|
|
77
|
+
checkDeadline();
|
|
78
|
+
if (!session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title || (session.isArchived || descriptor.isArchived) && incremental) throw new Error("Owned session identity or active membership changed: " + member.key);
|
|
79
|
+
if (!incremental && (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length || descriptor.isRunning || descriptor.starting || descriptor.pendingToolPermissions?.length))
|
|
80
|
+
throw new Error("Session has live or pending work; no changes permitted: " + member.key);
|
|
81
|
+
if (descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || descriptor.spawnedFrom)
|
|
82
|
+
throw new Error("Incoming session is not a qualified local role.");
|
|
83
|
+
}
|
|
84
|
+
const busyReasons = [
|
|
85
|
+
...manager.hasLosableWork(member.sessionId) ? ["losable"] : [],
|
|
86
|
+
...session.isRunning || descriptor?.isRunning ? ["running"] : [],
|
|
87
|
+
...session.starting || descriptor?.starting ? ["starting"] : [],
|
|
88
|
+
...session.pendingToolPermissions?.length || descriptor?.pendingToolPermissions?.length ? ["pending-permissions"] : []
|
|
89
|
+
];
|
|
90
|
+
result.push({
|
|
91
|
+
key: member.key,
|
|
92
|
+
sessionId: session.sessionId,
|
|
93
|
+
cwd: session.cwd,
|
|
94
|
+
title: session.title,
|
|
95
|
+
group: manager.getSessionCustomGroup(member.sessionId)?.name ?? null,
|
|
96
|
+
pinned: session.isStarred === true,
|
|
97
|
+
model: session.model ?? null,
|
|
98
|
+
effort: session.effort ?? null,
|
|
99
|
+
permissionMode: session.permissionMode ?? null,
|
|
100
|
+
archived: session.isArchived === true,
|
|
101
|
+
...incremental ? { busy: busyReasons.length > 0, busyReasons } : {}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
const before = await snapshot();
|
|
107
|
+
const outgoing = plan.outgoing ?? [];
|
|
108
|
+
const identities = [...plan.members, ...outgoing].map((m) => m.sessionId);
|
|
109
|
+
if (new Set(identities).size !== identities.length) throw new Error("Incoming and outgoing native identities overlap.");
|
|
110
|
+
const outgoingSnapshot = async (members = outgoing) => {
|
|
111
|
+
const result = [];
|
|
112
|
+
for (const member of members) {
|
|
113
|
+
checkDeadline();
|
|
114
|
+
const session = await manager.getSession(member.sessionId);
|
|
115
|
+
if (!/^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(member.sessionId) || !session || session.sessionId !== member.sessionId || session.cwd !== member.cwd || session.title !== member.title)
|
|
116
|
+
throw new Error("Outgoing native identity differs.");
|
|
117
|
+
if (manager.hasLosableWork(member.sessionId) || session.isRunning || session.starting || session.pendingToolPermissions?.length)
|
|
118
|
+
throw new Error("Outgoing session has live or pending work.");
|
|
119
|
+
if (!plan.noEffects) {
|
|
120
|
+
const descriptor = await nativeDescriptor(member);
|
|
121
|
+
if (manager.hasLosableWork(member.sessionId) || descriptor.isRunning || descriptor.starting || descriptor.pendingToolPermissions?.length)
|
|
122
|
+
throw new Error("Outgoing session has live or pending work.");
|
|
123
|
+
if (descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || descriptor.spawnedFrom || !manager.archiveCascadeClosureOf || manager.archiveCascadeClosureOf(descriptor).length)
|
|
124
|
+
throw new Error("Outgoing native archive closure is not qualified.");
|
|
125
|
+
}
|
|
126
|
+
result.push({ key: member.key, sessionId: member.sessionId, cwd: session.cwd, title: session.title, archived: session.isArchived === true });
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
};
|
|
130
|
+
const outgoingBefore = await outgoingSnapshot();
|
|
131
|
+
const sections = await command({ kind: "list_sections" });
|
|
132
|
+
checkpoint({ kind: "baseline", before, outgoingBefore, sections: sections.sections, at: Date.now() });
|
|
133
|
+
if (incremental) {
|
|
134
|
+
const settings = ["group", "pinned", "model", "effort", "permissionMode"];
|
|
135
|
+
const idleBefore = plan.members.filter((_, i) => !before[i].busy);
|
|
136
|
+
verifyContext([]);
|
|
137
|
+
if (!apply) return {
|
|
138
|
+
state: "preflight",
|
|
139
|
+
before,
|
|
140
|
+
outgoingBefore,
|
|
141
|
+
sections: sections.sections,
|
|
142
|
+
availableModels: models,
|
|
143
|
+
applied: false,
|
|
144
|
+
contextVerifiedSessionIds: idleBefore.map((m) => m.sessionId)
|
|
145
|
+
};
|
|
146
|
+
const appliedFields = [];
|
|
147
|
+
const deferredReasons = /* @__PURE__ */ new Map();
|
|
148
|
+
const differences = (member, row) => settings.filter((field) => member[field] !== row[field]);
|
|
149
|
+
for (const member of plan.members) {
|
|
150
|
+
const initial = (await snapshot([member]))[0];
|
|
151
|
+
if (initial.busy) {
|
|
152
|
+
deferredReasons.set(member.sessionId, initial.busyReasons);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
for (const setting of differences(member, initial)) {
|
|
156
|
+
let row = (await snapshot([member]))[0];
|
|
157
|
+
if (row.busy) {
|
|
158
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
if (row[setting] === member[setting]) continue;
|
|
162
|
+
verifyContext([member]);
|
|
163
|
+
if (setting === "group" && member.group !== null) {
|
|
164
|
+
let matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
165
|
+
if (!matching || matching.length > 1) throw new Error("Ambiguous native sidebar group: " + member.group);
|
|
166
|
+
if (!matching.length) {
|
|
167
|
+
row = (await snapshot([member]))[0];
|
|
168
|
+
if (row.busy) {
|
|
169
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
verifyContext([member]);
|
|
173
|
+
checkDeadline();
|
|
174
|
+
checkpoint({ kind: "attempt", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
|
|
175
|
+
manager[fenceKey].effectAttempted = true;
|
|
176
|
+
const created = await command({ kind: "create_group", name: member.group });
|
|
177
|
+
checkpoint({ kind: "acknowledged", operation: "create_group", key: member.key, name: member.group, at: Date.now() });
|
|
178
|
+
matching = manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
179
|
+
if (!created.groupId) throw new Error("Native create_group did not return an identity.");
|
|
180
|
+
if (matching?.length !== 1 || matching[0].id !== created.groupId)
|
|
181
|
+
throw new Error("Native group read-back mismatch.");
|
|
182
|
+
verifyContext([member]);
|
|
183
|
+
appliedFields.push({ key: member.key, operation: "create_group", name: member.group });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
row = (await snapshot([member]))[0];
|
|
187
|
+
if (row.busy) {
|
|
188
|
+
deferredReasons.set(member.sessionId, row.busyReasons);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
if (row[setting] === member[setting]) continue;
|
|
192
|
+
verifyContext([member]);
|
|
193
|
+
checkDeadline();
|
|
194
|
+
if (setting === "model" && !manager.getAvailableCodeModels().includes(member.model))
|
|
195
|
+
throw new Error("Requested model is absent from the native account catalog: " + member.model);
|
|
196
|
+
const operation = setting === "group" ? "move_sessions" : setting === "pinned" ? "set_pinned" : setting === "model" ? "set_model" : setting === "effort" ? "set_effort" : "set_permission_mode";
|
|
197
|
+
checkpoint({ kind: "attempt", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
198
|
+
manager[fenceKey].effectAttempted = true;
|
|
199
|
+
if (setting === "group") {
|
|
200
|
+
const matching = member.group === null ? [] : manager.listCustomGroups()?.filter((g) => g.name === member.group);
|
|
201
|
+
if (!matching || member.group !== null && matching.length !== 1) throw new Error("Native group identity changed.");
|
|
202
|
+
await command({
|
|
203
|
+
kind: "move_sessions",
|
|
204
|
+
sessionIds: [member.sessionId],
|
|
205
|
+
...member.group === null ? {} : { groupId: matching[0].id }
|
|
206
|
+
});
|
|
207
|
+
} else if (setting === "pinned") await command({ kind: "set_pinned", sessionId: member.sessionId, pinned: member.pinned });
|
|
208
|
+
else if (setting === "model") await manager.setModel(member.sessionId, member.model);
|
|
209
|
+
else if (setting === "effort") await manager.setEffort(member.sessionId, member.effort);
|
|
210
|
+
else if (!await manager.setPermissionMode(member.sessionId, member.permissionMode, "picker"))
|
|
211
|
+
throw new Error("Native permission mode was refused: " + member.key);
|
|
212
|
+
checkpoint({ kind: "acknowledged", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
213
|
+
const actual = (await snapshot([member]))[0];
|
|
214
|
+
if (actual[setting] !== member[setting]) throw new Error("Native read-back mismatch: " + member.key + " / " + setting);
|
|
215
|
+
verifyContext([member]);
|
|
216
|
+
appliedFields.push({ key: member.key, sessionId: member.sessionId, setting, value: member[setting] });
|
|
217
|
+
checkpoint({ kind: "verified-field", operation, setting, key: member.key, sessionId: member.sessionId, at: Date.now() });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const after2 = await snapshot();
|
|
221
|
+
const idleAfter = plan.members.filter((member, i) => !after2[i].busy && !deferredReasons.has(member.sessionId));
|
|
222
|
+
verifyContext(idleAfter);
|
|
223
|
+
const deferred = plan.members.flatMap((member, i) => {
|
|
224
|
+
const row = after2[i], fields = differences(member, row);
|
|
225
|
+
if (!fields.length) return [];
|
|
226
|
+
const reasons = deferredReasons.get(member.sessionId) ?? row.busyReasons;
|
|
227
|
+
if (!reasons?.length) throw new Error("Native read-back mismatch: " + member.key + " / " + fields.join(","));
|
|
228
|
+
return [{ key: member.key, sessionId: member.sessionId, fields, reasons }];
|
|
229
|
+
});
|
|
230
|
+
const attestedBusyKeys = after2.filter((row) => row.busy && !deferred.some((d) => d.sessionId === row.sessionId)).map((row) => row.key);
|
|
231
|
+
checkpoint({ kind: deferred.length ? "partial-verified" : "verified", after: after2, appliedFields, deferred, attestedBusyKeys, at: Date.now() });
|
|
232
|
+
delete manager[fenceKey];
|
|
233
|
+
return {
|
|
234
|
+
state: deferred.length ? "native-settings-partial" : "native-settings-verified",
|
|
235
|
+
before,
|
|
236
|
+
after: after2,
|
|
237
|
+
outgoingAfter: [],
|
|
238
|
+
verifiedAgents: after2.length - deferred.length,
|
|
239
|
+
complete: deferred.length === 0,
|
|
240
|
+
applied: appliedFields.length > 0,
|
|
241
|
+
appliedFields,
|
|
242
|
+
deferred,
|
|
243
|
+
attestedBusyKeys,
|
|
244
|
+
contextVerifiedSessionIds: idleAfter.map((m) => m.sessionId),
|
|
245
|
+
busyObservations: [...deferredReasons].map(([sessionId, reasons]) => ({ key: plan.members.find((m) => m.sessionId === sessionId).key, sessionId, reasons })),
|
|
246
|
+
modelInferenceVerified: false,
|
|
247
|
+
readOnlyConfinementVerified: false,
|
|
248
|
+
fullPackSwitchSupported: false
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (plan.mode === "recover") {
|
|
252
|
+
if (plan.noEffects) {
|
|
253
|
+
verifyContext();
|
|
254
|
+
checkpoint({ kind: "reconciled-no-effects", after: before, at: Date.now() });
|
|
255
|
+
delete manager[fenceKey];
|
|
256
|
+
return { state: "native-reconciled-no-effects", after: before, applied: false, nativeEffectsReplayed: false };
|
|
257
|
+
}
|
|
258
|
+
for (let i = 0; i < plan.members.length; i++) {
|
|
259
|
+
const wanted = plan.members[i];
|
|
260
|
+
const actual = before[i];
|
|
261
|
+
for (const setting of ["group", "pinned", "model", "effort", "permissionMode"])
|
|
262
|
+
if (actual[setting] !== wanted[setting]) throw new Error("Recovery read-back is incomplete; no native effects replayed.");
|
|
263
|
+
if (actual.archived) throw new Error("Recovery incoming session is still archived.");
|
|
264
|
+
}
|
|
265
|
+
if (outgoingBefore.some((s) => !s.archived)) throw new Error("Recovery outgoing roster is incomplete.");
|
|
266
|
+
verifyContext();
|
|
267
|
+
checkpoint({ kind: "reconciled", after: before, outgoingAfter: outgoingBefore, at: Date.now() });
|
|
268
|
+
delete manager[fenceKey];
|
|
269
|
+
return { state: "native-reconciled", after: before, outgoingAfter: outgoingBefore, applied: false, nativeEffectsReplayed: false };
|
|
270
|
+
}
|
|
271
|
+
if (!apply) return { state: "preflight", before, outgoingBefore, sections: sections.sections, availableModels: models, applied: false };
|
|
272
|
+
const effect = async (description, run) => {
|
|
273
|
+
checkDeadline();
|
|
274
|
+
verifyContext();
|
|
275
|
+
const affected = description.sessionIds ?? (description.sessionId ? [description.sessionId] : null);
|
|
276
|
+
await snapshot(affected ? plan.members.filter((m) => affected.includes(m.sessionId)) : plan.members);
|
|
277
|
+
await outgoingSnapshot(affected ? outgoing.filter((m) => affected.includes(m.sessionId)) : outgoing);
|
|
278
|
+
checkpoint({ kind: "attempt", ...description, at: Date.now() });
|
|
279
|
+
manager[fenceKey].effectAttempted = true;
|
|
280
|
+
await run();
|
|
281
|
+
checkpoint({ kind: "acknowledged", ...description, at: Date.now() });
|
|
282
|
+
};
|
|
283
|
+
const groupIds = new Map(groups.map((g) => [g.name, g.id]));
|
|
284
|
+
if (plan.mode === "switch") {
|
|
285
|
+
for (const member of outgoing) {
|
|
286
|
+
if (!(await manager.getSession(member.sessionId)).isArchived)
|
|
287
|
+
await effect({ operation: "archive", sessionId: member.sessionId }, () => manager.archiveSession(
|
|
288
|
+
member.sessionId,
|
|
289
|
+
{ cleanupWorktree: false, forceWorktreeCleanup: false, discardUntracked: false, isAutomatic: true }
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
for (const member of plan.members) {
|
|
293
|
+
const session = await manager.getSession(member.sessionId);
|
|
294
|
+
if (session.isArchived) {
|
|
295
|
+
const descriptor = await nativeDescriptor(member);
|
|
296
|
+
if (descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || descriptor.spawnedFrom)
|
|
297
|
+
throw new Error("Incoming session is not a qualified local role.");
|
|
298
|
+
await effect({ operation: "unarchive", sessionId: member.sessionId }, async () => {
|
|
299
|
+
await manager.unarchiveSession(member.sessionId);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
for (const name of new Set(plan.members.map((m) => m.group).filter((g) => g !== null))) {
|
|
305
|
+
if (!groupIds.has(name)) await effect({ operation: "create_group", name }, async () => {
|
|
306
|
+
const result = await command({ kind: "create_group", name });
|
|
307
|
+
if (!result.groupId) throw new Error("Native create_group did not return an identity.");
|
|
308
|
+
groupIds.set(name, result.groupId);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
await effect({ operation: "set_view" }, () => command({
|
|
312
|
+
kind: "set_view",
|
|
313
|
+
view: { groupBy: "custom", status: "active", sortBy: "recency", lastActivityDays: 0 }
|
|
314
|
+
}));
|
|
315
|
+
for (const group of new Set(plan.members.map((m) => m.group))) {
|
|
316
|
+
const sessionIds = plan.members.filter((m) => m.group === group).map((m) => m.sessionId);
|
|
317
|
+
await effect({ operation: "move_sessions", group, sessionIds }, () => command({
|
|
318
|
+
kind: "move_sessions",
|
|
319
|
+
sessionIds,
|
|
320
|
+
...group === null ? {} : { groupId: groupIds.get(group) }
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
for (const member of plan.members) {
|
|
324
|
+
await effect({ operation: "set_pinned", sessionId: member.sessionId }, () => command({ kind: "set_pinned", sessionId: member.sessionId, pinned: member.pinned }));
|
|
325
|
+
await effect({ operation: "set_model", sessionId: member.sessionId }, () => manager.setModel(member.sessionId, member.model));
|
|
326
|
+
await effect({ operation: "set_effort", sessionId: member.sessionId }, () => manager.setEffort(member.sessionId, member.effort));
|
|
327
|
+
await effect({ operation: "set_permission_mode", sessionId: member.sessionId }, async () => {
|
|
328
|
+
if (!await manager.setPermissionMode(member.sessionId, member.permissionMode, "picker"))
|
|
329
|
+
throw new Error("Native permission mode was refused: " + member.key);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const after = await snapshot();
|
|
333
|
+
const outgoingAfter = await outgoingSnapshot();
|
|
334
|
+
if (after.some((s) => s.archived) || outgoingAfter.some((s) => !s.archived)) throw new Error("Native archive membership read-back mismatch.");
|
|
335
|
+
for (let i = 0; i < plan.members.length; i++) {
|
|
336
|
+
const wanted = plan.members[i];
|
|
337
|
+
const actual = after[i];
|
|
338
|
+
for (const setting of ["group", "pinned", "model", "effort", "permissionMode"])
|
|
339
|
+
if (actual[setting] !== wanted[setting]) throw new Error("Native read-back mismatch: " + wanted.key + " / " + setting);
|
|
340
|
+
}
|
|
341
|
+
verifyContext();
|
|
342
|
+
checkpoint({ kind: "verified", after, outgoingAfter, at: Date.now() });
|
|
343
|
+
delete manager[fenceKey];
|
|
344
|
+
return {
|
|
345
|
+
state: "native-settings-verified",
|
|
346
|
+
before,
|
|
347
|
+
after,
|
|
348
|
+
outgoingAfter,
|
|
349
|
+
verifiedAgents: after.length,
|
|
350
|
+
applied: true,
|
|
351
|
+
modelInferenceVerified: false,
|
|
352
|
+
readOnlyConfinementVerified: false,
|
|
353
|
+
fullPackSwitchSupported: plan.mode === "switch"
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (apply) manager[fenceKey] = {
|
|
357
|
+
operationId,
|
|
358
|
+
running: false,
|
|
359
|
+
recoveryRequired: true,
|
|
360
|
+
effectAttempted: manager[fenceKey]?.effectAttempted === true
|
|
361
|
+
};
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function nativeClaudeDynamicLaneOperation(manager, plan, operation, checkpoint, verifyContext, qualifyOrganization, verifyHistory) {
|
|
366
|
+
if (!["read", "configure", "retire"].includes(operation) || !/^[a-z0-9][a-z0-9-]*$/.test(plan.laneId) || !/^[a-f0-9]{64}$/.test(plan.planDigest) || !plan.intake.key.includes("/intakes/") || (/* @__PURE__ */ new Set([plan.nativeId, plan.source.sessionId, plan.intake.sessionId])).size !== 3 || !["low", "medium", "high", "max"].includes(plan.effort) || !["default", "acceptEdits", "bypassPermissions"].includes(plan.permissionMode) || ![plan.nativeId, plan.source.sessionId, plan.intake.sessionId].every((id) => /^local_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(id)) || plan.cwd !== plan.source.cwd)
|
|
367
|
+
throw new Error("Exact child, source role and originating intake plan required.");
|
|
368
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-lane.operation.v1");
|
|
369
|
+
if (manager[symbol]) throw new Error("An unresolved child operation retains native custody.");
|
|
370
|
+
const fence = () => {
|
|
371
|
+
if (Date.now() >= plan.deadline) throw new Error("Child operation deadline expired; no retry.");
|
|
372
|
+
verifyContext();
|
|
373
|
+
};
|
|
374
|
+
const fixed = async (role) => {
|
|
375
|
+
const raw = await manager.getSessionWhenLoaded(role.sessionId, "topolo-dynamic-source", { warnOnMiss: false });
|
|
376
|
+
const dto = await manager.getSession(role.sessionId);
|
|
377
|
+
fence();
|
|
378
|
+
if (!raw || !dto || raw.sessionId !== role.sessionId || dto.sessionId !== role.sessionId || raw.cwd !== role.cwd || dto.cwd !== role.cwd || raw.title !== role.title || dto.title !== role.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom)
|
|
379
|
+
throw new Error("Source role or intake raw native identity differs.");
|
|
380
|
+
return { raw, dto };
|
|
381
|
+
};
|
|
382
|
+
const child = async (allowArchived = false) => {
|
|
383
|
+
await fixed(plan.source);
|
|
384
|
+
await fixed(plan.intake);
|
|
385
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-child", { warnOnMiss: false });
|
|
386
|
+
const dto = await manager.getSession(plan.nativeId);
|
|
387
|
+
fence();
|
|
388
|
+
if (!raw || !dto || raw.sessionId !== plan.nativeId || dto.sessionId !== plan.nativeId || raw.cwd !== plan.cwd || dto.cwd !== plan.cwd || raw.title !== plan.title || dto.title !== plan.title || !allowArchived && (raw.isArchived || dto.isArchived) || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom?.sessionId !== plan.intake.sessionId || raw.spawnedFrom?.sideSession !== true)
|
|
389
|
+
throw new Error("Actual child raw descriptor or original native parent differs.");
|
|
390
|
+
const busy = raw.isRunning || raw.starting || dto.isRunning || dto.starting || raw.pendingToolPermissions?.length || dto.pendingToolPermissions?.length || manager.hasLosableWork(plan.nativeId);
|
|
391
|
+
return {
|
|
392
|
+
nativeId: plan.nativeId,
|
|
393
|
+
cwd: dto.cwd,
|
|
394
|
+
model: dto.model,
|
|
395
|
+
effort: dto.effort,
|
|
396
|
+
permissionMode: dto.permissionMode,
|
|
397
|
+
nativeParentId: raw.spawnedFrom.sessionId,
|
|
398
|
+
archived: raw.isArchived === true && dto.isArchived === true,
|
|
399
|
+
busy: !!busy,
|
|
400
|
+
pendingApprovals: dto.pendingToolPermissions?.length ?? 0,
|
|
401
|
+
backend: "local",
|
|
402
|
+
foreignWorkspace: false
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
fence();
|
|
406
|
+
const before = await child();
|
|
407
|
+
if (!manager.getAvailableCodeModels().includes(plan.model)) throw new Error("Exact task model is unavailable; no parent fallback.");
|
|
408
|
+
if (operation === "read") return { state: "native-child-read", ...before, settingsACK: false, contextACK: false };
|
|
409
|
+
if (before.busy) throw new Error("Own child has active or pending work.");
|
|
410
|
+
await qualifyOrganization();
|
|
411
|
+
fence();
|
|
412
|
+
let attempted = false;
|
|
413
|
+
manager[symbol] = { laneId: plan.laneId, planDigest: plan.planDigest, attempted: false };
|
|
414
|
+
try {
|
|
415
|
+
if (operation === "retire") {
|
|
416
|
+
if (plan.lifecycleState !== "settled") throw new Error("Only a settled child with verified operation outcome may retire.");
|
|
417
|
+
const history = await verifyHistory(plan.nativeId);
|
|
418
|
+
if (typeof manager.archiveCascadeClosureOf !== "function" || typeof manager.archiveSession !== "function")
|
|
419
|
+
throw new Error("Qualified preserved-history native archive API unavailable.");
|
|
420
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-retire", { warnOnMiss: false });
|
|
421
|
+
const closure = manager.archiveCascadeClosureOf(raw);
|
|
422
|
+
if (!Array.isArray(closure) || closure.length) throw new Error("Child archive would affect another native session.");
|
|
423
|
+
const current = await child();
|
|
424
|
+
if (current.busy) throw new Error("Child became busy before archive.");
|
|
425
|
+
fence();
|
|
426
|
+
checkpoint({ state: "archive-may-have-effects", laneId: plan.laneId, nativeId: plan.nativeId });
|
|
427
|
+
attempted = true;
|
|
428
|
+
manager[symbol].attempted = true;
|
|
429
|
+
await manager.archiveSession(plan.nativeId, { cleanupWorktree: false, forceWorktreeCleanup: false, discardUntracked: false, isAutomatic: true });
|
|
430
|
+
const after2 = await child(true);
|
|
431
|
+
if (!after2.archived || after2.busy || await verifyHistory(plan.nativeId, history) !== true)
|
|
432
|
+
throw new Error("Native child archive or transcript preservation is unproven.");
|
|
433
|
+
checkpoint({ state: "retired", laneId: plan.laneId, nativeId: plan.nativeId, historyPreserved: true });
|
|
434
|
+
delete manager[symbol];
|
|
435
|
+
return { state: "retired", ...after2, historyPreserved: true };
|
|
436
|
+
}
|
|
437
|
+
for (const field of ["model", "effort", "permissionMode"]) {
|
|
438
|
+
const current = await child();
|
|
439
|
+
if (current.busy) throw new Error("Child became busy before native settings effect.");
|
|
440
|
+
if (current[field] === plan[field]) continue;
|
|
441
|
+
await qualifyOrganization();
|
|
442
|
+
fence();
|
|
443
|
+
if (!manager.getAvailableCodeModels().includes(plan.model)) throw new Error("Native account catalog changed.");
|
|
444
|
+
checkpoint({ state: "settings-may-have-effects", laneId: plan.laneId, nativeId: plan.nativeId, field });
|
|
445
|
+
attempted = true;
|
|
446
|
+
manager[symbol].attempted = true;
|
|
447
|
+
if (field === "model") await manager.setModel(plan.nativeId, plan.model);
|
|
448
|
+
else if (field === "effort") await manager.setEffort(plan.nativeId, plan.effort);
|
|
449
|
+
else if (!await manager.setPermissionMode(plan.nativeId, plan.permissionMode, "picker")) throw new Error("Child permission setting refused.");
|
|
450
|
+
const actual = await child();
|
|
451
|
+
if (actual[field] !== plan[field]) throw new Error("Actual child " + field + " differs; no fallback or task dispatch.");
|
|
452
|
+
checkpoint({ state: "native-field-ACK", laneId: plan.laneId, nativeId: plan.nativeId, field, value: actual[field] });
|
|
453
|
+
}
|
|
454
|
+
const after = await child();
|
|
455
|
+
const processRecord = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-process-settings", { warnOnMiss: false });
|
|
456
|
+
fence();
|
|
457
|
+
if (!processRecord?.query || typeof manager.getEffort !== "function" || typeof manager.readRunningModel !== "function")
|
|
458
|
+
throw new Error("Actual child process settings are unavailable; settings ACK is unproven.");
|
|
459
|
+
const query = processRecord.query;
|
|
460
|
+
const actualModel = await manager.readRunningModel(processRecord, query);
|
|
461
|
+
const actualEffort = await manager.getEffort(plan.nativeId);
|
|
462
|
+
const reread = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-process-settings", { warnOnMiss: false });
|
|
463
|
+
const finalChild = await child();
|
|
464
|
+
if (reread !== processRecord || reread.query !== query || actualModel !== plan.model || actualEffort !== plan.effort || finalChild.busy)
|
|
465
|
+
throw new Error("Actual child process model/effort differs or is unavailable; settings ACK is unproven.");
|
|
466
|
+
await qualifyOrganization();
|
|
467
|
+
fence();
|
|
468
|
+
if (after.model !== plan.model || after.effort !== plan.effort || after.permissionMode !== plan.permissionMode || after.busy)
|
|
469
|
+
throw new Error("Actual child settings are not the complete requested tuple.");
|
|
470
|
+
checkpoint({ state: "configured-settings-ACK", laneId: plan.laneId, nativeId: plan.nativeId, after });
|
|
471
|
+
delete manager[symbol];
|
|
472
|
+
return { state: "configured-settings-ACK", ...after, settingsACK: true, contextACK: false, modelTurnsStarted: false };
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (!attempted) delete manager[symbol];
|
|
475
|
+
else checkpoint({ state: "uncertain-effect-hold", laneId: plan.laneId, nativeId: plan.nativeId });
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function nativeClaudeDynamicContext(manager, plan, operation, helper, fingerprint, verifyContext, qualifyOrganization) {
|
|
480
|
+
if (!["context-start", "context-read"].includes(operation.kind) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(operation.operationId))
|
|
481
|
+
throw new Error("Closed own-child context operation required.");
|
|
482
|
+
const ready = async () => {
|
|
483
|
+
verifyContext();
|
|
484
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-context", { warnOnMiss: false });
|
|
485
|
+
const view = await manager.getSession(plan.nativeId);
|
|
486
|
+
verifyContext();
|
|
487
|
+
if (!raw || !view || raw.sessionId !== plan.nativeId || view.sessionId !== plan.nativeId || raw.cwd !== plan.cwd || view.cwd !== plan.cwd || raw.title !== plan.title || view.title !== plan.title || raw.isArchived || view.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom?.sessionId !== plan.intake.sessionId || raw.spawnedFrom?.sideSession !== true || raw.isRunning || view.isRunning || raw.starting || view.starting || raw.pendingToolPermissions?.length || view.pendingToolPermissions?.length || manager.hasLosableWork(plan.nativeId))
|
|
488
|
+
throw new Error("Exact configured child is not idle on the original native parent.");
|
|
489
|
+
if (!raw.query || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== plan.model || await manager.getEffort(plan.nativeId) !== plan.effort)
|
|
490
|
+
throw new Error("Current actual child process model/effort ACK differs.");
|
|
491
|
+
await qualifyOrganization();
|
|
492
|
+
verifyContext();
|
|
493
|
+
};
|
|
494
|
+
await ready();
|
|
495
|
+
if (typeof manager.getTranscriptForCallerElsewhere !== "function") throw new Error("Qualified own-child transcript resolver unavailable.");
|
|
496
|
+
if (operation.kind === "context-read") {
|
|
497
|
+
const history = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
498
|
+
await ready();
|
|
499
|
+
if (!Array.isArray(history) || !history.length) throw new Error("Own-child context transcript is missing; no acknowledgement.");
|
|
500
|
+
return helper("context-ack", { laneId: plan.laneId, operationId: operation.operationId, messages: history });
|
|
501
|
+
}
|
|
502
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-context.v1");
|
|
503
|
+
if (manager[symbol]) throw new Error("An unresolved child bootstrap owns delivery; no replay.");
|
|
504
|
+
manager[symbol] = { laneId: plan.laneId, operationId: operation.operationId };
|
|
505
|
+
let armed = false;
|
|
506
|
+
try {
|
|
507
|
+
const bootstrap = await helper("context-prepare", { laneId: plan.laneId, operationId: operation.operationId });
|
|
508
|
+
const history = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
509
|
+
if (!Array.isArray(history)) throw new Error("Exact own-child pre-bootstrap history is unavailable.");
|
|
510
|
+
await ready();
|
|
511
|
+
await helper("context-mark", {
|
|
512
|
+
laneId: plan.laneId,
|
|
513
|
+
operationId: operation.operationId,
|
|
514
|
+
state: "start-may-have-effects",
|
|
515
|
+
historyCount: history.length,
|
|
516
|
+
historyDigest: fingerprint(history)
|
|
517
|
+
});
|
|
518
|
+
armed = true;
|
|
519
|
+
const result = await manager.sendMessage(
|
|
520
|
+
plan.nativeId,
|
|
521
|
+
bootstrap.text,
|
|
522
|
+
void 0,
|
|
523
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
524
|
+
);
|
|
525
|
+
const state = result?.delivered === true ? "native-delivered" : result?.delivered === false ? "native-refused" : "uncertain";
|
|
526
|
+
await helper("context-mark", { laneId: plan.laneId, operationId: operation.operationId, state });
|
|
527
|
+
if (state === "uncertain") throw new Error("Child context delivery is uncertain; never replay.");
|
|
528
|
+
delete manager[symbol];
|
|
529
|
+
return { state, laneId: plan.laneId, operationId: operation.operationId, contextACK: false };
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (!armed) delete manager[symbol];
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function nativeClaudeDynamicFixture(manager, plan, operation, helper, verifyContext, qualifyOrganization) {
|
|
536
|
+
if (!["fixture-start", "fixture-read"].includes(operation.kind) || operation.kind === "fixture-start" && !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(operation.operationId ?? ""))
|
|
537
|
+
throw new Error("Closed one-child native fixture operation required.");
|
|
538
|
+
const read = async () => {
|
|
539
|
+
const raw2 = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-fixture", { warnOnMiss: false });
|
|
540
|
+
const dto = await manager.getSession(plan.nativeId);
|
|
541
|
+
verifyContext();
|
|
542
|
+
if (!raw2 || !dto || raw2.sessionId !== plan.nativeId || dto.sessionId !== plan.nativeId || raw2.cwd !== plan.cwd || dto.cwd !== plan.cwd || raw2.title !== plan.title || dto.title !== plan.title || raw2.isArchived || dto.isArchived || raw2.backend?.kind !== "local" || raw2.backend.remoteTarget || raw2.worktreePath || raw2.worktreeLazy || raw2.spawnedFrom?.sessionId !== plan.intake.sessionId || raw2.spawnedFrom?.sideSession !== true) throw new Error("Own native fixture child identity or original intake parent changed.");
|
|
543
|
+
return { raw: raw2, native: {
|
|
544
|
+
nativeId: plan.nativeId,
|
|
545
|
+
busy: !!(raw2.isRunning || raw2.starting || dto.isRunning || dto.starting || manager.hasLosableWork(plan.nativeId)),
|
|
546
|
+
pendingApprovals: dto.pendingToolPermissions?.length ?? 0
|
|
547
|
+
} };
|
|
548
|
+
};
|
|
549
|
+
const { raw, native } = await read();
|
|
550
|
+
if (operation.kind === "fixture-read") {
|
|
551
|
+
if (native.busy || native.pendingApprovals) return { state: "native-fixture-pending", ...native };
|
|
552
|
+
const messages = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
553
|
+
const latest = await read();
|
|
554
|
+
if (latest.native.busy || latest.native.pendingApprovals) return { state: "native-fixture-pending", ...latest.native };
|
|
555
|
+
if (!Array.isArray(messages)) throw new Error("Own native fixture transcript is unavailable.");
|
|
556
|
+
return helper("fixture-outcome", { laneId: plan.laneId, messages, native: latest.native });
|
|
557
|
+
}
|
|
558
|
+
if (native.busy || native.pendingApprovals || !raw.query || await manager.readRunningModel(raw, raw.query) !== plan.model || await manager.getEffort(plan.nativeId) !== plan.effort) throw new Error("Own fixture needs idle child and current actual process tuple ACK.");
|
|
559
|
+
await qualifyOrganization();
|
|
560
|
+
verifyContext();
|
|
561
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-fixture.v1");
|
|
562
|
+
if (manager[symbol]) throw new Error("An unresolved native fixture retains delivery custody.");
|
|
563
|
+
manager[symbol] = { laneId: plan.laneId, operationId: operation.operationId };
|
|
564
|
+
let armed = false;
|
|
565
|
+
try {
|
|
566
|
+
const fixture = await helper("fixture-prepare", { laneId: plan.laneId, operationId: operation.operationId });
|
|
567
|
+
const latest = await read();
|
|
568
|
+
if (latest.native.busy || latest.native.pendingApprovals) throw new Error("Own child became busy before fixture dispatch.");
|
|
569
|
+
await qualifyOrganization();
|
|
570
|
+
verifyContext();
|
|
571
|
+
await helper("fixture-mark", { laneId: plan.laneId, operationId: operation.operationId, state: "start-may-have-effects" });
|
|
572
|
+
armed = true;
|
|
573
|
+
const result = await manager.sendMessage(
|
|
574
|
+
plan.nativeId,
|
|
575
|
+
fixture.text,
|
|
576
|
+
void 0,
|
|
577
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
578
|
+
);
|
|
579
|
+
const state = result?.delivered === true ? "native-delivered" : result?.delivered === false ? "native-refused" : "uncertain";
|
|
580
|
+
await helper("fixture-mark", { laneId: plan.laneId, operationId: operation.operationId, state });
|
|
581
|
+
if (state === "uncertain") throw new Error("Native fixture delivery is uncertain; no replay.");
|
|
582
|
+
delete manager[symbol];
|
|
583
|
+
return { state, laneId: plan.laneId, operationId: operation.operationId, operationEffectVerified: false };
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (!armed) delete manager[symbol];
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function nativeClaudeReadEmptyCreation(manager, planned, verifyContext) {
|
|
590
|
+
const pending = planned.creationNativeId ? { nativeId: planned.creationNativeId } : manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.native-creation.v1")]?.get(planned.laneId);
|
|
591
|
+
if (!pending?.nativeId) throw new Error("Own actual created native identity is unresolved; no replay.");
|
|
592
|
+
const raw = await manager.getSessionWhenLoaded(pending.nativeId, "topolo-dynamic-empty-process", { warnOnMiss: false });
|
|
593
|
+
const view = await manager.getSession(pending.nativeId);
|
|
594
|
+
verifyContext();
|
|
595
|
+
if (!raw || !view || raw.sessionId !== pending.nativeId || view.sessionId !== pending.nativeId || raw.cwd !== planned.role.cwd || view.cwd !== planned.role.cwd || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.spawnedFrom?.sessionId !== planned.intake.nativeId || raw.spawnedFrom?.sideSession !== true || raw.isArchived || view.isArchived || raw.worktreePath || raw.worktreeLazy) throw new Error("Actual fresh native child placement/parent differs.");
|
|
596
|
+
if (raw.isRunning || view.isRunning || raw.messageBuffer?.length || raw.heldAcrossLoss?.length || raw.pendingToolPermissions?.length || view.pendingToolPermissions?.length || manager.hasLosableWork(pending.nativeId))
|
|
597
|
+
throw new Error("Fresh child has held/input/active work before configuration-only ACK.");
|
|
598
|
+
if (!raw.query || raw.pendingFirstStart || raw.startResumeInFlight) return { state: "created-initializing", nativeId: pending.nativeId, requestedTaskStarted: false, privateMCPVerified: false };
|
|
599
|
+
return {
|
|
600
|
+
state: "ready",
|
|
601
|
+
nativeId: pending.nativeId,
|
|
602
|
+
cwd: raw.cwd,
|
|
603
|
+
title: raw.title,
|
|
604
|
+
model: view.model,
|
|
605
|
+
effort: view.effort,
|
|
606
|
+
nativeParentId: raw.spawnedFrom.sessionId,
|
|
607
|
+
archived: false,
|
|
608
|
+
backend: "local",
|
|
609
|
+
foreignWorkspace: false,
|
|
610
|
+
busy: false,
|
|
611
|
+
pendingApprovals: 0,
|
|
612
|
+
requestedTaskStarted: false,
|
|
613
|
+
modelTurnsStarted: false,
|
|
614
|
+
privateMCPVerified: false,
|
|
615
|
+
proof: "current-native-descriptor-only"
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/lib/claude-pack-approvals.ts
|
|
620
|
+
async function nativeClaudeApprovalControl(manager, descriptor, token, net, fs, invoke, qualify, checkpoint) {
|
|
621
|
+
const key = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approval-control.v1");
|
|
622
|
+
qualify();
|
|
623
|
+
if (!Number.isSafeInteger(descriptor.pid) || descriptor.pid < 1 || !/^[a-f0-9]{64}$/.test(token) || !/^[a-f0-9]{64}$/.test(descriptor.identity)) throw new Error("Unqualified native control identity.");
|
|
624
|
+
const previous = manager[key];
|
|
625
|
+
if (fs.existsSync(descriptor.socket)) throw new Error("Native control socket path already exists; no unowned takeover.");
|
|
626
|
+
if (previous) {
|
|
627
|
+
if (previous.active) throw new Error("Native approval control requests are still in flight.");
|
|
628
|
+
await new Promise((done, fail) => previous.server.close((error) => error ? fail(error) : done()));
|
|
629
|
+
delete manager[key];
|
|
630
|
+
}
|
|
631
|
+
const control = { active: 0, server: null, descriptor };
|
|
632
|
+
const server = net.createServer((connection) => {
|
|
633
|
+
let buffer = "";
|
|
634
|
+
let dispatched = false;
|
|
635
|
+
connection.setEncoding("utf8");
|
|
636
|
+
connection.setTimeout(5e3, () => connection.destroy());
|
|
637
|
+
connection.on("error", () => connection.destroy());
|
|
638
|
+
connection.on("data", async (chunk) => {
|
|
639
|
+
if (dispatched) return;
|
|
640
|
+
buffer += chunk;
|
|
641
|
+
if (Buffer.byteLength(buffer) > 65536) {
|
|
642
|
+
dispatched = true;
|
|
643
|
+
connection.destroy();
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (!buffer.includes("\n")) return;
|
|
647
|
+
dispatched = true;
|
|
648
|
+
control.active++;
|
|
649
|
+
try {
|
|
650
|
+
if (control.failure) throw new Error(control.failure);
|
|
651
|
+
qualify();
|
|
652
|
+
const request = JSON.parse(buffer);
|
|
653
|
+
if (Object.keys(request).sort().join(",") !== "identity,operation,pid,token" || request.token !== token || request.pid !== descriptor.pid || request.identity !== descriptor.identity)
|
|
654
|
+
throw new Error("Native approval control authentication or process identity differs.");
|
|
655
|
+
const op = request.operation;
|
|
656
|
+
const fields = op?.kind === "smoke-start" ? ["kind", "key", "caseId", "operationId"] : op?.kind === "smoke-read" ? ["kind", "operationId"] : ["kind", "key", "intake", "fingerprint", "decision", "humanDecisionRecorded"];
|
|
657
|
+
if (!op || !["list", "details", "respond", "smoke-start", "smoke-read"].includes(op.kind) || Object.keys(op).some((k) => !fields.includes(k)))
|
|
658
|
+
throw new Error("Unsupported native control operation.");
|
|
659
|
+
const result = await invoke(op);
|
|
660
|
+
const reply = JSON.stringify({ result }) + "\n";
|
|
661
|
+
if (Buffer.byteLength(reply) > 262144) throw new Error("Native approval details exceed bounded transport; use the original native interaction.");
|
|
662
|
+
connection.end(reply);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
try {
|
|
665
|
+
checkpoint({ kind: "approval-control-refused" });
|
|
666
|
+
} catch {
|
|
667
|
+
control.failure = "Native approval audit unavailable; reconcile through the original interaction.";
|
|
668
|
+
}
|
|
669
|
+
connection.end(JSON.stringify({ error: control.failure || (error instanceof Error ? error.message : "Native approval control refused.") }) + "\n");
|
|
670
|
+
} finally {
|
|
671
|
+
control.active--;
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
control.server = server;
|
|
676
|
+
await new Promise((done, fail) => {
|
|
677
|
+
const failed = (error) => fail(error);
|
|
678
|
+
server.once("error", failed);
|
|
679
|
+
server.listen(descriptor.socket, () => {
|
|
680
|
+
server.off("error", failed);
|
|
681
|
+
done();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
try {
|
|
685
|
+
fs.chmodSync(descriptor.socket, 384);
|
|
686
|
+
checkpoint({ kind: "approval-control-installed", ...descriptor });
|
|
687
|
+
} catch (error) {
|
|
688
|
+
await new Promise((done) => server.close(() => done()));
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
server.on("error", () => {
|
|
692
|
+
control.failure = "Native approval control failed; reconcile through the original interaction.";
|
|
693
|
+
try {
|
|
694
|
+
checkpoint({ kind: "approval-control-error" });
|
|
695
|
+
} catch {
|
|
696
|
+
control.failure = "Native approval audit unavailable; reconcile through the original interaction.";
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
manager[key] = control;
|
|
700
|
+
return { state: "native-approval-control-installed", ...descriptor };
|
|
701
|
+
}
|
|
702
|
+
async function nativeClaudeOwnedSmokeOperation(manager, members, operation, helper, fingerprint, nativeIdentity, qualifyNative = () => {
|
|
703
|
+
}) {
|
|
704
|
+
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
705
|
+
const fields = operation?.kind === "smoke-start" ? ["kind", "key", "caseId", "operationId"] : ["kind", "operationId"];
|
|
706
|
+
if (!operation || !["smoke-start", "smoke-read"].includes(operation.kind) || Object.keys(operation).sort().join() !== fields.sort().join() || !uuid.test(operation.operationId) || operation.kind === "smoke-start" && (!/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(operation.key) || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(operation.caseId)))
|
|
707
|
+
throw new Error("Exact closed native smoke operation required.");
|
|
708
|
+
if (!members.length || members.length > 70 || new Set(members.map((m) => m.key)).size !== members.length || new Set(members.map((m) => m.sessionId)).size !== members.length) throw new Error("Unique native smoke roster required.");
|
|
709
|
+
const owner = /* @__PURE__ */ Symbol.for("topolo.agent-pack.owned-smoke.v1");
|
|
710
|
+
const rosterIdentity = fingerprint(members);
|
|
711
|
+
const summary = (record) => Object.fromEntries([
|
|
712
|
+
"operationId",
|
|
713
|
+
"harness",
|
|
714
|
+
"caseId",
|
|
715
|
+
"key",
|
|
716
|
+
"state",
|
|
717
|
+
"nativeId",
|
|
718
|
+
"hostedUsageVerified",
|
|
719
|
+
"operationEffectVerified",
|
|
720
|
+
"nativeStatus",
|
|
721
|
+
"historyPreserved",
|
|
722
|
+
"ack"
|
|
723
|
+
].filter((k) => k in record).map((k) => [k, record[k]]));
|
|
724
|
+
const check = (member2, session) => {
|
|
725
|
+
if (!session || session.sessionId !== member2.sessionId || session.cwd !== member2.cwd || session.title !== member2.title || session.isArchived || session.backend?.kind !== "local" || session.backend.remoteTarget || session.worktreePath || session.worktreeLazy || session.spawnedFrom || session.isRunning || session.starting || session.pendingToolPermissions?.length || manager.hasLosableWork(member2.sessionId) || session.model !== member2.model || session.effort !== member2.effort || session.permissionMode !== member2.permissionMode || member2.permissionMode !== "bypassPermissions" || !member2.permissionConsent || !manager.getAvailableCodeModels().includes(member2.model)) throw new Error("Exact idle native smoke ownership/settings unavailable.");
|
|
726
|
+
};
|
|
727
|
+
const current = () => {
|
|
728
|
+
qualifyNative();
|
|
729
|
+
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1")] || fingerprint(members) !== rosterIdentity)
|
|
730
|
+
throw new Error("Unresolved native configuration or changed smoke roster remains.");
|
|
731
|
+
for (const member2 of members) check(member2, manager.getSession(member2.sessionId));
|
|
732
|
+
};
|
|
733
|
+
const ready = async () => {
|
|
734
|
+
current();
|
|
735
|
+
for (const member2 of members) {
|
|
736
|
+
const descriptor = await manager.getSessionWhenLoaded(member2.sessionId, "topolo-owned-smoke", { warnOnMiss: false });
|
|
737
|
+
check(member2, descriptor);
|
|
738
|
+
check(member2, manager.getSession(member2.sessionId));
|
|
739
|
+
}
|
|
740
|
+
current();
|
|
741
|
+
};
|
|
742
|
+
if (operation.kind === "smoke-read") {
|
|
743
|
+
const record = await helper("read", { operation: operation.operationId });
|
|
744
|
+
if (record.state !== "native-delivered") return summary(record);
|
|
745
|
+
if (record.nativeIdentity !== nativeIdentity) return { ...summary(record), connectionCurrent: false };
|
|
746
|
+
const member2 = members.find((m) => m.key === record.key && m.sessionId === record.row.nativeId && m.cwd === record.row.cwd);
|
|
747
|
+
if (!member2) throw new Error("Native smoke read custody differs.");
|
|
748
|
+
const session = await manager.getSession(member2.sessionId);
|
|
749
|
+
if (session?.isRunning || session?.starting || manager[owner]) return summary(record);
|
|
750
|
+
check(member2, session);
|
|
751
|
+
const history = await manager.getTranscript(member2.sessionId);
|
|
752
|
+
if (!Array.isArray(history)) throw new Error("Qualified native smoke transcript API unavailable.");
|
|
753
|
+
const same = fingerprint(history.slice(0, record.historyCount)) === record.historyHash;
|
|
754
|
+
const markers = history.map((m, index) => m.uuid === record.operationId ? index : -1).filter((i) => i >= 0);
|
|
755
|
+
const marker = markers[0] ?? -1;
|
|
756
|
+
const user = markers.length === 1 && marker === record.historyCount && history[marker]?.type === "user" ? marker : -1;
|
|
757
|
+
const nextUser = history.findIndex((m, index) => index > user && m.type === "user");
|
|
758
|
+
const assistants = user < 0 ? [] : history.slice(user + 1, nextUser < 0 ? history.length : nextUser).filter((m) => m.type === "assistant");
|
|
759
|
+
const text = assistants.at(-1)?.message?.content?.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
760
|
+
let ack;
|
|
761
|
+
if (typeof text === "string" && text.length <= 2048) {
|
|
762
|
+
try {
|
|
763
|
+
ack = JSON.parse(text);
|
|
764
|
+
} catch {
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const expected = {
|
|
768
|
+
operationId: record.operationId,
|
|
769
|
+
logicalId: record.row.logicalId,
|
|
770
|
+
agentVersionId: record.row.agentVersionId,
|
|
771
|
+
contextMarkerHash: record.row.contextMarkerHash,
|
|
772
|
+
artifactHash: record.row.artifactHash
|
|
773
|
+
};
|
|
774
|
+
const valid = same && Number.isSafeInteger(record.historyCount) && record.historyCount >= 0 && ack && typeof ack === "object" && !Array.isArray(ack) && Object.keys(ack).sort().join() === [...Object.keys(expected), "status"].sort().join() && Object.entries(expected).every(([k, v]) => ack[k] === v) && ["ok", "blocked"].includes(ack.status);
|
|
775
|
+
await helper("verify", { operation: operation.operationId });
|
|
776
|
+
return summary(await helper("mark", {
|
|
777
|
+
operation: operation.operationId,
|
|
778
|
+
state: "uncertain",
|
|
779
|
+
nativeStatus: "terminal-evidence-unavailable",
|
|
780
|
+
historyPreserved: same,
|
|
781
|
+
...valid ? { ack } : {}
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
if (manager[owner]) throw new Error("Native smoke dispatch already reserved.");
|
|
785
|
+
const member = members.find((m) => m.key === operation.key);
|
|
786
|
+
if (!member) throw new Error("Smoke role is not natively owned.");
|
|
787
|
+
manager[owner] = operation.operationId;
|
|
788
|
+
let prepared = false;
|
|
789
|
+
try {
|
|
790
|
+
await ready();
|
|
791
|
+
const plan = await helper("prepare", {
|
|
792
|
+
key: operation.key,
|
|
793
|
+
case: operation.caseId,
|
|
794
|
+
operation: operation.operationId,
|
|
795
|
+
native_identity: nativeIdentity
|
|
796
|
+
});
|
|
797
|
+
prepared = true;
|
|
798
|
+
const actual = plan.roster.map((r) => [r.key, r.nativeId, r.cwd, r.title]).sort();
|
|
799
|
+
const wanted = members.map((m) => [m.key, m.sessionId, m.cwd, m.title]).sort();
|
|
800
|
+
if (JSON.stringify(actual) !== JSON.stringify(wanted)) throw new Error("Smoke binding must match the complete owned native roster.");
|
|
801
|
+
await ready();
|
|
802
|
+
await helper("verify", { operation: operation.operationId });
|
|
803
|
+
const history = await manager.getTranscript(member.sessionId);
|
|
804
|
+
if (!Array.isArray(history)) throw new Error("Qualified native smoke transcript API unavailable.");
|
|
805
|
+
await helper("mark", { operation: operation.operationId, state: "prepared", historyCount: history.length, historyHash: fingerprint(history) });
|
|
806
|
+
await ready();
|
|
807
|
+
const admission = await helper("arm", { operation: operation.operationId, native_identity: nativeIdentity });
|
|
808
|
+
current();
|
|
809
|
+
if (admission.operationId !== operation.operationId || admission.nativeIdentity !== nativeIdentity || admission.state !== "start-may-have-effects" || admission.bindingHash !== plan.record.bindingHash || !/^[a-f0-9]{64}$/.test(admission.bindingHash) || !Number.isFinite(admission.expiresAt) || admission.expiresAt !== plan.record.dispatchExpiresAt || Date.now() >= admission.expiresAt * 1e3)
|
|
810
|
+
throw new Error("Original smoke final dispatch admission expired or differs.");
|
|
811
|
+
const ack = await manager.sendMessage(
|
|
812
|
+
member.sessionId,
|
|
813
|
+
plan.text,
|
|
814
|
+
void 0,
|
|
815
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
816
|
+
);
|
|
817
|
+
return summary(await helper("mark", {
|
|
818
|
+
operation: operation.operationId,
|
|
819
|
+
state: ack?.delivered === true ? "native-delivered" : ack?.delivered === false ? "native-refused" : "uncertain",
|
|
820
|
+
nativeId: operation.operationId
|
|
821
|
+
}));
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (prepared) {
|
|
824
|
+
const record = await helper("read", { operation: operation.operationId });
|
|
825
|
+
await helper("mark", { operation: operation.operationId, state: record.state === "prepared" ? "preflight-refused" : "uncertain" });
|
|
826
|
+
}
|
|
827
|
+
throw error;
|
|
828
|
+
} finally {
|
|
829
|
+
delete manager[owner];
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function nativeClaudeApprovalOperation(manager, routes, operation, fingerprint, checkpoint, history = []) {
|
|
833
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1");
|
|
834
|
+
if (!routes.length || routes.length > 100 || new Set(routes.map((r) => r.sessionId)).size !== routes.length || new Set(routes.map((r) => r.key)).size !== routes.length) throw new Error("Unique owned approval roster required.");
|
|
835
|
+
const existing = manager[symbol];
|
|
836
|
+
const byId = existing?.byId ?? new Map(routes.map((r) => [r.sessionId, r]));
|
|
837
|
+
const byKey = existing?.byKey ?? new Map(routes.map((r) => [r.key, r]));
|
|
838
|
+
for (const r of routes) {
|
|
839
|
+
const intake = routes.find((candidate) => candidate.key === r.intakeKey);
|
|
840
|
+
if (!/^org:.+$/.test(r.contextKey) || !intake || !intake.key.includes("/intakes/") || intake.contextKey !== r.contextKey || intake.sessionId !== r.intakeSessionId)
|
|
841
|
+
throw new Error("Approval route has no same-context owned intake.");
|
|
842
|
+
}
|
|
843
|
+
const identity = fingerprint(routes);
|
|
844
|
+
let state = manager[symbol];
|
|
845
|
+
if (state?.auditFailed) throw new Error("Native approval audit unavailable; use the original native interaction.");
|
|
846
|
+
const owned = async (route) => {
|
|
847
|
+
if (typeof manager.getSessionWhenLoaded !== "function") throw new Error("Qualified native session descriptor API is unavailable.");
|
|
848
|
+
const descriptor = await manager.getSessionWhenLoaded(route.sessionId, "topolo-agent-pack-approval", { warnOnMiss: false });
|
|
849
|
+
const session = await manager.getSession(route.sessionId);
|
|
850
|
+
if (!session || session.sessionId !== route.sessionId || session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
851
|
+
throw new Error("Native approval session identity differs: " + route.key);
|
|
852
|
+
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title || descriptor.isArchived || descriptor.backend?.kind !== "local" || descriptor.backend.remoteTarget || descriptor.worktreePath || descriptor.worktreeLazy || (route.dynamic ? descriptor.spawnedFrom?.sessionId !== route.dynamic.nativeParentId || descriptor.spawnedFrom?.sideSession !== true || route.dynamic.nativeParentId !== route.intakeSessionId || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) : descriptor.spawnedFrom))
|
|
853
|
+
throw new Error("Native approval session descriptor identity differs: " + route.key);
|
|
854
|
+
return session;
|
|
855
|
+
};
|
|
856
|
+
if (state && state.identity !== identity) {
|
|
857
|
+
if (operation.kind !== "install" || state.notifications.size || [...state.entries.values()].some((e) => ["pending", "response-may-have-effects"].includes(e.record.state)))
|
|
858
|
+
throw new Error("An existing native approval owner has a different roster.");
|
|
859
|
+
for (const r of routes) await owned(r);
|
|
860
|
+
for (const route of state.routes) {
|
|
861
|
+
const session = await manager.getSession(route.sessionId);
|
|
862
|
+
if (!session || session.cwd !== route.cwd || session.title !== route.title || session.isRunning || session.starting || session.pendingToolPermissions?.length || manager.hasLosableWork(route.sessionId))
|
|
863
|
+
throw new Error("Outgoing approval owner is not safely idle.");
|
|
864
|
+
}
|
|
865
|
+
if (typeof manager.off !== "function") throw new Error("Native approval listener ownership cannot be released.");
|
|
866
|
+
manager.off("event", state.listener);
|
|
867
|
+
delete manager[symbol];
|
|
868
|
+
state = void 0;
|
|
869
|
+
}
|
|
870
|
+
const current = async (entry2) => {
|
|
871
|
+
const source2 = await owned(byId.get(entry2.record.sourceSessionId));
|
|
872
|
+
const matches = (source2.pendingToolPermissions ?? []).filter((r) => r.requestId === entry2.request.requestId);
|
|
873
|
+
if (matches.length !== 1 || fingerprint(matches[0]) !== entry2.record.fingerprint)
|
|
874
|
+
throw new Error("Exact native request is no longer pending; no replay.");
|
|
875
|
+
return matches[0];
|
|
876
|
+
};
|
|
877
|
+
if (operation.kind === "install") {
|
|
878
|
+
for (const r of routes) await owned(r);
|
|
879
|
+
if (!state) {
|
|
880
|
+
if (typeof manager.on !== "function" || typeof manager.sendMessage !== "function" || typeof manager.respondToToolPermission !== "function") throw new Error("Native approval transport unavailable.");
|
|
881
|
+
state = { identity, routes, byId, byKey, entries: /* @__PURE__ */ new Map(), notifications: /* @__PURE__ */ new Set(), deliveryAttempts: 0 };
|
|
882
|
+
manager[symbol] = state;
|
|
883
|
+
const save = (entry2) => checkpoint({ ...entry2.record });
|
|
884
|
+
const deliver = async (entry2) => {
|
|
885
|
+
const record = entry2.record;
|
|
886
|
+
if (state.notifications.has(record.key) || !["not-delivered", "awaiting-idle-intake"].includes(record.notificationState)) return;
|
|
887
|
+
if (record.sourceSessionId === record.intakeSessionId) {
|
|
888
|
+
record.notificationState = "native-intake";
|
|
889
|
+
save(entry2);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
state.notifications.add(record.key);
|
|
893
|
+
try {
|
|
894
|
+
await current(entry2);
|
|
895
|
+
const intake = await owned(byId.get(record.intakeSessionId));
|
|
896
|
+
if (manager.hasLosableWork(record.intakeSessionId) || intake.isRunning || intake.starting || intake.pendingToolPermissions?.length) {
|
|
897
|
+
record.notificationState = "awaiting-idle-intake";
|
|
898
|
+
save(entry2);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
const text = "Permission relay (generated by the Topolo pack adapter). " + record.sourceKey + " awaits a native decision. Request " + record.key + ", fingerprint " + record.fingerprint + ". Read exact live details using topolo agent-packs approvals details " + record.key + " --harness claude --intake " + record.intakeSessionId + " --fingerprint " + record.fingerprint + (byId.get(record.sourceSessionId)?.dynamic?.connectionId ? " --connection " + byId.get(record.sourceSessionId).dynamic.connectionId : "") + ". Present this to the human in this intake and relay only the recorded answer. This notification grants no approval." + (record.cardOnly ? " Claude requires approval on the original native card; report this limitation here." : "");
|
|
902
|
+
record.notificationState = "may-have-effects";
|
|
903
|
+
save(entry2);
|
|
904
|
+
state.deliveryAttempts++;
|
|
905
|
+
const ack = await manager.sendMessage(
|
|
906
|
+
record.intakeSessionId,
|
|
907
|
+
text,
|
|
908
|
+
void 0,
|
|
909
|
+
{ initiator: "agent", origin: { kind: "task-notification" } }
|
|
910
|
+
);
|
|
911
|
+
record.notificationState = ack?.delivered === true ? "delivered" : "delivery-refused";
|
|
912
|
+
save(entry2);
|
|
913
|
+
} catch {
|
|
914
|
+
record.notificationState = record.notificationState === "may-have-effects" ? "delivery-uncertain" : "delivery-failed";
|
|
915
|
+
save(entry2);
|
|
916
|
+
} finally {
|
|
917
|
+
state.notifications.delete(record.key);
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
const capture = async (sessionId, request) => {
|
|
921
|
+
const route = byId.get(sessionId);
|
|
922
|
+
if (!route || !request || typeof request.requestId !== "string") return;
|
|
923
|
+
const source2 = await owned(route);
|
|
924
|
+
const matches = (source2.pendingToolPermissions ?? []).filter((r) => r.requestId === request.requestId);
|
|
925
|
+
if (matches.length !== 1) throw new Error("Native event is not in the owned pending inventory.");
|
|
926
|
+
const pending2 = matches[0];
|
|
927
|
+
const key = fingerprint([identity, pending2.requestId]);
|
|
928
|
+
const hash = fingerprint(pending2);
|
|
929
|
+
let entry2 = state.entries.get(key);
|
|
930
|
+
if (entry2 && entry2.record.fingerprint !== hash) throw new Error("Native request identity was reused.");
|
|
931
|
+
if (!entry2) {
|
|
932
|
+
const old = history.filter((row) => row.key === key).at(-1);
|
|
933
|
+
if (old && (old.fingerprint !== hash || old.sourceSessionId !== sessionId || old.intakeSessionId !== route.intakeSessionId || old.contextKey !== route.contextKey))
|
|
934
|
+
throw new Error("Durable approval identity differs; no replay.");
|
|
935
|
+
entry2 = { request: pending2, record: {
|
|
936
|
+
key,
|
|
937
|
+
fingerprint: hash,
|
|
938
|
+
sourceKey: route.key,
|
|
939
|
+
sourceSessionId: sessionId,
|
|
940
|
+
intakeKey: route.intakeKey,
|
|
941
|
+
intakeSessionId: route.intakeSessionId,
|
|
942
|
+
contextKey: route.contextKey,
|
|
943
|
+
state: "pending",
|
|
944
|
+
notificationState: "not-delivered",
|
|
945
|
+
cardOnly: pending2.onlyOnCard === true || pending2.noAllowOnNotification === true,
|
|
946
|
+
nativeApproved: false,
|
|
947
|
+
...old ? { state: old.state, notificationState: old.notificationState } : {}
|
|
948
|
+
} };
|
|
949
|
+
state.entries.set(key, entry2);
|
|
950
|
+
save(entry2);
|
|
951
|
+
}
|
|
952
|
+
const lifecycle2 = state.dynamicLifecycle?.get(sessionId);
|
|
953
|
+
if (lifecycle2 && entry2.record.state === "pending") await lifecycle2.request(entry2.record, pending2);
|
|
954
|
+
if (entry2.record.state === "pending") await deliver(entry2);
|
|
955
|
+
};
|
|
956
|
+
state.listener = async (event) => {
|
|
957
|
+
try {
|
|
958
|
+
if (event?.type === "tool_permission_request") await capture(event.sessionId, event.request);
|
|
959
|
+
else if (event?.type === "session_updated") {
|
|
960
|
+
const source2 = byId.get(event.sessionId);
|
|
961
|
+
if (source2) {
|
|
962
|
+
const session = await owned(source2);
|
|
963
|
+
for (const entry2 of state.entries.values())
|
|
964
|
+
if (entry2.record.state === "pending" && entry2.record.sourceSessionId === event.sessionId && !(session.pendingToolPermissions ?? []).some((r) => r.requestId === entry2.request.requestId)) {
|
|
965
|
+
entry2.record.state = "native-request-no-longer-pending";
|
|
966
|
+
delete entry2.request;
|
|
967
|
+
save(entry2);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
for (const entry2 of state.entries.values())
|
|
971
|
+
if (entry2.record.state === "pending" && entry2.record.intakeSessionId === event.sessionId) await deliver(entry2);
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
try {
|
|
975
|
+
checkpoint({ kind: "approval-routing-refused", sessionId: event?.sessionId });
|
|
976
|
+
} catch {
|
|
977
|
+
state.auditFailed = true;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
manager.on("event", state.listener);
|
|
982
|
+
checkpoint({ kind: "approval-listener-installed", identity, roles: routes.length });
|
|
983
|
+
for (const r of routes) for (const request of (await owned(r)).pendingToolPermissions ?? []) await capture(r.sessionId, request);
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
state: "native-approval-listener-installed",
|
|
987
|
+
identity,
|
|
988
|
+
roles: routes.length,
|
|
989
|
+
modelTurnsMayHaveStartedByInstallation: state.deliveryAttempts > 0
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
if (!state) throw new Error("Native approval listener is not installed.");
|
|
993
|
+
if (operation.kind === "list") return { requests: [...state.entries.values()].map((e) => ({ ...e.record })) };
|
|
994
|
+
const entry = state.entries.get(operation.key);
|
|
995
|
+
if (!entry || entry.record.state !== "pending" || entry.record.intakeSessionId !== operation.intake || entry.record.fingerprint !== operation.fingerprint) throw new Error("Exact pending request and owning intake required.");
|
|
996
|
+
if (operation.kind === "respond" && operation.decision === "once" && byId.get(entry.record.sourceSessionId)?.dynamic)
|
|
997
|
+
throw Object.assign(
|
|
998
|
+
new Error("native_private_query_prestart_contract_unqualified: dynamic private query authorization remains closed."),
|
|
999
|
+
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
1000
|
+
);
|
|
1001
|
+
const pending = await current(entry);
|
|
1002
|
+
if (manager[symbol] !== state || state.entries.get(operation.key) !== entry || entry.record.state !== "pending" || state.auditFailed)
|
|
1003
|
+
throw new Error("Exact pending request changed during native identity validation; reconcile without replay.");
|
|
1004
|
+
if (operation.kind === "details") return { record: { ...entry.record }, request: pending };
|
|
1005
|
+
if (operation.kind !== "respond" || operation.humanDecisionRecorded !== true || !["once", "deny"].includes(operation.decision ?? ""))
|
|
1006
|
+
throw new Error("Recorded human one-request decision required; never auto-approve.");
|
|
1007
|
+
if (operation.decision !== "deny" && entry.record.cardOnly) throw new Error("Claude requires the original card; intake cannot impersonate its origin.");
|
|
1008
|
+
entry.record.state = "response-may-have-effects";
|
|
1009
|
+
checkpoint({ ...entry.record });
|
|
1010
|
+
await manager.respondToToolPermission(pending.requestId, operation.decision, void 0, "elsewhere");
|
|
1011
|
+
const source = await owned(byId.get(entry.record.sourceSessionId));
|
|
1012
|
+
if (source.pendingToolPermissions?.some((r) => r.requestId === pending.requestId))
|
|
1013
|
+
throw new Error("Native decision did not settle; reconcile without replay.");
|
|
1014
|
+
const lifecycle = state.dynamicLifecycle?.get(entry.record.sourceSessionId);
|
|
1015
|
+
if (lifecycle) await lifecycle.decision(entry.record, pending, operation.decision);
|
|
1016
|
+
entry.record.state = "native-request-settled";
|
|
1017
|
+
checkpoint({ ...entry.record });
|
|
1018
|
+
return { state: entry.record.state, nativeApproved: false, operationEffectVerified: false };
|
|
1019
|
+
}
|
|
1020
|
+
async function nativeClaudeAppendDynamicRoute(manager, route, verify, checkpoint, lifecycle) {
|
|
1021
|
+
const state = manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")];
|
|
1022
|
+
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || route.dynamic.nativeParentId !== route.intakeSessionId)
|
|
1023
|
+
throw new Error("Current qualified original approval listener required.");
|
|
1024
|
+
const intake = state.byId.get(route.intakeSessionId);
|
|
1025
|
+
if (!intake || intake.key !== route.intakeKey || intake.contextKey !== route.contextKey || !intake.key.includes("/intakes/"))
|
|
1026
|
+
throw new Error("Dynamic approval route has no exact same-context original intake.");
|
|
1027
|
+
await verify();
|
|
1028
|
+
const raw = await manager.getSessionWhenLoaded(route.sessionId, "topolo-dynamic-route", { warnOnMiss: false });
|
|
1029
|
+
const dto = await manager.getSession(route.sessionId);
|
|
1030
|
+
if (!raw || !dto || raw.sessionId !== route.sessionId || raw.cwd !== route.cwd || raw.title !== route.title || dto.sessionId !== route.sessionId || dto.cwd !== route.cwd || dto.title !== route.title || raw.isArchived || dto.isArchived || raw.backend?.kind !== "local" || raw.backend.remoteTarget || raw.worktreePath || raw.worktreeLazy || raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true)
|
|
1031
|
+
throw new Error("Actual dynamic child descriptor differs before route append.");
|
|
1032
|
+
await verify();
|
|
1033
|
+
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")] !== state || state.auditFailed)
|
|
1034
|
+
throw new Error("Original native permission connection changed.");
|
|
1035
|
+
const old = state.byId.get(route.sessionId);
|
|
1036
|
+
if (old) {
|
|
1037
|
+
if (JSON.stringify(old) !== JSON.stringify(route)) throw new Error("Native child route conflicts with an existing owner.");
|
|
1038
|
+
return { state: "dynamic-route-current", generation: state.identity };
|
|
1039
|
+
}
|
|
1040
|
+
if (state.byKey.has(route.key)) throw new Error("Dynamic role key is already owned.");
|
|
1041
|
+
checkpoint({ state: "dynamic-route-append", route });
|
|
1042
|
+
state.byId.set(route.sessionId, route);
|
|
1043
|
+
state.byKey.set(route.key, route);
|
|
1044
|
+
state.dynamicRoutes ??= /* @__PURE__ */ new Map();
|
|
1045
|
+
state.dynamicRoutes.set(route.sessionId, route);
|
|
1046
|
+
if (lifecycle) {
|
|
1047
|
+
state.dynamicLifecycle ??= /* @__PURE__ */ new Map();
|
|
1048
|
+
state.dynamicLifecycle.set(route.sessionId, lifecycle);
|
|
1049
|
+
}
|
|
1050
|
+
return { state: "dynamic-route-appended", generation: state.identity, pendingRequestsPreserved: true };
|
|
1051
|
+
}
|
|
1052
|
+
async function nativeClaudeLaneControl(net, fs, manager, descriptor, token, invoke, qualify, checkpoint) {
|
|
1053
|
+
if (!/^[a-f0-9]{64}$/.test(token) || !/^[a-f0-9]{64}$/.test(descriptor.identity) || !/^[a-z0-9][a-z0-9-]*$/.test(descriptor.laneId) || !Number.isSafeInteger(descriptor.pid) || descriptor.pid < 1)
|
|
1054
|
+
throw new Error("Exact private lane native control identity required.");
|
|
1055
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-controls.v1");
|
|
1056
|
+
manager[symbol] ??= /* @__PURE__ */ new Map();
|
|
1057
|
+
const controls = manager[symbol];
|
|
1058
|
+
if (controls.has(descriptor.laneId) || fs.existsSync(descriptor.socket)) throw new Error("Own native lane control already exists; no takeover.");
|
|
1059
|
+
qualify();
|
|
1060
|
+
const control = { active: 0, descriptor, server: null, closed: false };
|
|
1061
|
+
const allowed = /* @__PURE__ */ new Set(["read", "context-start", "context-read", "fixture-start", "fixture-read", "retire", "approval-list", "approval-details", "approval-respond", "close"]);
|
|
1062
|
+
const server = net.createServer((connection) => {
|
|
1063
|
+
let text = "";
|
|
1064
|
+
let dispatched = false;
|
|
1065
|
+
connection.setEncoding("utf8");
|
|
1066
|
+
connection.setTimeout(24e4, () => connection.destroy());
|
|
1067
|
+
connection.on("error", () => connection.destroy());
|
|
1068
|
+
connection.on("data", async (chunk) => {
|
|
1069
|
+
if (dispatched) return;
|
|
1070
|
+
text += chunk;
|
|
1071
|
+
if (Buffer.byteLength(text) > 65536) {
|
|
1072
|
+
dispatched = true;
|
|
1073
|
+
connection.destroy();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (!text.includes("\n")) return;
|
|
1077
|
+
dispatched = true;
|
|
1078
|
+
control.active++;
|
|
1079
|
+
try {
|
|
1080
|
+
qualify();
|
|
1081
|
+
const request = JSON.parse(text);
|
|
1082
|
+
if (Object.keys(request).sort().join() !== "identity,operation,pid,token" || request.token !== token || request.pid !== descriptor.pid || request.identity !== descriptor.identity)
|
|
1083
|
+
throw new Error("Private native lane control authentication differs.");
|
|
1084
|
+
const operation = request.operation;
|
|
1085
|
+
const fields = operation?.kind === "approval-respond" ? ["kind", "key", "intake", "fingerprint", "decision", "humanDecisionRecorded"] : operation?.kind === "approval-details" ? ["kind", "key", "intake", "fingerprint"] : ["context-start", "context-read", "fixture-start"].includes(operation?.kind) ? ["kind", "operationId"] : ["kind"];
|
|
1086
|
+
if (!operation || !allowed.has(operation.kind) || Object.keys(operation).some((k) => !fields.includes(k))) throw new Error("Closed private lane operation required.");
|
|
1087
|
+
if (operation.kind === "close" && control.active !== 1) throw new Error("Own native control has other in-flight operations.");
|
|
1088
|
+
const result = await invoke(operation);
|
|
1089
|
+
const encoded = JSON.stringify({ result }) + "\n";
|
|
1090
|
+
if (Buffer.byteLength(encoded) > 1048576) throw new Error("Bounded private native lane reply exceeded.");
|
|
1091
|
+
connection.end(encoded);
|
|
1092
|
+
if (operation.kind === "close") connection.once("close", () => server.close(() => {
|
|
1093
|
+
control.closed = true;
|
|
1094
|
+
controls.delete(descriptor.laneId);
|
|
1095
|
+
checkpoint({ state: "normal-own-lane-control-closed", laneId: descriptor.laneId });
|
|
1096
|
+
if (fs.existsSync(descriptor.socket)) {
|
|
1097
|
+
const current = fs.lstatSync(descriptor.socket);
|
|
1098
|
+
if (current.ino === control.inode && current.uid === control.uid) fs.unlinkSync(descriptor.socket);
|
|
1099
|
+
}
|
|
1100
|
+
}));
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
connection.end(JSON.stringify({ error: error instanceof Error ? error.message : "Native lane control refused." }) + "\n");
|
|
1103
|
+
} finally {
|
|
1104
|
+
control.active--;
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
control.server = server;
|
|
1109
|
+
await new Promise((done, fail) => {
|
|
1110
|
+
server.once("error", fail);
|
|
1111
|
+
server.listen(descriptor.socket, done);
|
|
1112
|
+
});
|
|
1113
|
+
try {
|
|
1114
|
+
fs.chmodSync(descriptor.socket, 384);
|
|
1115
|
+
const stat = fs.lstatSync(descriptor.socket);
|
|
1116
|
+
control.inode = stat.ino;
|
|
1117
|
+
control.uid = stat.uid;
|
|
1118
|
+
checkpoint({ state: "private-native-lane-control-ready", ...descriptor });
|
|
1119
|
+
controls.set(descriptor.laneId, control);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
await new Promise((done) => server.close(() => done()));
|
|
1122
|
+
throw error;
|
|
1123
|
+
}
|
|
1124
|
+
return { state: "private-native-lane-control-ready", ...descriptor };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/lib/claude-pack-backend.ts
|
|
1128
|
+
var CLAUDE_NATIVE_QUALIFIER = {
|
|
1129
|
+
version: "2.37337.1",
|
|
1130
|
+
manager: "index.chunk-BolytlRP.js",
|
|
1131
|
+
sidebar: "index.chunk-BoQoqlAs.js",
|
|
1132
|
+
hashes: {
|
|
1133
|
+
"index.chunk-BolytlRP.js": "e387edfb06a34a59d99b54c04365f6e32d0108e8710124b4d1a628eea726d030",
|
|
1134
|
+
"index.chunk-BoQoqlAs.js": "5c07a7e836d98dcd6afcfc2a62619e71d173a136ecb9af847716ea377b58b1df",
|
|
1135
|
+
"index.chunk-BWk6pVUt.js": "f7d74e113f34761a675816fe2abf65b3ddc190327ee7623d9c822f7b59cceaca"
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
function validateInspectorEndpoint(endpoint) {
|
|
1139
|
+
const url = new URL(endpoint);
|
|
1140
|
+
if (url.protocol !== "ws:" || url.hostname !== "127.0.0.1" || !url.port || url.username || url.password || url.search || url.hash || !/^\/[0-9a-f-]{36}$/.test(url.pathname))
|
|
1141
|
+
throw new Error("An explicit ws://127.0.0.1:<port>/<inspector UUID> endpoint is required.");
|
|
1142
|
+
return url.href;
|
|
1143
|
+
}
|
|
1144
|
+
function snapshotClaudeMarkdown(root, fs, crypto, path) {
|
|
1145
|
+
const inventory = {};
|
|
1146
|
+
const visit = (directory) => {
|
|
1147
|
+
const stat = fs.lstatSync(directory);
|
|
1148
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Owned context directory is not regular: " + directory);
|
|
1149
|
+
for (const name of fs.readdirSync(directory).sort()) {
|
|
1150
|
+
const file = path.resolve(directory, name);
|
|
1151
|
+
const entry = fs.lstatSync(file);
|
|
1152
|
+
if (entry.isSymbolicLink()) throw new Error("Owned context must not follow symlinks: " + file);
|
|
1153
|
+
if (entry.isDirectory()) visit(file);
|
|
1154
|
+
else if (/\.md$/i.test(name)) {
|
|
1155
|
+
if (!entry.isFile()) throw new Error("Owned Markdown context is not a regular file: " + file);
|
|
1156
|
+
inventory[file] = crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
visit(root);
|
|
1161
|
+
return inventory;
|
|
1162
|
+
}
|
|
1163
|
+
function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
1164
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(pack)) throw new Error("Invalid pack name.");
|
|
1165
|
+
const root = realpathSync(store);
|
|
1166
|
+
const state = JSON.parse(readFileSync(resolve(root, "claude/state.json"), "utf8"));
|
|
1167
|
+
const policy = JSON.parse(readFileSync(policyPath, "utf8"));
|
|
1168
|
+
if (state.harness !== "claude" || policy.format !== "topolo-agent-execution-policy" || policy.pack !== pack)
|
|
1169
|
+
throw new Error("Claude state and reviewed policy must name the same pack.");
|
|
1170
|
+
const records = state.agents.filter((a) => a.pack === pack);
|
|
1171
|
+
if (!records.length || records.length !== policy.agents.length) throw new Error("Reviewed policy must cover the complete installed roster.");
|
|
1172
|
+
const members = [];
|
|
1173
|
+
const context = {};
|
|
1174
|
+
const roleContextFiles = {};
|
|
1175
|
+
const contextInventories = {};
|
|
1176
|
+
const captureMarkdown = (cwd) => {
|
|
1177
|
+
contextInventories[cwd] = snapshotClaudeMarkdown(cwd, filesystem, cryptography, paths);
|
|
1178
|
+
Object.assign(context, contextInventories[cwd]);
|
|
1179
|
+
};
|
|
1180
|
+
for (const record of records) {
|
|
1181
|
+
const matches = policy.agents.filter((a) => a.key === record.key && a.title === record.title);
|
|
1182
|
+
if (matches.length !== 1 || record.sourceRevision !== policy.sourceRevision || record.importState !== "desktop-adopted")
|
|
1183
|
+
throw new Error("Reviewed policy does not match adopted role: " + record.key);
|
|
1184
|
+
const role = matches[0];
|
|
1185
|
+
const settings = role.harnesses.claude.nativeSettings;
|
|
1186
|
+
const cwd = realpathSync(record.workspace);
|
|
1187
|
+
if (!cwd.startsWith(resolve(root, "claude/workspaces", pack) + sep)) throw new Error("Role workspace is outside the owned pack.");
|
|
1188
|
+
captureMarkdown(cwd);
|
|
1189
|
+
const charter = resolve(cwd, "CLAUDE.md");
|
|
1190
|
+
const transcript = realpathSync(record.transcript);
|
|
1191
|
+
context[charter] = createHash("sha256").update(readFileSync(charter)).digest("hex");
|
|
1192
|
+
context[transcript] = createHash("sha256").update(readFileSync(transcript)).digest("hex");
|
|
1193
|
+
roleContextFiles[record.desktopSessionId] = [charter, transcript];
|
|
1194
|
+
members.push({
|
|
1195
|
+
key: record.key,
|
|
1196
|
+
sessionId: record.desktopSessionId,
|
|
1197
|
+
cwd,
|
|
1198
|
+
title: record.title,
|
|
1199
|
+
group: record.pinnedRequested ? null : role.sidebar.group,
|
|
1200
|
+
pinned: record.pinnedRequested === true,
|
|
1201
|
+
model: settings.model,
|
|
1202
|
+
effort: settings.effortLevel,
|
|
1203
|
+
permissionMode: settings.permissions.defaultMode,
|
|
1204
|
+
...policy.installationConsent || settings.permissions.defaultMode === "bypassPermissions" ? {
|
|
1205
|
+
permissionConsent: qualifyPermissionConsent(policy, record.key, records, settings.permissions.defaultMode === "bypassPermissions")
|
|
1206
|
+
} : {}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
for (const file of [resolve(root, "claude/state.json"), realpathSync(policyPath)])
|
|
1210
|
+
context[file] = createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
1211
|
+
const activePath = resolve(root, "claude/active.json");
|
|
1212
|
+
const active = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
1213
|
+
const target = new Set(members.map((m) => m.key));
|
|
1214
|
+
const outgoing = [];
|
|
1215
|
+
if (active) {
|
|
1216
|
+
if (!Array.isArray(active.keys) || new Set(active.keys).size !== active.keys.length) throw new Error("Invalid verified active roster.");
|
|
1217
|
+
for (const key of active.keys.filter((key2) => !target.has(key2))) {
|
|
1218
|
+
if (mode !== "switch") throw new Error("Use a native switch to replace the verified active roster.");
|
|
1219
|
+
const candidates = state.agents.filter((a) => a.key === key && a.importState === "desktop-adopted");
|
|
1220
|
+
if (candidates.length !== 1) throw new Error("Outgoing role has no unique owned adoption record.");
|
|
1221
|
+
const record = candidates[0];
|
|
1222
|
+
const cwd = realpathSync(record.workspace);
|
|
1223
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(record.pack)) throw new Error("Invalid outgoing pack name.");
|
|
1224
|
+
if (!cwd.startsWith(resolve(root, "claude/workspaces", record.pack) + sep)) throw new Error("Outgoing workspace is outside the owned pack.");
|
|
1225
|
+
captureMarkdown(cwd);
|
|
1226
|
+
for (const file of [resolve(cwd, "CLAUDE.md"), realpathSync(record.transcript)])
|
|
1227
|
+
context[file] = createHash("sha256").update(readFileSync(file)).digest("hex");
|
|
1228
|
+
outgoing.push({ key, sessionId: record.desktopSessionId, cwd, title: record.title });
|
|
1229
|
+
}
|
|
1230
|
+
} else if (mode === "switch") throw new Error("Configure and verify an initial active roster before switching.");
|
|
1231
|
+
const approvalRoutes = members.filter((m) => m.permissionConsent).map((m) => {
|
|
1232
|
+
const intake = members.find((i) => i.key === m.permissionConsent.intakeKey);
|
|
1233
|
+
if (!intake) throw new Error("Owning intake is not in the native roster.");
|
|
1234
|
+
return {
|
|
1235
|
+
key: m.key,
|
|
1236
|
+
sessionId: m.sessionId,
|
|
1237
|
+
cwd: m.cwd,
|
|
1238
|
+
title: m.title,
|
|
1239
|
+
...m.permissionConsent,
|
|
1240
|
+
intakeSessionId: intake.sessionId
|
|
1241
|
+
};
|
|
1242
|
+
});
|
|
1243
|
+
return {
|
|
1244
|
+
members,
|
|
1245
|
+
outgoing,
|
|
1246
|
+
context,
|
|
1247
|
+
contextInventories,
|
|
1248
|
+
roleContextFiles,
|
|
1249
|
+
globalContextFiles: [resolve(root, "claude/state.json"), realpathSync(policyPath)],
|
|
1250
|
+
approvalRoutes,
|
|
1251
|
+
installationConsent: policy.installationConsent,
|
|
1252
|
+
libraryRoot: root,
|
|
1253
|
+
sourceRevision: policy.sourceRevision,
|
|
1254
|
+
mode,
|
|
1255
|
+
activeBefore: active
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function verifyClaudePlanContext(plan, members, fs, crypto, path, snapshotMarkdown) {
|
|
1259
|
+
if (!plan.contextInventories || !Object.keys(plan.contextInventories).length) throw new Error("Complete context inventory proof is unavailable.");
|
|
1260
|
+
if (members && (!plan.roleContextFiles || !plan.globalContextFiles?.length)) throw new Error("Scoped role context proof is unavailable.");
|
|
1261
|
+
const selected = members ?? plan.members;
|
|
1262
|
+
if (new Set(selected.map((m) => m.sessionId)).size !== selected.length || selected.some((m) => !plan.members.some((p) => p.sessionId === m.sessionId && p.key === m.key && p.cwd === m.cwd && p.title === m.title)))
|
|
1263
|
+
throw new Error("Scoped context identity differs from the complete roster.");
|
|
1264
|
+
const roots = members ? selected.map((m) => m.cwd) : Object.keys(plan.contextInventories);
|
|
1265
|
+
for (const root of roots) {
|
|
1266
|
+
const inventory = plan.contextInventories[root];
|
|
1267
|
+
if (!inventory || JSON.stringify(snapshotMarkdown(root, fs, crypto, path)) !== JSON.stringify(inventory))
|
|
1268
|
+
throw new Error("Context or transcript changed during configuration: Markdown inventory " + root);
|
|
1269
|
+
}
|
|
1270
|
+
const files = members ? [...plan.globalContextFiles, ...selected.flatMap((m) => {
|
|
1271
|
+
const files2 = plan.roleContextFiles[m.sessionId];
|
|
1272
|
+
if (!files2?.length) throw new Error("Scoped role context proof is unavailable.");
|
|
1273
|
+
return files2;
|
|
1274
|
+
})] : Object.keys(plan.context);
|
|
1275
|
+
for (const file of files)
|
|
1276
|
+
if (!plan.context[file] || crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") !== plan.context[file])
|
|
1277
|
+
throw new Error("Context or transcript changed during configuration: " + file);
|
|
1278
|
+
}
|
|
1279
|
+
function qualifyPermissionConsent(policy, key, records, full) {
|
|
1280
|
+
const consent = policy.installationConsent;
|
|
1281
|
+
const intakeKey = consent?.intakeRoutes?.[key];
|
|
1282
|
+
if (consent?.format !== "topolo-pack-installation-consent" || consent.schemaVersion !== 1 || consent.authority !== "local-installation-consent" || consent.approved !== true || consent.pack !== policy.pack || typeof consent.contextKey !== "string" || !/^org:.+$/.test(consent.contextKey) || !consent.principalId || !["manual", "role-default", "full-access"].includes(consent.profile) || full && (consent.overrides?.[key] ?? consent.profile) !== "full-access" || !records.some((record) => record.key === intakeKey && record.key.includes("/intakes/")))
|
|
1283
|
+
throw new Error("Explicit organization-bound full access and owned intake consent required.");
|
|
1284
|
+
return { contextKey: consent.contextKey, intakeKey };
|
|
1285
|
+
}
|
|
1286
|
+
function nativePackExpression(pid, plan, journal, apply) {
|
|
1287
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("Explicit Claude main-process PID required.");
|
|
1288
|
+
const qualifier = CLAUDE_NATIVE_QUALIFIER;
|
|
1289
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1290
|
+
const smokeSource = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(existsSync);
|
|
1291
|
+
if (!smokeSource) throw new Error("Bundled owned smoke source unavailable.");
|
|
1292
|
+
return `(async () => {
|
|
1293
|
+
const expected = ${JSON.stringify({ pid, qualifier, journal, apply })};
|
|
1294
|
+
if (process.pid !== expected.pid || typeof process.getBuiltinModule !== 'function') throw new Error('Claude process identity or Node runtime differs.');
|
|
1295
|
+
const load = process.getBuiltinModule('module').createRequire(process.resourcesPath + '/app.asar/package.json');
|
|
1296
|
+
if (load('electron').app.getVersion() !== expected.qualifier.version) throw new Error('Unqualified Claude Desktop version.');
|
|
1297
|
+
const fs = load('node:fs'), crypto = load('node:crypto'), path = load('node:path');
|
|
1298
|
+
const base = process.resourcesPath + '/app.asar/.vite/build/';
|
|
1299
|
+
for (const [file, hash] of Object.entries(expected.qualifier.hashes)) {
|
|
1300
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(base + file)).digest('hex') !== hash) throw new Error('Claude native module hash changed: ' + file);
|
|
1301
|
+
}
|
|
1302
|
+
const manager = load(base + expected.qualifier.manager).claudeCodeSessionManager;
|
|
1303
|
+
const transport = load(base + expected.qualifier.sidebar);
|
|
1304
|
+
const checkpoint = value => {
|
|
1305
|
+
const fd = fs.openSync(expected.journal, 'a');
|
|
1306
|
+
try { fs.writeSync(fd, JSON.stringify(value) + '\\n'); fs.fsyncSync(fd); } finally { fs.closeSync(fd); }
|
|
1307
|
+
};
|
|
1308
|
+
const context = ${JSON.stringify(plan.context)};
|
|
1309
|
+
const inventories = ${JSON.stringify(plan.contextInventories)};
|
|
1310
|
+
const snapshotMarkdown = (${snapshotClaudeMarkdown.toString()});
|
|
1311
|
+
const contextPlan = {context, contextInventories: inventories, members: ${JSON.stringify(plan.members)},
|
|
1312
|
+
roleContextFiles: ${JSON.stringify(plan.roleContextFiles)}, globalContextFiles: ${JSON.stringify(plan.globalContextFiles)}};
|
|
1313
|
+
const verifyContext = members => (${verifyClaudePlanContext.toString()})(contextPlan, members, fs, crypto, path, snapshotMarkdown);
|
|
1314
|
+
const result = await (${nativeClaudePackOperation.toString()})(manager,
|
|
1315
|
+
value => transport.P(value, transport.M),
|
|
1316
|
+
{ members: ${JSON.stringify(plan.members)}, outgoing: ${JSON.stringify(plan.outgoing)},
|
|
1317
|
+
operationId: ${JSON.stringify(plan.operationId)}, mode: ${JSON.stringify(plan.recovery ? "recover" : plan.mode)},
|
|
1318
|
+
noEffects: ${JSON.stringify(plan.noEffects)},
|
|
1319
|
+
deadline: Date.now() + 45000 }, expected.apply, checkpoint, verifyContext);
|
|
1320
|
+
${plan.approvalRoutes.length ? `const routes = ${JSON.stringify(plan.approvalRoutes)};
|
|
1321
|
+
const fingerprint = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
1322
|
+
const qualifyNative = () => {
|
|
1323
|
+
if (process.pid !== expected.pid || load('electron').app.getVersion() !== expected.qualifier.version) throw new Error('Native process changed.');
|
|
1324
|
+
for (const [file, hash] of Object.entries(expected.qualifier.hashes))
|
|
1325
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(base + file)).digest('hex') !== hash) throw new Error('Native module changed.');
|
|
1326
|
+
};
|
|
1327
|
+
const smokeIdentity = fingerprint({pid: expected.pid, qualifier: expected.qualifier, routes});
|
|
1328
|
+
const invoke = op => ['smoke-start', 'smoke-read'].includes(op.kind) ?
|
|
1329
|
+
(${nativeClaudeOwnedSmokeOperation.toString()})(manager, ${JSON.stringify(plan.members)}, op,
|
|
1330
|
+
(action, payload) => (${runClaudeSmokeHelper.toString()})(${JSON.stringify(plan.libraryRoot)}, action, payload,
|
|
1331
|
+
${JSON.stringify(realpathSync(smokeSource))}, load('node:child_process').spawn), fingerprint, smokeIdentity, qualifyNative) :
|
|
1332
|
+
(${nativeClaudeApprovalOperation.toString()})(manager, routes, op, fingerprint,
|
|
1333
|
+
row => checkpoint({kind: 'approval', ...row}),
|
|
1334
|
+
fs.readFileSync(expected.journal, 'utf8').split('\\n').filter(Boolean).map(line => JSON.parse(line)));
|
|
1335
|
+
if (expected.apply) {
|
|
1336
|
+
const control = ${JSON.stringify(plan.control)};
|
|
1337
|
+
if (!control) throw new Error('Owned native control setup is unavailable.');
|
|
1338
|
+
const identity = fingerprint(routes);
|
|
1339
|
+
const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
|
|
1340
|
+
const listener = manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
1341
|
+
const incremental = ${JSON.stringify(plan.mode === "configure" && !plan.recovery)};
|
|
1342
|
+
const unchanged = listener?.identity === identity && listener.listener && !listener.auditFailed &&
|
|
1343
|
+
current?.descriptor.pid === expected.pid && current.descriptor.identity === identity &&
|
|
1344
|
+
current.descriptor.socket === control.socket && current.server?.listening === true && !current.failure;
|
|
1345
|
+
if (!incremental) {
|
|
1346
|
+
result.approvalRouting = await invoke({kind: 'install'});
|
|
1347
|
+
const stat = fs.lstatSync(control.secretPath);
|
|
1348
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
1349
|
+
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
1350
|
+
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
1351
|
+
{socket: control.socket, pid: expected.pid, identity}, secret.token,
|
|
1352
|
+
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
1353
|
+
} else if (unchanged) {
|
|
1354
|
+
// Re-attest the owned listener/control; do not replace matching hooks
|
|
1355
|
+
// or interrupt an in-flight request merely to configure idle settings.
|
|
1356
|
+
result.approvalRouting = {state: 'native-approval-listener-installed', roles: routes.length,
|
|
1357
|
+
identity, control: current.descriptor, unchanged: true};
|
|
1358
|
+
} else {
|
|
1359
|
+
qualifyNative();
|
|
1360
|
+
const deferred = [];
|
|
1361
|
+
for (const route of routes) {
|
|
1362
|
+
const descriptor = await manager.getSessionWhenLoaded(route.sessionId, 'topolo-agent-pack-approval', {warnOnMiss: false});
|
|
1363
|
+
const session = await manager.getSession(route.sessionId);
|
|
1364
|
+
if (!descriptor || descriptor.sessionId !== route.sessionId || descriptor.cwd !== route.cwd || descriptor.title !== route.title ||
|
|
1365
|
+
descriptor.isArchived || descriptor.backend?.kind !== 'local' || descriptor.backend.remoteTarget || descriptor.worktreePath ||
|
|
1366
|
+
descriptor.worktreeLazy || descriptor.spawnedFrom || !session || session.sessionId !== route.sessionId ||
|
|
1367
|
+
session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
1368
|
+
throw new Error('Native approval route ownership changed.');
|
|
1369
|
+
if (session.pendingToolPermissions?.length || descriptor.pendingToolPermissions?.length)
|
|
1370
|
+
deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'pending-permissions'});
|
|
1371
|
+
}
|
|
1372
|
+
if (current?.active || current?.failure || listener?.auditFailed ||
|
|
1373
|
+
(listener && listener.identity !== identity) ||
|
|
1374
|
+
(current && (current.descriptor.pid !== expected.pid || current.descriptor.identity !== identity)))
|
|
1375
|
+
for (const route of routes) deferred.push({key: route.key, fields: ['intakeHooks'], reason: 'existing-control-custody'});
|
|
1376
|
+
if (deferred.length) {
|
|
1377
|
+
result.routingDeferred = deferred;
|
|
1378
|
+
result.state = 'native-settings-partial';result.complete = false;
|
|
1379
|
+
checkpoint({kind: 'routing-deferred', deferred, at: Date.now()});
|
|
1380
|
+
} else {
|
|
1381
|
+
verifyContext(result.after.filter(row => !row.busy).map(row => contextPlan.members.find(m => m.sessionId === row.sessionId)));
|
|
1382
|
+
result.approvalRouting = await invoke({kind: 'install'});
|
|
1383
|
+
const stat = fs.lstatSync(control.secretPath);
|
|
1384
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
1385
|
+
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
1386
|
+
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
1387
|
+
{socket: control.socket, pid: expected.pid, identity}, secret.token,
|
|
1388
|
+
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
} else if (${JSON.stringify(plan.recovery === true && !plan.noEffects)}) {
|
|
1392
|
+
const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
|
|
1393
|
+
if (!current || current.descriptor.pid !== expected.pid || current.descriptor.identity !== fingerprint(routes))
|
|
1394
|
+
throw new Error('Native intake control is not reconciled.');
|
|
1395
|
+
result.approvalRouting = {state: 'native-approval-listener-installed', roles: routes.length, control: current.descriptor};
|
|
1396
|
+
}` : ""}
|
|
1397
|
+
return result;
|
|
1398
|
+
})()`;
|
|
1399
|
+
}
|
|
1400
|
+
function evaluateClaudeMain(endpoint, expression, socketFactory = (url) => new WebSocket(url)) {
|
|
1401
|
+
const url = validateInspectorEndpoint(endpoint);
|
|
1402
|
+
return new Promise((resolveResult, reject) => {
|
|
1403
|
+
const socket = socketFactory(url);
|
|
1404
|
+
let finished = false;
|
|
1405
|
+
const finish = (error, result) => {
|
|
1406
|
+
if (finished) return;
|
|
1407
|
+
finished = true;
|
|
1408
|
+
clearTimeout(timer);
|
|
1409
|
+
socket.close();
|
|
1410
|
+
if (error) reject(error);
|
|
1411
|
+
else resolveResult(result);
|
|
1412
|
+
};
|
|
1413
|
+
const timer = setTimeout(() => finish(new Error("Claude backend timed out; reconcile the journal before retry.")), 55e3);
|
|
1414
|
+
socket.addEventListener("open", () => socket.send(JSON.stringify({
|
|
1415
|
+
id: 1,
|
|
1416
|
+
method: "Runtime.evaluate",
|
|
1417
|
+
params: { expression, awaitPromise: true, returnByValue: true }
|
|
1418
|
+
})));
|
|
1419
|
+
socket.addEventListener("message", (event) => {
|
|
1420
|
+
try {
|
|
1421
|
+
const reply = JSON.parse(String(event.data));
|
|
1422
|
+
if (reply.id !== 1) return;
|
|
1423
|
+
if (reply.error || reply.result?.exceptionDetails)
|
|
1424
|
+
finish(new Error(reply.error?.message || reply.result.exceptionDetails.exception?.description || "Native operation failed; reconcile the journal."));
|
|
1425
|
+
else finish(void 0, reply.result.result.value);
|
|
1426
|
+
} catch {
|
|
1427
|
+
finish(new Error("Invalid Claude backend reply; reconcile the journal."));
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
socket.addEventListener("error", () => finish(new Error("Claude backend connection failed.")));
|
|
1431
|
+
socket.addEventListener("close", () => finish(new Error("Claude backend disconnected; reconcile the journal.")));
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
async function lockClaudeLibrary(root) {
|
|
1435
|
+
const script = 'import os,sys,fcntl\nf=os.open(sys.argv[1],os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600)\nfcntl.flock(f,fcntl.LOCK_EX|fcntl.LOCK_NB)\nprint("locked",flush=True)\nsys.stdin.buffer.read()\nos.close(f)';
|
|
1436
|
+
const child = spawn("python3", ["-c", script, resolve(root, ".lock")], { stdio: ["pipe", "pipe", "pipe"] });
|
|
1437
|
+
await new Promise((done, fail) => {
|
|
1438
|
+
let ready = false;
|
|
1439
|
+
const timer = setTimeout(() => {
|
|
1440
|
+
child.kill();
|
|
1441
|
+
fail(new Error("Pack library lock timed out."));
|
|
1442
|
+
}, 5e3);
|
|
1443
|
+
child.once("error", (error) => {
|
|
1444
|
+
clearTimeout(timer);
|
|
1445
|
+
fail(error);
|
|
1446
|
+
});
|
|
1447
|
+
child.stdout.once("data", (data) => {
|
|
1448
|
+
if (String(data).trim() === "locked") {
|
|
1449
|
+
ready = true;
|
|
1450
|
+
clearTimeout(timer);
|
|
1451
|
+
done();
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
child.once("exit", () => {
|
|
1455
|
+
clearTimeout(timer);
|
|
1456
|
+
if (!ready) fail(new Error("Another operation owns the pack library, or its lock is unavailable."));
|
|
1457
|
+
});
|
|
1458
|
+
child.stderr.resume();
|
|
1459
|
+
});
|
|
1460
|
+
return async () => {
|
|
1461
|
+
await new Promise((done) => {
|
|
1462
|
+
if (child.exitCode !== null || child.signalCode !== null) done();
|
|
1463
|
+
else {
|
|
1464
|
+
child.once("exit", () => done());
|
|
1465
|
+
child.stdin.end();
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function durableRecord(path, value, flag = "a") {
|
|
1471
|
+
const fd = openSync(path, flag, 384);
|
|
1472
|
+
try {
|
|
1473
|
+
writeFileSync(fd, JSON.stringify(value) + "\n");
|
|
1474
|
+
fsyncSync(fd);
|
|
1475
|
+
} finally {
|
|
1476
|
+
closeSync(fd);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function syncDirectory(path) {
|
|
1480
|
+
const fd = openSync(path, "r");
|
|
1481
|
+
try {
|
|
1482
|
+
fsyncSync(fd);
|
|
1483
|
+
} finally {
|
|
1484
|
+
closeSync(fd);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
function verifyPlanContext(plan, members) {
|
|
1488
|
+
verifyClaudePlanContext(plan, members, filesystem, cryptography, paths, snapshotClaudeMarkdown);
|
|
1489
|
+
}
|
|
1490
|
+
function verifyClaudeOrganizationPolicy(root, pack, consent, run = spawnSync, locate = existsSync) {
|
|
1491
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1492
|
+
const bundled = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(locate);
|
|
1493
|
+
if (!bundled) throw new Error("Bundled organization policy loader unavailable.");
|
|
1494
|
+
const source = realpathSync(bundled);
|
|
1495
|
+
const script = 'import json,sys\nsys.path.insert(0,sys.argv[4])\nfrom topolo_agent_pack import permissions,topolo\nfrom topolo_agent_pack.storage import Library\nlib=Library(sys.argv[1]);manifest,_=lib.load(sys.argv[2]);c=permissions.load(lib,sys.argv[2],manifest["agents"])\nif c!=json.loads(sys.argv[3]):raise RuntimeError("Installation consent changed")\np,proof=topolo.organization_execution_policy(topolo.Client(c["environment"],c["contextKey"]),c,manifest["agents"],c.get("agentWorkspaceId"))\npermissions.authorize_native_scope(proof)\nprint(json.dumps(proof))';
|
|
1496
|
+
const result = run(
|
|
1497
|
+
"python3",
|
|
1498
|
+
["-I", "-B", "-c", script, root, pack, JSON.stringify(consent), source],
|
|
1499
|
+
{ encoding: "utf8", timeout: 2e5, cwd: source, env: { ...process.env } }
|
|
1500
|
+
);
|
|
1501
|
+
if (result.error || result.status !== 0) throw new Error("Fresh authenticated organization policy qualification failed; no native permission widening.");
|
|
1502
|
+
const proof = JSON.parse(result.stdout);
|
|
1503
|
+
if (proof.profileCeilingVerified !== true || proof.contextKey !== consent.contextKey || proof.scopePolicyMode !== "unrestricted" || proof.toolActionRestrictionRequested !== false)
|
|
1504
|
+
throw new Error("Organization profile ceiling proof differs.");
|
|
1505
|
+
return proof;
|
|
1506
|
+
}
|
|
1507
|
+
async function configureClaudePack(options, evaluate = evaluateClaudeMain, qualify = verifyClaudeOrganizationPolicy) {
|
|
1508
|
+
validateInspectorEndpoint(options.endpoint);
|
|
1509
|
+
const root = realpathSync(options.store);
|
|
1510
|
+
if (lstatSync(resolve(root, "claude")).isSymbolicLink()) throw new Error("Claude library must not be a symlink.");
|
|
1511
|
+
const release = await lockClaudeLibrary(root);
|
|
1512
|
+
try {
|
|
1513
|
+
const markerPath = resolve(root, "claude/native-operation.json");
|
|
1514
|
+
const recovering = options.mode === "recover";
|
|
1515
|
+
if (recovering && options.apply) throw new Error("Recovery is read-only; --apply is not permitted.");
|
|
1516
|
+
if (!recovering && existsSync(markerPath)) throw new Error("An unresolved Claude operation requires read-only recovery before another operation.");
|
|
1517
|
+
const directory = resolve(root, "claude/journals");
|
|
1518
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
1519
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error("Claude journal directory must not be a symlink.");
|
|
1520
|
+
const operationId = randomUUID();
|
|
1521
|
+
let plan;
|
|
1522
|
+
let journal;
|
|
1523
|
+
let marker;
|
|
1524
|
+
if (recovering) {
|
|
1525
|
+
if (!existsSync(markerPath) || lstatSync(markerPath).isSymbolicLink()) throw new Error("No owned unresolved Claude operation is available.");
|
|
1526
|
+
marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
1527
|
+
journal = realpathSync(marker.journal);
|
|
1528
|
+
if (!journal.startsWith(realpathSync(directory) + sep)) throw new Error("Recovery journal is outside the owned library.");
|
|
1529
|
+
const lines = readFileSync(journal, "utf8").split("\n").filter(Boolean);
|
|
1530
|
+
const line = lines[0];
|
|
1531
|
+
if (createHash("sha256").update(line).digest("hex") !== marker.planHash) throw new Error("Recovery plan identity changed.");
|
|
1532
|
+
const record = JSON.parse(line);
|
|
1533
|
+
if (record.pack !== options.pack || record.operationId !== marker.operationId || !record.apply) throw new Error("Recovery pack or operation identity differs.");
|
|
1534
|
+
plan = { ...record, approvalRoutes: record.approvalRoutes ?? [], recovery: true, noEffects: marker.format === 2 && marker.dispatchState === "not-dispatched" && !lines.map((line2) => JSON.parse(line2)).some((r) => r.kind === "attempt") };
|
|
1535
|
+
} else {
|
|
1536
|
+
plan = { ...prepareClaudeNativePlan(root, options.pack, options.policy, options.mode === "switch" ? "switch" : "configure"), operationId };
|
|
1537
|
+
if (options.apply && plan.approvalRoutes.length) {
|
|
1538
|
+
const connectionPath = resolve(root, "claude/approval-connection.json");
|
|
1539
|
+
if (plan.mode === "configure" && existsSync(connectionPath)) {
|
|
1540
|
+
const entry = lstatSync(connectionPath);
|
|
1541
|
+
if (!entry.isFile() || entry.isSymbolicLink() || typeof process.getuid !== "function" || entry.uid !== process.getuid() || entry.mode & 63)
|
|
1542
|
+
throw new Error("Unowned native approval connection.");
|
|
1543
|
+
const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
|
|
1544
|
+
if (connection.pid === options.pid && connection.pack === options.pack && JSON.stringify(connection.routes) === JSON.stringify(plan.approvalRoutes) && JSON.stringify(connection.installationConsent) === JSON.stringify(plan.installationConsent)) {
|
|
1545
|
+
if (typeof connection.secretPath !== "string" || typeof connection.socket !== "string" || !resolve(connection.secretPath).startsWith(realpathSync(resolve(root, "claude/private")) + sep) || realpathSync(connection.secretPath) !== resolve(connection.secretPath) || dirname(resolve(connection.socket)) !== resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16)))
|
|
1546
|
+
throw new Error("Native approval connection is outside owned control paths.");
|
|
1547
|
+
const secret = lstatSync(connection.secretPath);
|
|
1548
|
+
if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
|
|
1549
|
+
throw new Error("Unowned native control secret.");
|
|
1550
|
+
plan.control = { socket: connection.socket, secretPath: connection.secretPath };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (!plan.control) {
|
|
1554
|
+
const directory2 = resolve(root, "claude/private");
|
|
1555
|
+
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
1556
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
1557
|
+
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
1558
|
+
for (const path of [directory2, socketDirectory]) {
|
|
1559
|
+
const stat = lstatSync(path);
|
|
1560
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
1561
|
+
throw new Error("Native control directory must be owned and private.");
|
|
1562
|
+
}
|
|
1563
|
+
plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
|
|
1564
|
+
durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
1565
|
+
syncDirectory(directory2);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
journal = resolve(directory, operationId + ".jsonl");
|
|
1569
|
+
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
1570
|
+
durableRecord(journal, record, "wx");
|
|
1571
|
+
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash: createHash("sha256").update(JSON.stringify(record)).digest("hex") };
|
|
1572
|
+
if (options.apply) durableRecord(markerPath, marker, "wx");
|
|
1573
|
+
syncDirectory(directory);
|
|
1574
|
+
syncDirectory(resolve(root, "claude"));
|
|
1575
|
+
}
|
|
1576
|
+
try {
|
|
1577
|
+
const incremental = !recovering && plan.mode === "configure";
|
|
1578
|
+
verifyPlanContext(plan, incremental ? [] : void 0);
|
|
1579
|
+
const organizationPolicy = options.apply && plan.approvalRoutes.length ? qualify(root, options.pack, plan.installationConsent) : null;
|
|
1580
|
+
if (options.apply) {
|
|
1581
|
+
const dispatched = { ...marker, dispatchState: "may-have-effects" };
|
|
1582
|
+
const temporaryMarker = resolve(root, "claude/native-dispatch-" + operationId + ".json");
|
|
1583
|
+
durableRecord(temporaryMarker, dispatched, "wx");
|
|
1584
|
+
renameSync(temporaryMarker, markerPath);
|
|
1585
|
+
syncDirectory(resolve(root, "claude"));
|
|
1586
|
+
marker = dispatched;
|
|
1587
|
+
}
|
|
1588
|
+
const result = await evaluate(options.endpoint, nativePackExpression(options.pid, plan, journal, options.apply));
|
|
1589
|
+
if (incremental && result.contextVerifiedSessionIds !== void 0) {
|
|
1590
|
+
if (!Array.isArray(result.contextVerifiedSessionIds) || new Set(result.contextVerifiedSessionIds).size !== result.contextVerifiedSessionIds.length || result.contextVerifiedSessionIds.some((id) => !plan.members.some((m) => m.sessionId === id)))
|
|
1591
|
+
throw new Error("Native scoped context identities differ.");
|
|
1592
|
+
const observations = result.busyObservations ?? [];
|
|
1593
|
+
if (!Array.isArray(observations) || observations.some((row) => !plan.members.some((m) => m.key === row.key && m.sessionId === row.sessionId) || !Array.isArray(row.reasons) || !row.reasons.length || row.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
|
|
1594
|
+
throw new Error("Native busy context attestation differs.");
|
|
1595
|
+
for (let i = 0; i < plan.members.length; i++)
|
|
1596
|
+
if (!result.contextVerifiedSessionIds.includes(plan.members[i].sessionId) && result.after?.[i]?.busy !== true && !observations.some((row) => row.sessionId === plan.members[i].sessionId))
|
|
1597
|
+
throw new Error("Native result omits idle context qualification.");
|
|
1598
|
+
}
|
|
1599
|
+
const contextMembers = incremental && Array.isArray(result.after) ? plan.members.filter((m, i) => result.after[i]?.busy !== true && (result.contextVerifiedSessionIds === void 0 || result.contextVerifiedSessionIds.includes(m.sessionId))) : void 0;
|
|
1600
|
+
verifyPlanContext(plan, contextMembers);
|
|
1601
|
+
if (options.apply || recovering) {
|
|
1602
|
+
if (plan.noEffects) {
|
|
1603
|
+
if (result.state !== "native-reconciled-no-effects" || result.nativeEffectsReplayed !== false || result.after?.length !== plan.members.length || result.after.some((actual, i) => ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== plan.members[i][key])))
|
|
1604
|
+
throw new Error("No-effect recovery has no qualified native acknowledgement.");
|
|
1605
|
+
const activePath2 = resolve(root, "claude/active.json");
|
|
1606
|
+
const current2 = existsSync(activePath2) ? JSON.parse(readFileSync(activePath2, "utf8")) : null;
|
|
1607
|
+
if (JSON.stringify(current2) !== JSON.stringify(plan.activeBefore)) throw new Error("Active roster changed during no-effect recovery.");
|
|
1608
|
+
durableRecord(journal, { kind: "context-verified", context: plan.context, nativeEffects: "none-attempted" });
|
|
1609
|
+
if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
|
|
1610
|
+
unlinkSync(markerPath);
|
|
1611
|
+
syncDirectory(resolve(root, "claude"));
|
|
1612
|
+
return { ...result, journal, contextPreserved: true };
|
|
1613
|
+
}
|
|
1614
|
+
if (incremental && result.state === "native-settings-partial") {
|
|
1615
|
+
const settings = ["group", "pinned", "model", "effort", "permissionMode"];
|
|
1616
|
+
if (result.after?.length !== plan.members.length || !Array.isArray(result.deferred) || !result.deferred.length && !result.routingDeferred?.length) throw new Error("Native partial result has no qualified complete roster.");
|
|
1617
|
+
if (new Set(result.deferred.map((d) => d.sessionId)).size !== result.deferred.length || result.deferred.some((d) => !plan.members.some((m) => m.key === d.key && m.sessionId === d.sessionId) || !Array.isArray(d.fields) || !d.fields.length || new Set(d.fields).size !== d.fields.length || d.fields.some((field) => !settings.includes(field)) || !Array.isArray(d.reasons) || !d.reasons.length || d.reasons.some((reason) => !["running", "starting", "losable", "pending-permissions"].includes(reason))))
|
|
1618
|
+
throw new Error("Native deferred identity or fields differ.");
|
|
1619
|
+
if (result.routingDeferred !== void 0 && (!Array.isArray(result.routingDeferred) || result.routingDeferred.some((d) => !plan.approvalRoutes.some((r) => r.key === d.key) || JSON.stringify(d.fields) !== JSON.stringify(["intakeHooks"]) || !["pending-permissions", "existing-control-custody"].includes(d.reason))))
|
|
1620
|
+
throw new Error("Native deferred routing ownership differs.");
|
|
1621
|
+
for (let i = 0; i < plan.members.length; i++) {
|
|
1622
|
+
const wanted = plan.members[i], actual = result.after[i];
|
|
1623
|
+
if (actual.archived !== false || ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== wanted[key]))
|
|
1624
|
+
throw new Error("Native partial roster identity differs.");
|
|
1625
|
+
const mismatch = settings.filter((key) => actual[key] !== wanted[key]);
|
|
1626
|
+
const deferred = result.deferred.find((d) => d.sessionId === wanted.sessionId);
|
|
1627
|
+
if (JSON.stringify(mismatch.slice().sort()) !== JSON.stringify((deferred?.fields ?? []).slice().sort()))
|
|
1628
|
+
throw new Error("Native partial result hides an undeferred mismatch.");
|
|
1629
|
+
}
|
|
1630
|
+
const activePath2 = resolve(root, "claude/active.json");
|
|
1631
|
+
const active2 = existsSync(activePath2) ? JSON.parse(readFileSync(activePath2, "utf8")) : null;
|
|
1632
|
+
if (JSON.stringify(active2) !== JSON.stringify(plan.activeBefore)) throw new Error("Active roster changed during partial configuration.");
|
|
1633
|
+
durableRecord(journal, {
|
|
1634
|
+
kind: "partial-verified",
|
|
1635
|
+
after: result.after,
|
|
1636
|
+
deferred: result.deferred,
|
|
1637
|
+
routingDeferred: result.routingDeferred ?? [],
|
|
1638
|
+
appliedFields: result.appliedFields ?? [],
|
|
1639
|
+
activeUnchanged: true
|
|
1640
|
+
});
|
|
1641
|
+
if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
|
|
1642
|
+
unlinkSync(markerPath);
|
|
1643
|
+
syncDirectory(resolve(root, "claude"));
|
|
1644
|
+
return {
|
|
1645
|
+
...result,
|
|
1646
|
+
complete: false,
|
|
1647
|
+
journal,
|
|
1648
|
+
contextPreserved: contextMembers?.length === plan.members.length,
|
|
1649
|
+
idleContextsVerified: contextMembers?.map((m) => m.key),
|
|
1650
|
+
activeRosterUnchanged: true,
|
|
1651
|
+
resume: "explicit-configure-only"
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
if (result.state !== (recovering ? "native-reconciled" : "native-settings-verified") || result.after?.length !== plan.members.length) throw new Error("Native result has no complete verified roster.");
|
|
1655
|
+
for (let i = 0; i < plan.members.length; i++) {
|
|
1656
|
+
const wanted = plan.members[i];
|
|
1657
|
+
const actual = result.after[i];
|
|
1658
|
+
if (actual.archived !== false || ["key", "sessionId", "cwd", "title", "group", "pinned", "model", "effort", "permissionMode"].some((key) => actual[key] !== wanted[key])) throw new Error("Native result settings do not match the reviewed roster.");
|
|
1659
|
+
}
|
|
1660
|
+
if (plan.outgoing.length && (result.outgoingAfter?.length !== plan.outgoing.length || result.outgoingAfter.some((actual, i) => actual.archived !== true || ["key", "sessionId", "cwd", "title"].some((key) => actual[key] !== plan.outgoing[i][key]))))
|
|
1661
|
+
throw new Error("Native outgoing archive result does not match the reviewed roster.");
|
|
1662
|
+
if (plan.approvalRoutes.length && (options.apply || recovering) && (result.approvalRouting?.state !== "native-approval-listener-installed" || result.approvalRouting.roles !== plan.members.length || result.approvalRouting.control?.pid !== options.pid || result.approvalRouting.control?.socket !== plan.control?.socket))
|
|
1663
|
+
throw new Error("Complete native intake routing acknowledgement is unavailable.");
|
|
1664
|
+
const activePath = resolve(root, "claude/active.json");
|
|
1665
|
+
const current = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
1666
|
+
const active = { operationId: plan.operationId, pack: options.pack, keys: plan.members.map((m) => m.key), sourceRevision: plan.sourceRevision };
|
|
1667
|
+
if (JSON.stringify(current) !== JSON.stringify(plan.activeBefore) && JSON.stringify(current) !== JSON.stringify(active))
|
|
1668
|
+
throw new Error("Verified active roster changed outside this operation.");
|
|
1669
|
+
const temporary = resolve(root, "claude/active-" + plan.operationId + ".json");
|
|
1670
|
+
if (existsSync(temporary)) {
|
|
1671
|
+
if (lstatSync(temporary).isSymbolicLink() || readFileSync(temporary, "utf8") !== JSON.stringify(active) + "\n") throw new Error("Active commit identity changed.");
|
|
1672
|
+
} else durableRecord(temporary, active, "wx");
|
|
1673
|
+
renameSync(temporary, activePath);
|
|
1674
|
+
syncDirectory(resolve(root, "claude"));
|
|
1675
|
+
durableRecord(journal, { kind: "context-verified", context: plan.context, active });
|
|
1676
|
+
if ((options.apply || recovering) && plan.approvalRoutes.length) {
|
|
1677
|
+
const target = resolve(root, "claude/approval-connection.json");
|
|
1678
|
+
const temporaryConnection = resolve(root, "claude/approval-connection-" + plan.operationId + ".json");
|
|
1679
|
+
durableRecord(temporaryConnection, {
|
|
1680
|
+
pid: options.pid,
|
|
1681
|
+
socket: plan.control.socket,
|
|
1682
|
+
secretPath: plan.control.secretPath,
|
|
1683
|
+
identity: result.approvalRouting.control.identity,
|
|
1684
|
+
pack: options.pack,
|
|
1685
|
+
installationConsent: plan.installationConsent,
|
|
1686
|
+
routes: plan.approvalRoutes,
|
|
1687
|
+
journal,
|
|
1688
|
+
organizationPolicy
|
|
1689
|
+
}, "wx");
|
|
1690
|
+
renameSync(temporaryConnection, target);
|
|
1691
|
+
syncDirectory(resolve(root, "claude"));
|
|
1692
|
+
}
|
|
1693
|
+
if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
|
|
1694
|
+
unlinkSync(markerPath);
|
|
1695
|
+
syncDirectory(resolve(root, "claude"));
|
|
1696
|
+
} else durableRecord(journal, { kind: "context-verified", context: plan.context });
|
|
1697
|
+
return {
|
|
1698
|
+
...result,
|
|
1699
|
+
journal,
|
|
1700
|
+
contextPreserved: !incremental || contextMembers?.length === plan.members.length,
|
|
1701
|
+
...incremental ? { idleContextsVerified: contextMembers?.map((m) => m.key) } : {}
|
|
1702
|
+
};
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
durableRecord(journal, { kind: "recovery-required" });
|
|
1705
|
+
throw new Error(`Claude configuration is incomplete. Reconcile ${journal}. ${error instanceof Error ? error.message : String(error)}`);
|
|
1706
|
+
}
|
|
1707
|
+
} finally {
|
|
1708
|
+
await release();
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function runClaudeSmokeHelper(root, action, payload, source, launch = spawn) {
|
|
1712
|
+
if (!["prepare", "arm", "mark", "read", "verify"].includes(action) || typeof root !== "string" || typeof source !== "string")
|
|
1713
|
+
return Promise.reject(new Error("Closed owned smoke helper required."));
|
|
1714
|
+
const script = 'import sys,json\nsys.path.insert(0,sys.argv[3])\nfrom topolo_agent_pack.storage import Library\nfrom topolo_agent_pack.live import OwnedSmoke\ns=OwnedSmoke(Library(sys.argv[1]),"claude")\np=json.loads(sys.stdin.read(16384))\na=sys.argv[2]\nr=s.prepare(**p) if a=="prepare" else s.arm(**p) if a=="arm" else s.mark(**p) if a=="mark" else s.verify(p["operation"]) if a=="verify" else s.read(p["operation"])\nprint(json.dumps(r))';
|
|
1715
|
+
return new Promise((done, fail) => {
|
|
1716
|
+
const child = launch("python3", ["-I", "-B", "-c", script, root, action, source], { stdio: ["pipe", "pipe", "pipe"] });
|
|
1717
|
+
let output = "";
|
|
1718
|
+
let settled = false;
|
|
1719
|
+
const finish = (error, value) => {
|
|
1720
|
+
if (settled) return;
|
|
1721
|
+
settled = true;
|
|
1722
|
+
clearTimeout(timer);
|
|
1723
|
+
error ? fail(error) : done(value);
|
|
1724
|
+
};
|
|
1725
|
+
const timer = setTimeout(() => {
|
|
1726
|
+
child.kill("SIGTERM");
|
|
1727
|
+
finish(new Error("Owned smoke helper timed out; reconcile without replay."));
|
|
1728
|
+
}, 6e4);
|
|
1729
|
+
child.stdout.setEncoding("utf8");
|
|
1730
|
+
child.stderr.resume();
|
|
1731
|
+
child.stdout.on("data", (data) => {
|
|
1732
|
+
output += data;
|
|
1733
|
+
if (Buffer.byteLength(output) > 1048576) {
|
|
1734
|
+
child.kill("SIGTERM");
|
|
1735
|
+
finish(new Error("Owned smoke evidence exceeds bounded transport."));
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
child.on("error", () => finish(new Error("Bundled owned smoke helper unavailable.")));
|
|
1739
|
+
child.on("close", (code) => {
|
|
1740
|
+
if (settled) return;
|
|
1741
|
+
if (code !== 0) return finish(new Error("Owned smoke qualification refused; preserve its journal."));
|
|
1742
|
+
try {
|
|
1743
|
+
finish(void 0, JSON.parse(output));
|
|
1744
|
+
} catch {
|
|
1745
|
+
finish(new Error("Invalid owned smoke helper result."));
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
child.stdin.on("error", () => finish(new Error("Owned smoke helper input failed; reconcile without replay.")));
|
|
1749
|
+
child.stdin.end(JSON.stringify(payload));
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
function requestClaudeApprovalControl(connection, operation, token, connect = createConnection) {
|
|
1753
|
+
const parent = lstatSync(dirname(connection.socket));
|
|
1754
|
+
const socket = lstatSync(connection.socket);
|
|
1755
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || parent.mode & 63 || !socket.isSocket() || socket.uid !== process.getuid?.() || socket.mode & 63)
|
|
1756
|
+
throw new Error("Unowned native control socket.");
|
|
1757
|
+
return new Promise((done, fail) => {
|
|
1758
|
+
const client = connect({ path: connection.socket });
|
|
1759
|
+
let reply = "";
|
|
1760
|
+
let finished = false;
|
|
1761
|
+
const finish = (error, result) => {
|
|
1762
|
+
if (finished) return;
|
|
1763
|
+
finished = true;
|
|
1764
|
+
client.destroy();
|
|
1765
|
+
error ? fail(error) : done(result);
|
|
1766
|
+
};
|
|
1767
|
+
client.setEncoding("utf8");
|
|
1768
|
+
client.setTimeout(connection.laneId ? 24e4 : operation.kind === "smoke-start" ? 6e4 : 1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
1769
|
+
client.on("connect", () => client.write(JSON.stringify({ token, pid: connection.pid, identity: connection.identity, operation }) + "\n"));
|
|
1770
|
+
client.on("data", (chunk) => {
|
|
1771
|
+
reply += chunk;
|
|
1772
|
+
if (Buffer.byteLength(reply) > 262144) return finish(new Error("Native approval response exceeds bounded transport."));
|
|
1773
|
+
if (!reply.includes("\n")) return;
|
|
1774
|
+
try {
|
|
1775
|
+
const result = JSON.parse(reply);
|
|
1776
|
+
finish(result.error ? new Error(result.error) : void 0, result.result);
|
|
1777
|
+
} catch {
|
|
1778
|
+
finish(new Error("Invalid native approval response."));
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
client.on("error", () => finish(new Error("Existing native approval control is unavailable.")));
|
|
1782
|
+
client.on("close", () => finish(new Error("Native approval response was lost; reconcile without replay.")));
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
async function operateClaudeApprovals(options, request = requestClaudeApprovalControl, qualify = verifyClaudeOrganizationPolicy) {
|
|
1786
|
+
const root = realpathSync(options.store);
|
|
1787
|
+
const file = resolve(root, "claude/approval-connection.json");
|
|
1788
|
+
if (lstatSync(file).isSymbolicLink()) throw new Error("Unowned approval connection.");
|
|
1789
|
+
const connection = JSON.parse(readFileSync(file, "utf8"));
|
|
1790
|
+
if (!Number.isSafeInteger(connection.pid) || connection.pid < 1 || !Array.isArray(connection.routes))
|
|
1791
|
+
throw new Error("Unqualified native approval connection.");
|
|
1792
|
+
if (options.kind === "respond" && options.decision === "once")
|
|
1793
|
+
qualify(root, connection.pack, connection.installationConsent);
|
|
1794
|
+
const journal = realpathSync(connection.journal);
|
|
1795
|
+
if (!journal.startsWith(realpathSync(resolve(root, "claude/journals")) + sep)) throw new Error("Unowned approval journal.");
|
|
1796
|
+
const privateDirectory = resolve(root, "claude/private");
|
|
1797
|
+
const secretPath = realpathSync(connection.secretPath);
|
|
1798
|
+
const stat = lstatSync(connection.secretPath);
|
|
1799
|
+
if (!secretPath.startsWith(realpathSync(privateDirectory) + sep) || !stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63) throw new Error("Unowned native control secret.");
|
|
1800
|
+
const secret = JSON.parse(readFileSync(secretPath, "utf8"));
|
|
1801
|
+
if (!/^[a-f0-9]{64}$/.test(secret.token) || !/^[a-f0-9]{64}$/.test(connection.identity)) throw new Error("Native control identity is invalid.");
|
|
1802
|
+
if (options.kind === "smoke-start" || options.kind === "smoke-read") return request(
|
|
1803
|
+
connection,
|
|
1804
|
+
{
|
|
1805
|
+
kind: options.kind,
|
|
1806
|
+
operationId: options.operationId,
|
|
1807
|
+
...options.kind === "smoke-start" ? { key: options.key, caseId: options.caseId } : {}
|
|
1808
|
+
},
|
|
1809
|
+
secret.token
|
|
1810
|
+
);
|
|
1811
|
+
return request(connection, {
|
|
1812
|
+
kind: options.kind,
|
|
1813
|
+
key: options.key,
|
|
1814
|
+
intake: options.intake,
|
|
1815
|
+
fingerprint: options.fingerprint,
|
|
1816
|
+
decision: options.decision,
|
|
1817
|
+
humanDecisionRecorded: options.humanDecisionRecorded
|
|
1818
|
+
}, secret.token);
|
|
1819
|
+
}
|
|
1820
|
+
async function operateClaudeSmoke(options, request = requestClaudeApprovalControl) {
|
|
1821
|
+
const fields = options.kind === "smoke-start" ? ["store", "kind", "operationId", "key", "caseId"] : ["store", "kind", "operationId"];
|
|
1822
|
+
if (!["smoke-start", "smoke-read"].includes(options.kind) || Object.keys(options).some((k) => !fields.includes(k)) || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(options.operationId) || options.kind === "smoke-start" && (!/^[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+$/.test(options.key ?? "") || !["stg-context-memory-bytes", "stg-peer-ack", "prod-role-context-ack"].includes(options.caseId ?? "")))
|
|
1823
|
+
throw new Error("Exact closed native smoke fields required.");
|
|
1824
|
+
return operateClaudeApprovals(options, request);
|
|
1825
|
+
}
|
|
1826
|
+
function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
1827
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(pack) || !/^[a-z0-9][a-z0-9-]*$/.test(laneId)) throw new Error("Exact pack and lane IDs required.");
|
|
1828
|
+
const root = realpathSync(store);
|
|
1829
|
+
const file = resolve(root, "dynamic-lanes/claude", laneId + ".json");
|
|
1830
|
+
const owned = (path) => {
|
|
1831
|
+
const info = lstatSync(path);
|
|
1832
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.mode & 18)
|
|
1833
|
+
throw new Error("Dynamic lane requires an owned regular context record.");
|
|
1834
|
+
return path;
|
|
1835
|
+
};
|
|
1836
|
+
owned(file);
|
|
1837
|
+
if (lstatSync(file).size > 131072) throw new Error("Dynamic lane record exceeds its schema bound.");
|
|
1838
|
+
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
1839
|
+
const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
|
|
1840
|
+
const digest = (value) => createHash("sha256").update(canonical(value)).digest("hex");
|
|
1841
|
+
if (record.format !== "topolo-dynamic-lane" || record.schemaVersion !== 1 || record.harness !== "claude" || record.laneId !== laneId || record.planDigest !== digest(record.plan) || record.creationReceiptDigest !== digest(record.creationReceipt) || typeof record.nativeTitle !== "string" || !record.nativeTitle || record.nativeTitle.length > 256 || record.plan.role.packId !== pack || record.plan.nativeParentId !== record.plan.intake.nativeId)
|
|
1842
|
+
throw new Error("Durable dynamic child identity or plan digest differs.");
|
|
1843
|
+
const stateFile = owned(resolve(root, "claude/state.json"));
|
|
1844
|
+
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
1845
|
+
const policyFile = owned(realpathSync(policyPath));
|
|
1846
|
+
const policy = JSON.parse(readFileSync(policyFile, "utf8"));
|
|
1847
|
+
if (state.harness !== "claude" || policy.format !== "topolo-agent-execution-policy" || policy.pack !== pack || policy.sourceRevision !== record.plan.role.packRevision || !policy.installationConsent)
|
|
1848
|
+
throw new Error("Dynamic child source policy or organization-bound installation is stale.");
|
|
1849
|
+
const context = {
|
|
1850
|
+
[stateFile]: createHash("sha256").update(readFileSync(stateFile)).digest("hex"),
|
|
1851
|
+
[policyFile]: createHash("sha256").update(readFileSync(policyFile)).digest("hex")
|
|
1852
|
+
};
|
|
1853
|
+
const inventories = {};
|
|
1854
|
+
const fixed = (descriptor) => {
|
|
1855
|
+
const rows = state.agents.filter((a) => a.key === descriptor.key && a.desktopSessionId === descriptor.nativeId && a.pack === pack && a.sourceRevision === policy.sourceRevision && a.importState === "desktop-adopted");
|
|
1856
|
+
const settings = policy.agents.filter((r) => r.key === descriptor.key);
|
|
1857
|
+
if (rows.length !== 1 || settings.length !== 1) throw new Error("Dynamic source has no unique current adopted role.");
|
|
1858
|
+
const row = rows[0];
|
|
1859
|
+
const cwd = realpathSync(row.workspace);
|
|
1860
|
+
if (cwd !== descriptor.cwd || !cwd.startsWith(resolve(root, "claude/workspaces", pack) + sep) || canonical(settings[0].harnesses.claude.nativeSettings) !== canonical(descriptor.settings))
|
|
1861
|
+
throw new Error("Dynamic source workspace or current fixed settings differ.");
|
|
1862
|
+
const files = [resolve(cwd, "CLAUDE.md"), resolve(cwd, "topolo-connection.json"), resolve(cwd, ".mcp.json")];
|
|
1863
|
+
const hashes = {};
|
|
1864
|
+
for (const f of files) hashes[owned(f)] = createHash("sha256").update(readFileSync(f)).digest("hex");
|
|
1865
|
+
const connection = JSON.parse(readFileSync(files[1], "utf8"));
|
|
1866
|
+
const expected = {
|
|
1867
|
+
environment: descriptor.environment,
|
|
1868
|
+
contextKey: descriptor.contextKey,
|
|
1869
|
+
pack,
|
|
1870
|
+
packRevision: descriptor.packRevision,
|
|
1871
|
+
nativeThreadId: descriptor.nativeId,
|
|
1872
|
+
agentId: descriptor.AgentLogicalId,
|
|
1873
|
+
agentVersionId: descriptor.AgentVersionId,
|
|
1874
|
+
agentWorkspaceId: descriptor.AgentWorkspaceId,
|
|
1875
|
+
bytesWorkspaceId: descriptor.BytesWorkspaceId,
|
|
1876
|
+
instanceId: descriptor.installationId
|
|
1877
|
+
};
|
|
1878
|
+
if (Object.entries(expected).some(([k, v]) => connection[k] !== v) || connection.identity?.organizationId !== descriptor.organizationId || descriptor.contextKey !== "org:" + descriptor.organizationId || descriptor.privateMCPDescriptorHash !== hashes[files[2]] || descriptor.contextDigest !== digest(hashes))
|
|
1879
|
+
throw new Error("Current hosted Agent, Bytes, private MCP or loaded-source context descriptor differs.");
|
|
1880
|
+
Object.assign(context, hashes);
|
|
1881
|
+
inventories[cwd] = snapshotClaudeMarkdown(cwd, filesystem, cryptography, paths);
|
|
1882
|
+
return { key: row.key, sessionId: row.desktopSessionId, cwd, title: row.title };
|
|
1883
|
+
};
|
|
1884
|
+
const source = fixed(record.plan.role);
|
|
1885
|
+
const intake = fixed(record.plan.intake);
|
|
1886
|
+
const consent = policy.installationConsent;
|
|
1887
|
+
if (consent.contextKey !== record.plan.role.contextKey || consent.environment !== record.plan.role.environment || consent.intakeRoutes?.[source.key] !== intake.key || record.creationReceipt.cwd !== source.cwd)
|
|
1888
|
+
throw new Error("Dynamic child original intake or installation scope differs.");
|
|
1889
|
+
return {
|
|
1890
|
+
root,
|
|
1891
|
+
pack,
|
|
1892
|
+
record,
|
|
1893
|
+
context,
|
|
1894
|
+
contextInventories: inventories,
|
|
1895
|
+
consent,
|
|
1896
|
+
nativePlan: {
|
|
1897
|
+
laneId,
|
|
1898
|
+
planDigest: record.planDigest,
|
|
1899
|
+
nativeId: record.nativeId,
|
|
1900
|
+
cwd: record.creationReceipt.cwd,
|
|
1901
|
+
title: record.nativeTitle,
|
|
1902
|
+
source,
|
|
1903
|
+
intake,
|
|
1904
|
+
lifecycleState: record.state,
|
|
1905
|
+
...record.plan.resolution.resolved,
|
|
1906
|
+
permissionMode: record.plan.verificationProfile === "manual" ? "default" : record.plan.role.settings.permissions.defaultMode,
|
|
1907
|
+
contextDigest: record.plan.role.contextDigest,
|
|
1908
|
+
deadline: Date.now() + 45e3
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function privateClaudeHelperEnvironment(connection, fs, crypto, path, hostEnv) {
|
|
1913
|
+
const fields = ["format", "schemaVersion", "root", "pack", "sourceRoleKey", "policyFile", "bindingFile", "policyDigest", "bindingDigest", "stateDigest", "profile"];
|
|
1914
|
+
if (!connection || typeof connection !== "object" || Array.isArray(connection) || Object.keys(connection).sort().join() !== fields.sort().join() || connection.format !== "topolo-private-helper-connection" || connection.schemaVersion !== 1 || !/^[a-z0-9][a-z0-9-]*$/.test(connection.pack) || typeof connection.sourceRoleKey !== "string" || connection.sourceRoleKey.split("/").length !== 3)
|
|
1915
|
+
throw new Error("Explicit approved private helper connection required before client startup.");
|
|
1916
|
+
const canonical = (value) => JSON.stringify(value, (_key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item);
|
|
1917
|
+
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1918
|
+
const owned = (file) => {
|
|
1919
|
+
const info = fs.lstatSync(file);
|
|
1920
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.nlink !== 1 || info.mode & 18)
|
|
1921
|
+
throw new Error("Private helper metadata requires owned regular records.");
|
|
1922
|
+
};
|
|
1923
|
+
const read = (file, expected) => {
|
|
1924
|
+
owned(file);
|
|
1925
|
+
const bytes = fs.readFileSync(file);
|
|
1926
|
+
if (hash(bytes) !== expected) throw new Error("Private helper connection metadata changed before client startup.");
|
|
1927
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
1928
|
+
};
|
|
1929
|
+
const root = fs.realpathSync(connection.root);
|
|
1930
|
+
if (root !== connection.root) throw new Error("Private helper library identity changed.");
|
|
1931
|
+
const policy = read(connection.policyFile, connection.policyDigest), consent = policy.installationConsent;
|
|
1932
|
+
const state = read(path.resolve(root, "claude/state.json"), connection.stateDigest);
|
|
1933
|
+
const binding = read(connection.bindingFile, connection.bindingDigest), profile = connection.profile;
|
|
1934
|
+
if (!profile || typeof profile !== "object" || Object.keys(profile).sort().join() !== ["format", "schemaVersion", "configPath", "environment", "contextKey", "identity"].sort().join() || profile.format !== "topolo-private-credential-profile" || profile.schemaVersion !== 1 || !["development", "staging", "production"].includes(profile.environment) || typeof profile.contextKey !== "string" || !profile.identity || Object.keys(profile.identity).sort().join() !== "organizationId,userId" || typeof profile.identity.userId !== "string" || !profile.identity.userId || typeof profile.identity.organizationId !== "string" || !profile.identity.organizationId || profile.contextKey !== "org:" + profile.identity.organizationId)
|
|
1935
|
+
throw new Error("Exact per-environment private profile identity reference required.");
|
|
1936
|
+
if (connection.bindingFile !== path.resolve(root, "connections", connection.pack, profile.environment, hash(profile.contextKey) + ".json") || canonical(binding.privateCredentialProfile) !== canonical(profile) || canonical(binding.identity) !== canonical(profile.identity) || binding.environment !== profile.environment || binding.contextKey !== profile.contextKey || policy.format !== "topolo-agent-execution-policy" || policy.pack !== connection.pack || policy.sourceRevision !== binding.packRevision || state.harness !== "claude" || !consent || consent.format !== "topolo-pack-installation-consent" || consent.schemaVersion !== 1 || consent.authority !== "local-installation-consent" || consent.approved !== true || consent.pack !== connection.pack || consent.environment !== profile.environment || consent.contextKey !== profile.contextKey || consent.principalId !== profile.identity.userId || !consent.agentWorkspaceId || binding.agentWorkspaceId !== consent.agentWorkspaceId || typeof binding.input?.instanceId !== "string" || !binding.input.instanceId || typeof binding.input.bytesWorkspaceId !== "string" || !binding.input.bytesWorkspaceId)
|
|
1937
|
+
throw new Error("Private helper profile differs from the actual pack installation and policy.");
|
|
1938
|
+
const intakeKey = consent.intakeRoutes?.[connection.sourceRoleKey];
|
|
1939
|
+
if (typeof intakeKey !== "string" || !intakeKey.includes("/intakes/")) throw new Error("Private helper original intake is missing.");
|
|
1940
|
+
for (const key of [connection.sourceRoleKey, intakeKey]) {
|
|
1941
|
+
const logical = key.split("/").slice(1).join("/");
|
|
1942
|
+
const rows = state.agents?.filter((row) => row.key === key && row.pack === connection.pack && row.sourceRevision === binding.packRevision && row.importState === "desktop-adopted");
|
|
1943
|
+
const roles = binding.input.roles?.filter((row) => row.logicalId === logical);
|
|
1944
|
+
if (key.split("/")[0] !== connection.pack || rows?.length !== 1 || roles?.length !== 1 || !policy.agents?.some((row) => row.key === key) || typeof roles[0].agentId !== "string" || !roles[0].agentId || typeof roles[0].agentVersionId !== "string" || !roles[0].agentVersionId)
|
|
1945
|
+
throw new Error("Private helper source or original intake is outside the current bound pack.");
|
|
1946
|
+
const cwd = fs.realpathSync(rows[0].workspace);
|
|
1947
|
+
if (!cwd.startsWith(path.resolve(root, "claude/workspaces", connection.pack) + path.sep)) throw new Error("Private helper role workspace is outside its pack.");
|
|
1948
|
+
const descriptorFile = path.resolve(cwd, "topolo-connection.json");
|
|
1949
|
+
owned(descriptorFile);
|
|
1950
|
+
const descriptor = JSON.parse(fs.readFileSync(descriptorFile, "utf8"));
|
|
1951
|
+
const expected = {
|
|
1952
|
+
harness: "claude",
|
|
1953
|
+
pack: connection.pack,
|
|
1954
|
+
packRevision: binding.packRevision,
|
|
1955
|
+
role: logical,
|
|
1956
|
+
nativeThreadId: rows[0].desktopSessionId,
|
|
1957
|
+
environment: profile.environment,
|
|
1958
|
+
contextKey: profile.contextKey,
|
|
1959
|
+
instanceId: binding.input.instanceId,
|
|
1960
|
+
agentId: roles[0].agentId,
|
|
1961
|
+
agentVersionId: roles[0].agentVersionId,
|
|
1962
|
+
agentWorkspaceId: binding.agentWorkspaceId,
|
|
1963
|
+
bytesWorkspaceId: binding.input.bytesWorkspaceId
|
|
1964
|
+
};
|
|
1965
|
+
if (Object.entries(expected).some(([name, value]) => descriptor[name] !== value) || canonical(descriptor.identity) !== canonical(profile.identity))
|
|
1966
|
+
throw new Error("Private helper role descriptor does not match the actual public connection binding.");
|
|
1967
|
+
}
|
|
1968
|
+
if (typeof profile.configPath !== "string" || !path.isAbsolute(profile.configPath) || fs.realpathSync(profile.configPath) !== profile.configPath)
|
|
1969
|
+
throw new Error("Explicit protected profile path must be absolute and must not follow links.");
|
|
1970
|
+
const leaf = fs.lstatSync(profile.configPath), parent = fs.lstatSync(path.dirname(profile.configPath));
|
|
1971
|
+
if (!leaf.isFile() || leaf.isSymbolicLink() || leaf.uid !== process.getuid?.() || leaf.nlink !== 1 || (leaf.mode & 511) !== 384 || !parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || (parent.mode & 511) !== 448)
|
|
1972
|
+
throw new Error("Private helper profile requires owned0600 single-link storage in an owned0700 directory.");
|
|
1973
|
+
const env = {};
|
|
1974
|
+
for (const key of ["HOME", "USER", "LOGNAME", "PATH", "LANG", "LC_ALL", "TMPDIR"]) if (typeof hostEnv[key] === "string") env[key] = hostEnv[key];
|
|
1975
|
+
return { ...env, TOPOLO_CONFIG_PATH: profile.configPath, TOPOLO_ENV: profile.environment, TOPOLO_CONTEXT: profile.contextKey };
|
|
1976
|
+
}
|
|
1977
|
+
function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRoleKey) {
|
|
1978
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(pack) || typeof sourceRoleKey !== "string" || sourceRoleKey.split("/").length !== 3)
|
|
1979
|
+
throw new Error("Exact existing pack source role required for a private helper connection.");
|
|
1980
|
+
const root = realpathSync(store), policyFile = realpathSync(policyPath);
|
|
1981
|
+
const policyBytes = readFileSync(policyFile), policy = JSON.parse(policyBytes.toString("utf8")), consent = policy.installationConsent;
|
|
1982
|
+
if (!consent || !["development", "staging", "production"].includes(consent.environment) || typeof consent.contextKey !== "string")
|
|
1983
|
+
throw new Error("Existing approved per-environment private connection required.");
|
|
1984
|
+
const bindingFile = resolve(root, "connections", pack, consent.environment, createHash("sha256").update(consent.contextKey).digest("hex") + ".json");
|
|
1985
|
+
const bindingBytes = readFileSync(bindingFile), binding = JSON.parse(bindingBytes.toString("utf8"));
|
|
1986
|
+
const connection = {
|
|
1987
|
+
format: "topolo-private-helper-connection",
|
|
1988
|
+
schemaVersion: 1,
|
|
1989
|
+
root,
|
|
1990
|
+
pack,
|
|
1991
|
+
sourceRoleKey,
|
|
1992
|
+
policyFile,
|
|
1993
|
+
bindingFile,
|
|
1994
|
+
policyDigest: createHash("sha256").update(policyBytes).digest("hex"),
|
|
1995
|
+
bindingDigest: createHash("sha256").update(bindingBytes).digest("hex"),
|
|
1996
|
+
stateDigest: createHash("sha256").update(readFileSync(resolve(root, "claude/state.json"))).digest("hex"),
|
|
1997
|
+
profile: binding.privateCredentialProfile
|
|
1998
|
+
};
|
|
1999
|
+
privateClaudeHelperEnvironment(connection, filesystem, cryptography, paths, process.env);
|
|
2000
|
+
return connection;
|
|
2001
|
+
}
|
|
2002
|
+
function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, connection, launch = spawn, runtime = { fs: filesystem, crypto: cryptography, path: paths, env: process.env }, qualifyEnvironment = privateClaudeHelperEnvironment) {
|
|
2003
|
+
const actions = [
|
|
2004
|
+
"peek",
|
|
2005
|
+
"plan",
|
|
2006
|
+
"creation-begin",
|
|
2007
|
+
"creation-record",
|
|
2008
|
+
"creation-read",
|
|
2009
|
+
"adopt",
|
|
2010
|
+
"configure-begin",
|
|
2011
|
+
"configure-ack",
|
|
2012
|
+
"context-prepare",
|
|
2013
|
+
"context-mark",
|
|
2014
|
+
"context-ack",
|
|
2015
|
+
"fixture-prepare",
|
|
2016
|
+
"fixture-mark",
|
|
2017
|
+
"fixture-outcome",
|
|
2018
|
+
"permission-request",
|
|
2019
|
+
"permission-decision",
|
|
2020
|
+
"retire-begin",
|
|
2021
|
+
"retire-ack",
|
|
2022
|
+
"read"
|
|
2023
|
+
];
|
|
2024
|
+
if (!actions.includes(action) || !/^[a-z0-9][a-z0-9-]*$/.test(pack) || !payload || typeof payload !== "object" || Array.isArray(payload))
|
|
2025
|
+
return Promise.reject(new Error("Closed typed native lifecycle helper required."));
|
|
2026
|
+
const encoded = JSON.stringify(payload);
|
|
2027
|
+
if (Buffer.byteLength(encoded) > 2097152) return Promise.reject(new Error("Own-child native transcript exceeds bounded transport; no broad history read."));
|
|
2028
|
+
const selectedRole = payload.sourceRoleKey ?? payload.request?.sourceRoleKey ?? payload.plan?.role?.key ?? payload.operation?.record?.plan?.role?.key;
|
|
2029
|
+
if (!connection || root !== connection.root || pack !== connection.pack || policyPath !== connection.policyFile || selectedRole !== void 0 && selectedRole !== connection.sourceRoleKey)
|
|
2030
|
+
return Promise.reject(new Error("Private helper arguments differ from the exact bound source connection."));
|
|
2031
|
+
let childEnv;
|
|
2032
|
+
try {
|
|
2033
|
+
childEnv = qualifyEnvironment(connection, runtime.fs, runtime.crypto, runtime.path, runtime.env);
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
return Promise.reject(error);
|
|
2036
|
+
}
|
|
2037
|
+
const script = "import sys,json\nsys.path.insert(0,sys.argv[5])\nfrom topolo_agent_pack.storage import Library\nfrom topolo_agent_pack.lanes import ClaudeLifecycle\ns=ClaudeLifecycle(Library(sys.argv[1]),sys.argv[2],sys.argv[3])\np=json.loads(sys.stdin.read(2097153))\nprint(json.dumps(s.dispatch(sys.argv[4],p)))";
|
|
2038
|
+
return new Promise((done, fail) => {
|
|
2039
|
+
const child = launch("python3", ["-I", "-B", "-c", script, root, pack, policyPath, action, source], { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
|
|
2040
|
+
let output = "";
|
|
2041
|
+
let settled = false;
|
|
2042
|
+
const finish = (error, value) => {
|
|
2043
|
+
if (settled) return;
|
|
2044
|
+
settled = true;
|
|
2045
|
+
clearTimeout(timer);
|
|
2046
|
+
error ? fail(error) : done(value);
|
|
2047
|
+
};
|
|
2048
|
+
const timer = setTimeout(() => {
|
|
2049
|
+
child.kill("SIGTERM");
|
|
2050
|
+
finish(new Error("Owned lifecycle helper timed out; preserve possible-effect custody."));
|
|
2051
|
+
}, 2e5);
|
|
2052
|
+
child.stdout.setEncoding("utf8");
|
|
2053
|
+
child.stderr.resume();
|
|
2054
|
+
child.stdout.on("data", (data) => {
|
|
2055
|
+
output += data;
|
|
2056
|
+
if (Buffer.byteLength(output) > 1048576) {
|
|
2057
|
+
child.kill("SIGTERM");
|
|
2058
|
+
finish(new Error("Bounded native lifecycle reply exceeded."));
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
child.on("error", () => finish(new Error("Bundled lifecycle helper unavailable.")));
|
|
2062
|
+
child.on("close", (code) => {
|
|
2063
|
+
if (settled) return;
|
|
2064
|
+
if (code !== 0) return finish(new Error("Current context/policy/native lifecycle helper refused; no fallback or replay."));
|
|
2065
|
+
try {
|
|
2066
|
+
finish(void 0, JSON.parse(output));
|
|
2067
|
+
} catch {
|
|
2068
|
+
finish(new Error("Typed lifecycle helper returned invalid JSON."));
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
child.stdin.on("error", () => finish(new Error("Own lifecycle helper input closed; reconcile original effect.")));
|
|
2072
|
+
child.stdin.end(encoded);
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
function requireClaudePrivateQueryEffectQualified(operation) {
|
|
2076
|
+
if (["create", "configure", "context-start", "fixture-start", "retire", "approval-once"].includes(operation))
|
|
2077
|
+
throw Object.assign(
|
|
2078
|
+
new Error("native_private_query_prestart_contract_unqualified: private native query startup, exclusion and owned lifecycle are not qualified."),
|
|
2079
|
+
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
function claudeLaneExpression(options, source, journal, prepared) {
|
|
2083
|
+
if (!Number.isSafeInteger(options.pid) || options.pid < 1 || !["plan", "create", "create-read", "adopt", "read", "configure", "context-start", "context-read", "fixture-start", "fixture-read", "retire"].includes(options.operation))
|
|
2084
|
+
throw new Error("Closed typed Claude lane operation required.");
|
|
2085
|
+
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2086
|
+
if (!options.privateConnection) throw new Error("Explicit approved private helper connection required before native access.");
|
|
2087
|
+
return `(async()=>{
|
|
2088
|
+
const expected=${JSON.stringify({ ...options, qualifier: CLAUDE_NATIVE_QUALIFIER, source, journal })};
|
|
2089
|
+
const requireClaudePrivateQueryEffectQualified=(${requireClaudePrivateQueryEffectQualified.toString()});
|
|
2090
|
+
requireClaudePrivateQueryEffectQualified(expected.operation);
|
|
2091
|
+
if(process.pid!==expected.pid || typeof process.getBuiltinModule!=='function')throw new Error('Native process identity changed.');
|
|
2092
|
+
const load=process.getBuiltinModule('module').createRequire(process.resourcesPath+'/app.asar/package.json');
|
|
2093
|
+
const fs=load('node:fs'),crypto=load('node:crypto'),path=load('node:path');
|
|
2094
|
+
const base=process.resourcesPath+'/app.asar/.vite/build/';
|
|
2095
|
+
const qualify=()=>{
|
|
2096
|
+
if(process.pid!==expected.pid || load('electron').app.getVersion()!==expected.qualifier.version)throw new Error('Native version/PID changed.');
|
|
2097
|
+
for(const [file,hash] of Object.entries(expected.qualifier.hashes))
|
|
2098
|
+
if(crypto.createHash('sha256').update(fs.readFileSync(base+file)).digest('hex')!==hash)throw new Error('Native module changed.');
|
|
2099
|
+
};
|
|
2100
|
+
qualify();const manager=load(base+expected.qualifier.manager).claudeCodeSessionManager;
|
|
2101
|
+
const canonical=value=>JSON.stringify(value,(_key,item)=>item&&typeof item==='object'&&!Array.isArray(item)?Object.fromEntries(Object.keys(item).sort().map(k=>[k,item[k]])):item);
|
|
2102
|
+
const fingerprint=value=>crypto.createHash('sha256').update(canonical(value)).digest('hex');
|
|
2103
|
+
const generation=fingerprint({pid:expected.pid,qualifier:expected.qualifier,root:expected.root,pack:expected.pack});
|
|
2104
|
+
const models=()=>{
|
|
2105
|
+
qualify();const rows=manager.getAvailableCodeModels();
|
|
2106
|
+
if(!Array.isArray(rows)||!rows.length||rows.some(m=>typeof m!=='string')||new Set(rows).size!==rows.length)throw new Error('Current native account model IDs unavailable.');
|
|
2107
|
+
return {harness:'claude',source:'native-account-model-ids',connectionGeneration:generation,models:[...rows].sort()};
|
|
2108
|
+
};
|
|
2109
|
+
const privateClaudeHelperEnvironment=(${privateClaudeHelperEnvironment.toString()});
|
|
2110
|
+
const helper=(action,payload)=>(${runClaudeLaneHelper.toString()})(expected.root,expected.pack,expected.policy,action,
|
|
2111
|
+
{...payload,...(action==='context-mark'?{historyCount:payload.historyCount??null,historyDigest:payload.historyDigest??null}:{}),models:models()},
|
|
2112
|
+
expected.source,expected.privateConnection,load('node:child_process').spawn,{fs,crypto,path,env:process.env},privateClaudeHelperEnvironment);
|
|
2113
|
+
const checkpoint=value=>{const fd=fs.openSync(expected.journal,'a');try{fs.writeSync(fd,JSON.stringify(value)+'\\n');fs.fsyncSync(fd);}finally{fs.closeSync(fd);}};
|
|
2114
|
+
const fixed=async role=>{
|
|
2115
|
+
const raw=await manager.getSessionWhenLoaded(role.nativeId,'topolo-dynamic-source',{warnOnMiss:false});
|
|
2116
|
+
const view=await manager.getSession(role.nativeId);qualify();
|
|
2117
|
+
if(!raw||!view||raw.sessionId!==role.nativeId||view.sessionId!==role.nativeId||raw.cwd!==role.cwd||view.cwd!==role.cwd||
|
|
2118
|
+
raw.isArchived||view.isArchived||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.spawnedFrom||raw.worktreePath||raw.worktreeLazy)
|
|
2119
|
+
throw new Error('Exact source/intake actual native descriptor unavailable.');
|
|
2120
|
+
return {raw,view};
|
|
2121
|
+
};
|
|
2122
|
+
if(expected.operation==='plan'){
|
|
2123
|
+
if(Object.keys(expected.input).join()!=='request')throw new Error('Closed intake lane request required.');
|
|
2124
|
+
const current=await helper('peek',{sourceRoleKey:expected.input.request.sourceRoleKey});
|
|
2125
|
+
await fixed(current.role);await fixed(current.intake);
|
|
2126
|
+
const rows=await manager.getAllSessions();qualify();
|
|
2127
|
+
if(!Array.isArray(rows))throw new Error('Actual native ID-only creation inventory unavailable.');
|
|
2128
|
+
return helper('plan',{request:expected.input.request,nativeIds:rows.map(r=>r.sessionId)});
|
|
2129
|
+
}
|
|
2130
|
+
if(expected.operation==='create-read'){
|
|
2131
|
+
if(Object.keys(expected.input).join()!=='laneId')throw new Error('Exact immutable owned creation lane required.');
|
|
2132
|
+
const laneId=expected.input.laneId;
|
|
2133
|
+
const pending=await helper('creation-read',{laneId});
|
|
2134
|
+
const p=pending.plan;
|
|
2135
|
+
await fixed(p.role);await fixed(p.intake);
|
|
2136
|
+
const current=async()=>{const proof=await helper('creation-read',{laneId});
|
|
2137
|
+
if(canonical(proof.plan)!==canonical(p))throw new Error('Current immutable creation plan differs.');};
|
|
2138
|
+
if(!pending.creation.nativeId)throw new Error('Own native creation identity is unresolved; no replay.');
|
|
2139
|
+
await current();
|
|
2140
|
+
const native=await (${nativeClaudeReadEmptyCreation.toString()})(manager,{...p,creationNativeId:pending.creation.nativeId},qualify);
|
|
2141
|
+
if(native.state!=='ready')return {...native,laneId};
|
|
2142
|
+
const receipt={format:'topolo-native-lane-created',schemaVersion:1,state:'ready',planDigest:fingerprint(p),
|
|
2143
|
+
harness:'claude',nativeId:native.nativeId,logicalOriginThreadId:p.logicalOriginThreadId,cwd:native.cwd,
|
|
2144
|
+
model:native.model,effort:native.effort,nativeParentId:native.nativeParentId,fresh:true,creationSource:'supported-native-host-tool'};
|
|
2145
|
+
const record=await helper('adopt',{plan:p,receipt,native});
|
|
2146
|
+
return {state:'created-adopted-unconfigured',receipt,record,modelTurnsStarted:false,requestedTaskStarted:false};
|
|
2147
|
+
}
|
|
2148
|
+
if(expected.operation==='adopt'){
|
|
2149
|
+
if(Object.keys(expected.input).sort().join()!=='plan,receipt')throw new Error('Exact own host plan and creation receipt required.');
|
|
2150
|
+
const p=expected.input.plan,r=expected.input.receipt;await fixed(p.role);await fixed(p.intake);
|
|
2151
|
+
const raw=await manager.getSessionWhenLoaded(r.nativeId,'topolo-dynamic-adopt',{warnOnMiss:false});
|
|
2152
|
+
const view=await manager.getSession(r.nativeId);qualify();
|
|
2153
|
+
if(!raw||!view||raw.sessionId!==r.nativeId||view.sessionId!==r.nativeId||raw.cwd!==r.cwd||view.cwd!==r.cwd||raw.title!==view.title||
|
|
2154
|
+
raw.isArchived||view.isArchived||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.worktreePath||raw.worktreeLazy||
|
|
2155
|
+
raw.spawnedFrom?.sessionId!==p.intake.nativeId||raw.spawnedFrom?.sideSession!==true)throw new Error('Actual child native raw parent or placement differs.');
|
|
2156
|
+
return helper('adopt',{plan:p,receipt:r,native:{nativeId:r.nativeId,cwd:view.cwd,title:view.title,model:view.model,effort:view.effort,
|
|
2157
|
+
nativeParentId:raw.spawnedFrom.sessionId,archived:false,backend:'local',foreignWorkspace:false,
|
|
2158
|
+
busy:!!(view.isRunning||view.starting||raw.isRunning||raw.starting||manager.hasLosableWork(r.nativeId)),pendingApprovals:view.pendingToolPermissions?.length??0}});
|
|
2159
|
+
}
|
|
2160
|
+
const prepared=${JSON.stringify(prepared ?? null)};
|
|
2161
|
+
if(!prepared)throw new Error('Exact registered own child preparation required.');
|
|
2162
|
+
const nativePlan={...prepared.nativePlan,deadline:Date.now()+200000};
|
|
2163
|
+
const snapshotMarkdown=(${snapshotClaudeMarkdown.toString()});
|
|
2164
|
+
const verifyContext=()=>{
|
|
2165
|
+
qualify();
|
|
2166
|
+
for(const [file,hash] of Object.entries(prepared.context))
|
|
2167
|
+
if(crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')!==hash)throw new Error('Current source/registration/policy context changed.');
|
|
2168
|
+
for(const [cwd,inventory] of Object.entries(prepared.contextInventories))
|
|
2169
|
+
if(canonical(snapshotMarkdown(cwd,fs,crypto,path))!==canonical(inventory))throw new Error('Current source charter inventory changed.');
|
|
2170
|
+
};
|
|
2171
|
+
const qualifyOrganization=async()=>{await helper('read',{laneId:nativePlan.laneId});verifyContext();};
|
|
2172
|
+
let historyBefore;
|
|
2173
|
+
const ownHistory=async()=>{
|
|
2174
|
+
const raw=await manager.getSessionWhenLoaded(nativePlan.nativeId,'topolo-dynamic-history',{warnOnMiss:false});
|
|
2175
|
+
if(!raw||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.spawnedFrom?.sessionId!==nativePlan.intake.sessionId)throw new Error('Own child transcript eligibility changed.');
|
|
2176
|
+
const messages=await manager.getTranscriptForCallerElsewhere(nativePlan.nativeId);qualify();
|
|
2177
|
+
if(!Array.isArray(messages))throw new Error('Exact own child transcript resolver unavailable.');return messages;
|
|
2178
|
+
};
|
|
2179
|
+
const history=(id,before)=>{if(id!==nativePlan.nativeId)throw new Error('Unrelated transcript forbidden.');
|
|
2180
|
+
return before===undefined?historyBefore:fingerprint(historyBefore)===before;};
|
|
2181
|
+
if(expected.operation==='configure'){
|
|
2182
|
+
const operation=await helper('configure-begin',{laneId:nativePlan.laneId,generation});
|
|
2183
|
+
// Helper's owned marker changes the registration; verify immutable source inputs and that precise beforeimage.
|
|
2184
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2185
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'configure',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2186
|
+
const configured=await helper('configure-ack',{operation,native:result});
|
|
2187
|
+
const intakeRoute={key:prepared.record.plan.intake.key,sessionId:nativePlan.intake.sessionId,cwd:nativePlan.intake.cwd,title:nativePlan.intake.title,
|
|
2188
|
+
contextKey:prepared.record.plan.intake.contextKey,intakeKey:prepared.record.plan.intake.key,intakeSessionId:nativePlan.intake.sessionId};
|
|
2189
|
+
if(!manager[Symbol.for('topolo.agent-pack.approvals.v1')])
|
|
2190
|
+
await (${nativeClaudeApprovalOperation.toString()})(manager,[intakeRoute],{kind:'install'},fingerprint,checkpoint);
|
|
2191
|
+
const route={key:prepared.record.plan.role.key+'/dynamic/'+nativePlan.laneId,sessionId:nativePlan.nativeId,cwd:nativePlan.cwd,title:nativePlan.title,
|
|
2192
|
+
contextKey:prepared.record.plan.role.contextKey,intakeKey:prepared.record.plan.intake.key,intakeSessionId:nativePlan.intake.sessionId,
|
|
2193
|
+
dynamic:{laneId:nativePlan.laneId,planDigest:nativePlan.planDigest,nativeParentId:nativePlan.intake.sessionId,generation,connectionId:nativePlan.laneId}};
|
|
2194
|
+
await (${nativeClaudeAppendDynamicRoute.toString()})(manager,route,qualifyOrganization,checkpoint,{
|
|
2195
|
+
request:async(record,pending)=>{if(typeof pending.toolUseId!=='string')throw new Error('Actual native permission toolUseId is missing.');
|
|
2196
|
+
await helper('permission-request',{laneId:nativePlan.laneId,request:{nativeId:nativePlan.nativeId,fingerprint:record.fingerprint,requestId:pending.requestId,
|
|
2197
|
+
generation,intakeKey:record.intakeKey,intakeNativeId:record.intakeSessionId,toolUseId:pending.toolUseId}});},
|
|
2198
|
+
decision:async(record,pending,decision)=>helper('permission-decision',{laneId:nativePlan.laneId,decision:decision==='once'?'approved':'denied',
|
|
2199
|
+
intakeKey:record.intakeKey,fingerprint:record.fingerprint,generation})});
|
|
2200
|
+
const control=expected.control;
|
|
2201
|
+
if(!control)throw new Error('Exact owned per-lane native control setup required.');
|
|
2202
|
+
const secretInfo=fs.lstatSync(control.secretPath);
|
|
2203
|
+
if(!secretInfo.isFile()||secretInfo.isSymbolicLink()||secretInfo.uid!==process.getuid()||secretInfo.mode&63)throw new Error('Unowned private native control secret.');
|
|
2204
|
+
const secret=JSON.parse(fs.readFileSync(control.secretPath,'utf8'));
|
|
2205
|
+
const ownedOperation=async op=>{
|
|
2206
|
+
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind);
|
|
2207
|
+
qualify();const current=await helper('read',{laneId:nativePlan.laneId});
|
|
2208
|
+
const scopedPlan={...nativePlan,lifecycleState:current.state,deadline:Date.now()+200000};
|
|
2209
|
+
if(op.kind.startsWith('approval-')){
|
|
2210
|
+
const state=manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
2211
|
+
if(!state)throw new Error('Original native approval listener unavailable.');
|
|
2212
|
+
if(op.kind!=='approval-list'){
|
|
2213
|
+
const entry=state.entries.get(op.key);
|
|
2214
|
+
if(!entry||entry.record.sourceSessionId!==nativePlan.nativeId||entry.record.intakeSessionId!==nativePlan.intake.sessionId)
|
|
2215
|
+
throw new Error('Native approval is outside the exact owned child/original intake.');
|
|
2216
|
+
}
|
|
2217
|
+
const result=await (${nativeClaudeApprovalOperation.toString()})(manager,state.routes,
|
|
2218
|
+
{...op,kind:op.kind.slice('approval-'.length)},fingerprint,checkpoint);
|
|
2219
|
+
return op.kind==='approval-list'?{requests:result.requests.filter(r=>r.sourceSessionId===nativePlan.nativeId)}:result;
|
|
2220
|
+
}
|
|
2221
|
+
if(op.kind==='context-start'||op.kind==='context-read')return (${nativeClaudeDynamicContext.toString()})(manager,scopedPlan,
|
|
2222
|
+
{kind:op.kind,operationId:op.operationId},helper,fingerprint,verifyContext,qualifyOrganization);
|
|
2223
|
+
if(op.kind==='fixture-start'||op.kind==='fixture-read')return (${nativeClaudeDynamicFixture.toString()})(manager,scopedPlan,
|
|
2224
|
+
{kind:op.kind,operationId:op.operationId},helper,verifyContext,qualifyOrganization);
|
|
2225
|
+
if(op.kind==='read')return (${nativeClaudeDynamicLaneOperation.toString()})(manager,scopedPlan,'read',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2226
|
+
if(op.kind==='retire'){
|
|
2227
|
+
const messages=await ownHistory();const before=fingerprint(messages);const retirement=await helper('retire-begin',{laneId:scopedPlan.laneId});
|
|
2228
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,scopedPlan,'retire',checkpoint,verifyContext,qualifyOrganization,
|
|
2229
|
+
async(id,saved)=>{if(id!==scopedPlan.nativeId)throw new Error('Unrelated transcript forbidden.');return saved===undefined?before:fingerprint(await ownHistory())===saved;});
|
|
2230
|
+
const retired=await helper('retire-ack',{operation:retirement,native:result});
|
|
2231
|
+
const state=manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
2232
|
+
if([...state.entries.values()].some(e=>e.record.sourceSessionId===scopedPlan.nativeId&&['pending','response-may-have-effects'].includes(e.record.state)))
|
|
2233
|
+
throw new Error('Own retired route still has an unresolved native request.');
|
|
2234
|
+
state.byId.delete(scopedPlan.nativeId);state.byKey.delete(route.key);state.dynamicRoutes?.delete(scopedPlan.nativeId);state.dynamicLifecycle?.delete(scopedPlan.nativeId);
|
|
2235
|
+
return retired;
|
|
2236
|
+
}
|
|
2237
|
+
if(op.kind==='close'){
|
|
2238
|
+
if(current.state!=='retired')throw new Error('Only a normally retired own child control may close.');
|
|
2239
|
+
return {state:'normal-own-lane-control-close-approved',laneId:scopedPlan.laneId};
|
|
2240
|
+
}
|
|
2241
|
+
throw new Error('Unsupported exact owned native lane operation.');
|
|
2242
|
+
};
|
|
2243
|
+
const connection=await (${nativeClaudeLaneControl.toString()})(load('node:net'),fs,manager,
|
|
2244
|
+
{socket:control.socket,pid:expected.pid,identity:control.identity,laneId:nativePlan.laneId},secret.token,
|
|
2245
|
+
ownedOperation,qualify,checkpoint);
|
|
2246
|
+
return {record:configured,connection};
|
|
2247
|
+
|
|
2248
|
+
}
|
|
2249
|
+
if(expected.operation==='read')return (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'read',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2250
|
+
if(expected.operation==='context-start'||expected.operation==='context-read'){
|
|
2251
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2252
|
+
return (${nativeClaudeDynamicContext.toString()})(manager,nativePlan,{kind:expected.operation,operationId:expected.input.operationId},
|
|
2253
|
+
helper,fingerprint,verifyContext,qualifyOrganization);
|
|
2254
|
+
}
|
|
2255
|
+
if(expected.operation==='fixture-start'||expected.operation==='fixture-read'){
|
|
2256
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2257
|
+
return (${nativeClaudeDynamicFixture.toString()})(manager,nativePlan,{kind:expected.operation,operationId:expected.input.operationId},
|
|
2258
|
+
helper,verifyContext,qualifyOrganization);
|
|
2259
|
+
}
|
|
2260
|
+
if(expected.operation==='retire'){
|
|
2261
|
+
historyBefore=await ownHistory();const before=fingerprint(historyBefore);
|
|
2262
|
+
const operation=await helper('retire-begin',{laneId:nativePlan.laneId});
|
|
2263
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2264
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'retire',checkpoint,verifyContext,qualifyOrganization,
|
|
2265
|
+
async(id,saved)=>{if(id!==nativePlan.nativeId)throw new Error('Unrelated archive transcript forbidden.');if(saved===undefined)return before;return fingerprint(await ownHistory())===saved;});
|
|
2266
|
+
historyBefore=await ownHistory();if(fingerprint(historyBefore)!==before)throw new Error('Own child transcript preservation differs.');
|
|
2267
|
+
return helper('retire-ack',{operation,native:{...result,historyPreserved:true}});
|
|
2268
|
+
}
|
|
2269
|
+
throw new Error('Closed native fixture operation not yet qualified on this connection.');
|
|
2270
|
+
})()`;
|
|
2271
|
+
}
|
|
2272
|
+
async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
2273
|
+
requireClaudePrivateQueryEffectQualified(options.operation);
|
|
2274
|
+
validateInspectorEndpoint(options.endpoint);
|
|
2275
|
+
const root = realpathSync(options.store);
|
|
2276
|
+
const policy = realpathSync(options.policy);
|
|
2277
|
+
const release = await lockClaudeLibrary(root);
|
|
2278
|
+
try {
|
|
2279
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2280
|
+
const source = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(existsSync);
|
|
2281
|
+
if (!source) throw new Error("Installed typed lifecycle helper source unavailable.");
|
|
2282
|
+
const directory = resolve(root, "claude/dynamic-journals");
|
|
2283
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2284
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error("Unowned native lifecycle journal directory.");
|
|
2285
|
+
const journal = resolve(directory, randomUUID() + ".jsonl");
|
|
2286
|
+
durableRecord(journal, { operation: options.operation, pid: options.pid, at: Date.now() }, "wx");
|
|
2287
|
+
const prepared = ["plan", "create", "create-read", "adopt"].includes(options.operation) ? void 0 : prepareClaudeDynamicLane(root, options.pack, policy, options.input.laneId);
|
|
2288
|
+
let sourceRoleKey = prepared?.record.plan.role.key ?? (options.operation === "plan" ? options.input.request?.sourceRoleKey : options.input.plan?.role?.key);
|
|
2289
|
+
if (["create", "create-read"].includes(options.operation)) {
|
|
2290
|
+
if (typeof options.input.laneId !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(options.input.laneId)) throw new Error("Exact existing creation lane required.");
|
|
2291
|
+
const file = resolve(root, "dynamic-lanes/claude/plans", options.input.laneId + ".json");
|
|
2292
|
+
const info = lstatSync(file);
|
|
2293
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.mode & 18)
|
|
2294
|
+
throw new Error("Unowned immutable creation plan.");
|
|
2295
|
+
sourceRoleKey = JSON.parse(readFileSync(file, "utf8")).role?.key;
|
|
2296
|
+
}
|
|
2297
|
+
const privateConnection = prepareClaudePrivateHelperConnection(root, options.pack, policy, sourceRoleKey);
|
|
2298
|
+
let control;
|
|
2299
|
+
let connectionFile;
|
|
2300
|
+
if (options.operation === "configure") {
|
|
2301
|
+
if (!prepared) throw new Error("Exact fresh child preparation required.");
|
|
2302
|
+
const laneId = prepared.nativePlan.laneId;
|
|
2303
|
+
const privateRoot = resolve(root, "claude/dynamic-controls");
|
|
2304
|
+
mkdirSync(privateRoot, { recursive: true, mode: 448 });
|
|
2305
|
+
if (lstatSync(privateRoot).isSymbolicLink()) throw new Error("Unowned dynamic control directory.");
|
|
2306
|
+
const directory2 = resolve(privateRoot, laneId);
|
|
2307
|
+
mkdirSync(directory2, { mode: 448 });
|
|
2308
|
+
const secretPath = resolve(directory2, "secret.json");
|
|
2309
|
+
durableRecord(secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
2310
|
+
const identity = createHash("sha256").update(JSON.stringify({ pid: options.pid, laneId, planDigest: prepared.record.planDigest, qualifier: CLAUDE_NATIVE_QUALIFIER })).digest("hex");
|
|
2311
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" + createHash("sha256").update(root + "\0" + laneId).digest("hex").slice(0, 24));
|
|
2312
|
+
mkdirSync(socketDirectory, { mode: 448 });
|
|
2313
|
+
const socket = resolve(socketDirectory, "control.sock");
|
|
2314
|
+
control = { socket, secretPath, identity };
|
|
2315
|
+
connectionFile = resolve(directory2, "connection.json");
|
|
2316
|
+
durableRecord(connectionFile, { state: "prepared", pid: options.pid, laneId, pack: options.pack, policy, root, journal, ...control }, "wx");
|
|
2317
|
+
}
|
|
2318
|
+
const result = await evaluate(options.endpoint, claudeLaneExpression({
|
|
2319
|
+
root,
|
|
2320
|
+
pack: options.pack,
|
|
2321
|
+
policy,
|
|
2322
|
+
pid: options.pid,
|
|
2323
|
+
operation: options.operation,
|
|
2324
|
+
input: options.input,
|
|
2325
|
+
privateConnection,
|
|
2326
|
+
...control ? { control } : {}
|
|
2327
|
+
}, realpathSync(source), journal, prepared));
|
|
2328
|
+
if (connectionFile) {
|
|
2329
|
+
const value = JSON.parse(readFileSync(connectionFile, "utf8"));
|
|
2330
|
+
writeFileSync(connectionFile, JSON.stringify({ ...value, state: "ready" }) + "\n", { mode: 384 });
|
|
2331
|
+
}
|
|
2332
|
+
return result;
|
|
2333
|
+
} finally {
|
|
2334
|
+
await release();
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
function operateClaudeLaneControl(options, request = requestClaudeApprovalControl) {
|
|
2338
|
+
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind);
|
|
2339
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.connection)) throw new Error("Exact owned native lane connection ID required.");
|
|
2340
|
+
const root = realpathSync(options.store);
|
|
2341
|
+
const directory = resolve(root, "claude/dynamic-controls", options.connection);
|
|
2342
|
+
const owned = (file2) => {
|
|
2343
|
+
const info = lstatSync(file2);
|
|
2344
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 63)
|
|
2345
|
+
throw new Error("Unowned private native lane connection record.");
|
|
2346
|
+
};
|
|
2347
|
+
const file = resolve(directory, "connection.json");
|
|
2348
|
+
owned(file);
|
|
2349
|
+
const connection = JSON.parse(readFileSync(file, "utf8"));
|
|
2350
|
+
if (connection.state !== "ready" || connection.laneId !== options.connection || connection.root !== root || !Number.isSafeInteger(connection.pid))
|
|
2351
|
+
throw new Error("Exact current ready native lane connection required.");
|
|
2352
|
+
owned(connection.secretPath);
|
|
2353
|
+
if (realpathSync(connection.secretPath) !== resolve(directory, "secret.json")) throw new Error("Private native secret path differs.");
|
|
2354
|
+
const secret = JSON.parse(readFileSync(connection.secretPath, "utf8"));
|
|
2355
|
+
return request(connection, options.operation, secret.token);
|
|
2356
|
+
}
|
|
2357
|
+
export {
|
|
2358
|
+
CLAUDE_NATIVE_QUALIFIER,
|
|
2359
|
+
claudeLaneExpression,
|
|
2360
|
+
configureClaudePack,
|
|
2361
|
+
evaluateClaudeMain,
|
|
2362
|
+
lockClaudeLibrary,
|
|
2363
|
+
nativePackExpression,
|
|
2364
|
+
operateClaudeApprovals,
|
|
2365
|
+
operateClaudeLane,
|
|
2366
|
+
operateClaudeLaneControl,
|
|
2367
|
+
operateClaudeSmoke,
|
|
2368
|
+
prepareClaudeDynamicLane,
|
|
2369
|
+
prepareClaudeNativePlan,
|
|
2370
|
+
prepareClaudePrivateHelperConnection,
|
|
2371
|
+
privateClaudeHelperEnvironment,
|
|
2372
|
+
requestClaudeApprovalControl,
|
|
2373
|
+
requireClaudePrivateQueryEffectQualified,
|
|
2374
|
+
runClaudeLaneHelper,
|
|
2375
|
+
runClaudeSmokeHelper,
|
|
2376
|
+
snapshotClaudeMarkdown,
|
|
2377
|
+
validateInspectorEndpoint,
|
|
2378
|
+
verifyClaudeOrganizationPolicy,
|
|
2379
|
+
verifyClaudePlanContext
|
|
2380
|
+
};
|