@topolo/cli 0.13.2 → 0.13.4
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/agent-packs/README.md +81 -3
- package/agent-packs/coverage_changed.py +122 -0
- package/agent-packs/src/topolo_agent_pack/approvals.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +49 -5
- package/agent-packs/src/topolo_agent_pack/execution.py +27 -9
- package/agent-packs/src/topolo_agent_pack/live.py +338 -5
- package/agent-packs/src/topolo_agent_pack/permissions.py +167 -0
- package/agent-packs/src/topolo_agent_pack/policy.py +11 -6
- package/agent-packs/src/topolo_agent_pack/topolo.py +264 -12
- package/dist/{claude-pack-backend-IDQQX3DG.js → claude-pack-backend-5DWMIDRA.js} +442 -10
- package/dist/index.js +48 -14
- package/package.json +3 -3
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/lib/claude-pack-backend.ts
|
|
4
|
-
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
5
5
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
6
6
|
import * as filesystem from "fs";
|
|
7
7
|
import * as cryptography from "crypto";
|
|
8
8
|
import * as paths from "path";
|
|
9
|
-
import { spawn } from "child_process";
|
|
10
|
-
import { resolve, sep } 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 { tmpdir } from "os";
|
|
11
14
|
import { WebSocket } from "undici";
|
|
12
15
|
|
|
13
16
|
// src/lib/claude-native-pack.ts
|
|
@@ -44,7 +47,7 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
44
47
|
const groups = manager.listCustomGroups();
|
|
45
48
|
if (!groups) throw new Error("Native sidebar groups have not been reported by the running window.");
|
|
46
49
|
for (const member of plan.members) {
|
|
47
|
-
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"].includes(member.permissionMode)))
|
|
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)))
|
|
48
51
|
throw new Error("Unqualified session settings in plan.");
|
|
49
52
|
if (!plan.noEffects && !models.includes(member.model)) throw new Error("Requested model is absent from the native account catalog: " + member.model);
|
|
50
53
|
if (!plan.noEffects && member.group !== null && groups.filter((g) => g.name === member.group).length > 1)
|
|
@@ -212,6 +215,263 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
// src/lib/claude-pack-approvals.ts
|
|
219
|
+
async function nativeClaudeApprovalControl(manager, descriptor, token, net, fs, invoke, qualify, checkpoint) {
|
|
220
|
+
const key = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approval-control.v1");
|
|
221
|
+
qualify();
|
|
222
|
+
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.");
|
|
223
|
+
const previous = manager[key];
|
|
224
|
+
if (fs.existsSync(descriptor.socket)) throw new Error("Native control socket path already exists; no unowned takeover.");
|
|
225
|
+
if (previous) {
|
|
226
|
+
if (previous.active) throw new Error("Native approval control requests are still in flight.");
|
|
227
|
+
await new Promise((done, fail) => previous.server.close((error) => error ? fail(error) : done()));
|
|
228
|
+
delete manager[key];
|
|
229
|
+
}
|
|
230
|
+
const control = { active: 0, server: null, descriptor };
|
|
231
|
+
const server = net.createServer((connection) => {
|
|
232
|
+
let buffer = "";
|
|
233
|
+
let dispatched = false;
|
|
234
|
+
connection.setEncoding("utf8");
|
|
235
|
+
connection.setTimeout(5e3, () => connection.destroy());
|
|
236
|
+
connection.on("error", () => connection.destroy());
|
|
237
|
+
connection.on("data", async (chunk) => {
|
|
238
|
+
if (dispatched) return;
|
|
239
|
+
buffer += chunk;
|
|
240
|
+
if (Buffer.byteLength(buffer) > 65536) {
|
|
241
|
+
dispatched = true;
|
|
242
|
+
connection.destroy();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (!buffer.includes("\n")) return;
|
|
246
|
+
dispatched = true;
|
|
247
|
+
control.active++;
|
|
248
|
+
try {
|
|
249
|
+
if (control.failure) throw new Error(control.failure);
|
|
250
|
+
qualify();
|
|
251
|
+
const request = JSON.parse(buffer);
|
|
252
|
+
if (Object.keys(request).sort().join(",") !== "identity,operation,pid,token" || request.token !== token || request.pid !== descriptor.pid || request.identity !== descriptor.identity)
|
|
253
|
+
throw new Error("Native approval control authentication or process identity differs.");
|
|
254
|
+
const op = request.operation;
|
|
255
|
+
if (!op || !["list", "details", "respond"].includes(op.kind) || Object.keys(op).some((k) => !["kind", "key", "intake", "fingerprint", "decision", "humanDecisionRecorded"].includes(k)))
|
|
256
|
+
throw new Error("Unsupported native control operation.");
|
|
257
|
+
const result = await invoke(op);
|
|
258
|
+
const reply = JSON.stringify({ result }) + "\n";
|
|
259
|
+
if (Buffer.byteLength(reply) > 262144) throw new Error("Native approval details exceed bounded transport; use the original native interaction.");
|
|
260
|
+
connection.end(reply);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
try {
|
|
263
|
+
checkpoint({ kind: "approval-control-refused" });
|
|
264
|
+
} catch {
|
|
265
|
+
control.failure = "Native approval audit unavailable; reconcile through the original interaction.";
|
|
266
|
+
}
|
|
267
|
+
connection.end(JSON.stringify({ error: control.failure || (error instanceof Error ? error.message : "Native approval control refused.") }) + "\n");
|
|
268
|
+
} finally {
|
|
269
|
+
control.active--;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
control.server = server;
|
|
274
|
+
await new Promise((done, fail) => {
|
|
275
|
+
const failed = (error) => fail(error);
|
|
276
|
+
server.once("error", failed);
|
|
277
|
+
server.listen(descriptor.socket, () => {
|
|
278
|
+
server.off("error", failed);
|
|
279
|
+
done();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
fs.chmodSync(descriptor.socket, 384);
|
|
284
|
+
checkpoint({ kind: "approval-control-installed", ...descriptor });
|
|
285
|
+
} catch (error) {
|
|
286
|
+
await new Promise((done) => server.close(() => done()));
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
server.on("error", () => {
|
|
290
|
+
control.failure = "Native approval control failed; reconcile through the original interaction.";
|
|
291
|
+
try {
|
|
292
|
+
checkpoint({ kind: "approval-control-error" });
|
|
293
|
+
} catch {
|
|
294
|
+
control.failure = "Native approval audit unavailable; reconcile through the original interaction.";
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
manager[key] = control;
|
|
298
|
+
return { state: "native-approval-control-installed", ...descriptor };
|
|
299
|
+
}
|
|
300
|
+
async function nativeClaudeApprovalOperation(manager, routes, operation, fingerprint, checkpoint, history = []) {
|
|
301
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1");
|
|
302
|
+
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.");
|
|
303
|
+
const byId = new Map(routes.map((r) => [r.sessionId, r]));
|
|
304
|
+
const byKey = new Map(routes.map((r) => [r.key, r]));
|
|
305
|
+
for (const r of routes) {
|
|
306
|
+
const intake = byKey.get(r.intakeKey);
|
|
307
|
+
if (!/^org:.+$/.test(r.contextKey) || !intake || !intake.key.includes("/intakes/") || intake.contextKey !== r.contextKey || intake.sessionId !== r.intakeSessionId)
|
|
308
|
+
throw new Error("Approval route has no same-context owned intake.");
|
|
309
|
+
}
|
|
310
|
+
const identity = fingerprint(routes);
|
|
311
|
+
let state = manager[symbol];
|
|
312
|
+
if (state?.auditFailed) throw new Error("Native approval audit unavailable; use the original native interaction.");
|
|
313
|
+
const owned = async (route) => {
|
|
314
|
+
const session = await manager.getSession(route.sessionId);
|
|
315
|
+
if (!session || session.sessionId !== route.sessionId || session.cwd !== route.cwd || session.title !== route.title || session.isArchived || session.backend?.kind !== "local" || session.backend.remoteTarget || session.worktreePath || session.worktreeLazy || session.spawnedFrom)
|
|
316
|
+
throw new Error("Native approval session identity differs: " + route.key);
|
|
317
|
+
return session;
|
|
318
|
+
};
|
|
319
|
+
if (state && state.identity !== identity) {
|
|
320
|
+
if (operation.kind !== "install" || state.notifications.size || [...state.entries.values()].some((e) => ["pending", "response-may-have-effects"].includes(e.record.state)))
|
|
321
|
+
throw new Error("An existing native approval owner has a different roster.");
|
|
322
|
+
for (const r of routes) await owned(r);
|
|
323
|
+
for (const route of state.routes) {
|
|
324
|
+
const session = await manager.getSession(route.sessionId);
|
|
325
|
+
if (!session || session.cwd !== route.cwd || session.title !== route.title || session.isRunning || session.starting || session.pendingToolPermissions?.length || manager.hasLosableWork(route.sessionId))
|
|
326
|
+
throw new Error("Outgoing approval owner is not safely idle.");
|
|
327
|
+
}
|
|
328
|
+
if (typeof manager.off !== "function") throw new Error("Native approval listener ownership cannot be released.");
|
|
329
|
+
manager.off("event", state.listener);
|
|
330
|
+
delete manager[symbol];
|
|
331
|
+
state = void 0;
|
|
332
|
+
}
|
|
333
|
+
const current = async (entry2) => {
|
|
334
|
+
const source2 = await owned(byId.get(entry2.record.sourceSessionId));
|
|
335
|
+
const matches = (source2.pendingToolPermissions ?? []).filter((r) => r.requestId === entry2.request.requestId);
|
|
336
|
+
if (matches.length !== 1 || fingerprint(matches[0]) !== entry2.record.fingerprint)
|
|
337
|
+
throw new Error("Exact native request is no longer pending; no replay.");
|
|
338
|
+
return matches[0];
|
|
339
|
+
};
|
|
340
|
+
if (operation.kind === "install") {
|
|
341
|
+
for (const r of routes) await owned(r);
|
|
342
|
+
if (!state) {
|
|
343
|
+
if (typeof manager.on !== "function" || typeof manager.sendMessage !== "function" || typeof manager.respondToToolPermission !== "function") throw new Error("Native approval transport unavailable.");
|
|
344
|
+
state = { identity, routes, entries: /* @__PURE__ */ new Map(), notifications: /* @__PURE__ */ new Set(), deliveryAttempts: 0 };
|
|
345
|
+
manager[symbol] = state;
|
|
346
|
+
const save = (entry2) => checkpoint({ ...entry2.record });
|
|
347
|
+
const deliver = async (entry2) => {
|
|
348
|
+
const record = entry2.record;
|
|
349
|
+
if (state.notifications.has(record.key) || !["not-delivered", "awaiting-idle-intake"].includes(record.notificationState)) return;
|
|
350
|
+
if (record.sourceSessionId === record.intakeSessionId) {
|
|
351
|
+
record.notificationState = "native-intake";
|
|
352
|
+
save(entry2);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
state.notifications.add(record.key);
|
|
356
|
+
try {
|
|
357
|
+
await current(entry2);
|
|
358
|
+
const intake = await owned(byId.get(record.intakeSessionId));
|
|
359
|
+
if (manager.hasLosableWork(record.intakeSessionId) || intake.isRunning || intake.starting || intake.pendingToolPermissions?.length) {
|
|
360
|
+
record.notificationState = "awaiting-idle-intake";
|
|
361
|
+
save(entry2);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
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 + ". 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." : "");
|
|
365
|
+
record.notificationState = "may-have-effects";
|
|
366
|
+
save(entry2);
|
|
367
|
+
state.deliveryAttempts++;
|
|
368
|
+
const ack = await manager.sendMessage(
|
|
369
|
+
record.intakeSessionId,
|
|
370
|
+
text,
|
|
371
|
+
void 0,
|
|
372
|
+
{ initiator: "agent", origin: { kind: "task-notification" } }
|
|
373
|
+
);
|
|
374
|
+
record.notificationState = ack?.delivered === true ? "delivered" : "delivery-refused";
|
|
375
|
+
save(entry2);
|
|
376
|
+
} catch {
|
|
377
|
+
record.notificationState = record.notificationState === "may-have-effects" ? "delivery-uncertain" : "delivery-failed";
|
|
378
|
+
save(entry2);
|
|
379
|
+
} finally {
|
|
380
|
+
state.notifications.delete(record.key);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const capture = async (sessionId, request) => {
|
|
384
|
+
const route = byId.get(sessionId);
|
|
385
|
+
if (!route || !request || typeof request.requestId !== "string") return;
|
|
386
|
+
const source2 = await owned(route);
|
|
387
|
+
const matches = (source2.pendingToolPermissions ?? []).filter((r) => r.requestId === request.requestId);
|
|
388
|
+
if (matches.length !== 1) throw new Error("Native event is not in the owned pending inventory.");
|
|
389
|
+
const pending2 = matches[0];
|
|
390
|
+
const key = fingerprint([identity, pending2.requestId]);
|
|
391
|
+
const hash = fingerprint(pending2);
|
|
392
|
+
let entry2 = state.entries.get(key);
|
|
393
|
+
if (entry2 && entry2.record.fingerprint !== hash) throw new Error("Native request identity was reused.");
|
|
394
|
+
if (!entry2) {
|
|
395
|
+
const old = history.filter((row) => row.key === key).at(-1);
|
|
396
|
+
if (old && (old.fingerprint !== hash || old.sourceSessionId !== sessionId || old.intakeSessionId !== route.intakeSessionId || old.contextKey !== route.contextKey))
|
|
397
|
+
throw new Error("Durable approval identity differs; no replay.");
|
|
398
|
+
entry2 = { request: pending2, record: {
|
|
399
|
+
key,
|
|
400
|
+
fingerprint: hash,
|
|
401
|
+
sourceKey: route.key,
|
|
402
|
+
sourceSessionId: sessionId,
|
|
403
|
+
intakeKey: route.intakeKey,
|
|
404
|
+
intakeSessionId: route.intakeSessionId,
|
|
405
|
+
contextKey: route.contextKey,
|
|
406
|
+
state: "pending",
|
|
407
|
+
notificationState: "not-delivered",
|
|
408
|
+
cardOnly: pending2.onlyOnCard === true || pending2.noAllowOnNotification === true,
|
|
409
|
+
nativeApproved: false,
|
|
410
|
+
...old ? { state: old.state, notificationState: old.notificationState } : {}
|
|
411
|
+
} };
|
|
412
|
+
state.entries.set(key, entry2);
|
|
413
|
+
save(entry2);
|
|
414
|
+
}
|
|
415
|
+
if (entry2.record.state === "pending") await deliver(entry2);
|
|
416
|
+
};
|
|
417
|
+
state.listener = async (event) => {
|
|
418
|
+
try {
|
|
419
|
+
if (event?.type === "tool_permission_request") await capture(event.sessionId, event.request);
|
|
420
|
+
else if (event?.type === "session_updated") {
|
|
421
|
+
const source2 = byId.get(event.sessionId);
|
|
422
|
+
if (source2) {
|
|
423
|
+
const session = await owned(source2);
|
|
424
|
+
for (const entry2 of state.entries.values())
|
|
425
|
+
if (entry2.record.state === "pending" && entry2.record.sourceSessionId === event.sessionId && !(session.pendingToolPermissions ?? []).some((r) => r.requestId === entry2.request.requestId)) {
|
|
426
|
+
entry2.record.state = "native-request-no-longer-pending";
|
|
427
|
+
delete entry2.request;
|
|
428
|
+
save(entry2);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
for (const entry2 of state.entries.values())
|
|
432
|
+
if (entry2.record.state === "pending" && entry2.record.intakeSessionId === event.sessionId) await deliver(entry2);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
try {
|
|
436
|
+
checkpoint({ kind: "approval-routing-refused", sessionId: event?.sessionId });
|
|
437
|
+
} catch {
|
|
438
|
+
state.auditFailed = true;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
manager.on("event", state.listener);
|
|
443
|
+
checkpoint({ kind: "approval-listener-installed", identity, roles: routes.length });
|
|
444
|
+
for (const r of routes) for (const request of (await owned(r)).pendingToolPermissions ?? []) await capture(r.sessionId, request);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
state: "native-approval-listener-installed",
|
|
448
|
+
identity,
|
|
449
|
+
roles: routes.length,
|
|
450
|
+
modelTurnsMayHaveStartedByInstallation: state.deliveryAttempts > 0
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (!state) throw new Error("Native approval listener is not installed.");
|
|
454
|
+
if (operation.kind === "list") return { requests: [...state.entries.values()].map((e) => ({ ...e.record })) };
|
|
455
|
+
const entry = state.entries.get(operation.key);
|
|
456
|
+
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.");
|
|
457
|
+
const pending = await current(entry);
|
|
458
|
+
if (manager[symbol] !== state || state.entries.get(operation.key) !== entry || entry.record.state !== "pending" || state.auditFailed)
|
|
459
|
+
throw new Error("Exact pending request changed during native identity validation; reconcile without replay.");
|
|
460
|
+
if (operation.kind === "details") return { record: { ...entry.record }, request: pending };
|
|
461
|
+
if (operation.kind !== "respond" || operation.humanDecisionRecorded !== true || !["once", "deny"].includes(operation.decision ?? ""))
|
|
462
|
+
throw new Error("Recorded human one-request decision required; never auto-approve.");
|
|
463
|
+
if (operation.decision !== "deny" && entry.record.cardOnly) throw new Error("Claude requires the original card; intake cannot impersonate its origin.");
|
|
464
|
+
entry.record.state = "response-may-have-effects";
|
|
465
|
+
checkpoint({ ...entry.record });
|
|
466
|
+
await manager.respondToToolPermission(pending.requestId, operation.decision, void 0, "elsewhere");
|
|
467
|
+
const source = await owned(byId.get(entry.record.sourceSessionId));
|
|
468
|
+
if (source.pendingToolPermissions?.some((r) => r.requestId === pending.requestId))
|
|
469
|
+
throw new Error("Native decision did not settle; reconcile without replay.");
|
|
470
|
+
entry.record.state = "native-request-settled";
|
|
471
|
+
checkpoint({ ...entry.record });
|
|
472
|
+
return { state: entry.record.state, nativeApproved: false, operationEffectVerified: false };
|
|
473
|
+
}
|
|
474
|
+
|
|
215
475
|
// src/lib/claude-pack-backend.ts
|
|
216
476
|
var CLAUDE_NATIVE_QUALIFIER = {
|
|
217
477
|
version: "2.31226.1",
|
|
@@ -286,7 +546,10 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
286
546
|
pinned: record.pinnedRequested === true,
|
|
287
547
|
model: settings.model,
|
|
288
548
|
effort: settings.effortLevel,
|
|
289
|
-
permissionMode: settings.permissions.defaultMode
|
|
549
|
+
permissionMode: settings.permissions.defaultMode,
|
|
550
|
+
...policy.installationConsent || settings.permissions.defaultMode === "bypassPermissions" ? {
|
|
551
|
+
permissionConsent: qualifyPermissionConsent(policy, record.key, records, settings.permissions.defaultMode === "bypassPermissions")
|
|
552
|
+
} : {}
|
|
290
553
|
});
|
|
291
554
|
}
|
|
292
555
|
for (const file of [resolve(root, "claude/state.json"), realpathSync(policyPath)])
|
|
@@ -311,7 +574,36 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
311
574
|
outgoing.push({ key, sessionId: record.desktopSessionId, cwd, title: record.title });
|
|
312
575
|
}
|
|
313
576
|
} else if (mode === "switch") throw new Error("Configure and verify an initial active roster before switching.");
|
|
314
|
-
|
|
577
|
+
const approvalRoutes = members.filter((m) => m.permissionConsent).map((m) => {
|
|
578
|
+
const intake = members.find((i) => i.key === m.permissionConsent.intakeKey);
|
|
579
|
+
if (!intake) throw new Error("Owning intake is not in the native roster.");
|
|
580
|
+
return {
|
|
581
|
+
key: m.key,
|
|
582
|
+
sessionId: m.sessionId,
|
|
583
|
+
cwd: m.cwd,
|
|
584
|
+
title: m.title,
|
|
585
|
+
...m.permissionConsent,
|
|
586
|
+
intakeSessionId: intake.sessionId
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
members,
|
|
591
|
+
outgoing,
|
|
592
|
+
context,
|
|
593
|
+
contextInventories,
|
|
594
|
+
approvalRoutes,
|
|
595
|
+
installationConsent: policy.installationConsent,
|
|
596
|
+
sourceRevision: policy.sourceRevision,
|
|
597
|
+
mode,
|
|
598
|
+
activeBefore: active
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function qualifyPermissionConsent(policy, key, records, full) {
|
|
602
|
+
const consent = policy.installationConsent;
|
|
603
|
+
const intakeKey = consent?.intakeRoutes?.[key];
|
|
604
|
+
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/")))
|
|
605
|
+
throw new Error("Explicit organization-bound full access and owned intake consent required.");
|
|
606
|
+
return { contextKey: consent.contextKey, intakeKey };
|
|
315
607
|
}
|
|
316
608
|
function nativePackExpression(pid, plan, journal, apply) {
|
|
317
609
|
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error("Explicit Claude main-process PID required.");
|
|
@@ -344,12 +636,39 @@ function nativePackExpression(pid, plan, journal, apply) {
|
|
|
344
636
|
if (crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== hash)
|
|
345
637
|
throw new Error('Context or transcript changed during configuration: ' + file);
|
|
346
638
|
};
|
|
347
|
-
|
|
639
|
+
const result = await (${nativeClaudePackOperation.toString()})(manager,
|
|
348
640
|
value => transport.U(value, transport.V),
|
|
349
641
|
{ members: ${JSON.stringify(plan.members)}, outgoing: ${JSON.stringify(plan.outgoing)},
|
|
350
642
|
operationId: ${JSON.stringify(plan.operationId)}, mode: ${JSON.stringify(plan.recovery ? "recover" : plan.mode)},
|
|
351
643
|
noEffects: ${JSON.stringify(plan.noEffects)},
|
|
352
644
|
deadline: Date.now() + 45000 }, expected.apply, checkpoint, verifyContext);
|
|
645
|
+
${plan.approvalRoutes.length ? `const routes = ${JSON.stringify(plan.approvalRoutes)};
|
|
646
|
+
const fingerprint = value => crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
647
|
+
const invoke = op => (${nativeClaudeApprovalOperation.toString()})(manager, routes, op, fingerprint,
|
|
648
|
+
row => checkpoint({kind: 'approval', ...row}),
|
|
649
|
+
fs.readFileSync(expected.journal, 'utf8').split('\\n').filter(Boolean).map(line => JSON.parse(line)));
|
|
650
|
+
if (expected.apply) {
|
|
651
|
+
result.approvalRouting = await invoke({kind: 'install'});
|
|
652
|
+
const control = ${JSON.stringify(plan.control)};
|
|
653
|
+
if (!control) throw new Error('Owned native control setup is unavailable.');
|
|
654
|
+
const stat = fs.lstatSync(control.secretPath);
|
|
655
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid() || (stat.mode & 63)) throw new Error('Unowned native control secret.');
|
|
656
|
+
const secret = JSON.parse(fs.readFileSync(control.secretPath, 'utf8'));
|
|
657
|
+
const qualifyNative = () => {
|
|
658
|
+
if (process.pid !== expected.pid || load('electron').app.getVersion() !== expected.qualifier.version) throw new Error('Native process changed.');
|
|
659
|
+
for (const [file, hash] of Object.entries(expected.qualifier.hashes))
|
|
660
|
+
if (crypto.createHash('sha256').update(fs.readFileSync(base + file)).digest('hex') !== hash) throw new Error('Native module changed.');
|
|
661
|
+
};
|
|
662
|
+
result.approvalRouting.control = await (${nativeClaudeApprovalControl.toString()})(manager,
|
|
663
|
+
{socket: control.socket, pid: expected.pid, identity: fingerprint(routes)}, secret.token,
|
|
664
|
+
load('node:net'), fs, invoke, qualifyNative, checkpoint);
|
|
665
|
+
} else if (${JSON.stringify(plan.recovery === true && !plan.noEffects)}) {
|
|
666
|
+
const current = manager[Symbol.for('topolo.agent-pack.approval-control.v1')];
|
|
667
|
+
if (!current || current.descriptor.pid !== expected.pid || current.descriptor.identity !== fingerprint(routes))
|
|
668
|
+
throw new Error('Native intake control is not reconciled.');
|
|
669
|
+
result.approvalRouting = {state: 'native-approval-listener-installed', roles: routes.length, control: current.descriptor};
|
|
670
|
+
}` : ""}
|
|
671
|
+
return result;
|
|
353
672
|
})()`;
|
|
354
673
|
}
|
|
355
674
|
function evaluateClaudeMain(endpoint, expression, socketFactory = (url) => new WebSocket(url)) {
|
|
@@ -447,7 +766,24 @@ function verifyPlanContext(plan) {
|
|
|
447
766
|
for (const [file, hash] of Object.entries(plan.context))
|
|
448
767
|
if (createHash("sha256").update(readFileSync(file)).digest("hex") !== hash) throw new Error("Context or transcript changed during configuration: " + file);
|
|
449
768
|
}
|
|
450
|
-
|
|
769
|
+
function verifyClaudeOrganizationPolicy(root, pack, consent, run = spawnSync, locate = existsSync) {
|
|
770
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
771
|
+
const bundled = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(locate);
|
|
772
|
+
if (!bundled) throw new Error("Bundled organization policy loader unavailable.");
|
|
773
|
+
const source = realpathSync(bundled);
|
|
774
|
+
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))';
|
|
775
|
+
const result = run(
|
|
776
|
+
"python3",
|
|
777
|
+
["-I", "-B", "-c", script, root, pack, JSON.stringify(consent), source],
|
|
778
|
+
{ encoding: "utf8", timeout: 2e5, cwd: source, env: { ...process.env } }
|
|
779
|
+
);
|
|
780
|
+
if (result.error || result.status !== 0) throw new Error("Fresh authenticated organization policy qualification failed; no native permission widening.");
|
|
781
|
+
const proof = JSON.parse(result.stdout);
|
|
782
|
+
if (proof.profileCeilingVerified !== true || proof.contextKey !== consent.contextKey || proof.scopePolicyMode !== "unrestricted" || proof.toolActionRestrictionRequested !== false)
|
|
783
|
+
throw new Error("Organization profile ceiling proof differs.");
|
|
784
|
+
return proof;
|
|
785
|
+
}
|
|
786
|
+
async function configureClaudePack(options, evaluate = evaluateClaudeMain, qualify = verifyClaudeOrganizationPolicy) {
|
|
451
787
|
validateInspectorEndpoint(options.endpoint);
|
|
452
788
|
const root = realpathSync(options.store);
|
|
453
789
|
if (lstatSync(resolve(root, "claude")).isSymbolicLink()) throw new Error("Claude library must not be a symlink.");
|
|
@@ -474,9 +810,23 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain) {
|
|
|
474
810
|
if (createHash("sha256").update(line).digest("hex") !== marker.planHash) throw new Error("Recovery plan identity changed.");
|
|
475
811
|
const record = JSON.parse(line);
|
|
476
812
|
if (record.pack !== options.pack || record.operationId !== marker.operationId || !record.apply) throw new Error("Recovery pack or operation identity differs.");
|
|
477
|
-
plan = { ...record, recovery: true, noEffects: marker.format === 2 && marker.dispatchState === "not-dispatched" && !lines.map((line2) => JSON.parse(line2)).some((r) => r.kind === "attempt") };
|
|
813
|
+
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") };
|
|
478
814
|
} else {
|
|
479
815
|
plan = { ...prepareClaudeNativePlan(root, options.pack, options.policy, options.mode === "switch" ? "switch" : "configure"), operationId };
|
|
816
|
+
if (options.apply && plan.approvalRoutes.length) {
|
|
817
|
+
const directory2 = resolve(root, "claude/private");
|
|
818
|
+
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
819
|
+
const socketDirectory = resolve(tmpdir(), "topolo-claude-pack-" + createHash("sha256").update(root).digest("hex").slice(0, 16));
|
|
820
|
+
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
821
|
+
for (const path of [directory2, socketDirectory]) {
|
|
822
|
+
const stat = lstatSync(path);
|
|
823
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
824
|
+
throw new Error("Native control directory must be owned and private.");
|
|
825
|
+
}
|
|
826
|
+
plan.control = { socket: resolve(socketDirectory, operationId + ".sock"), secretPath: resolve(directory2, operationId + ".json") };
|
|
827
|
+
durableRecord(plan.control.secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
828
|
+
syncDirectory(directory2);
|
|
829
|
+
}
|
|
480
830
|
journal = resolve(directory, operationId + ".jsonl");
|
|
481
831
|
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
482
832
|
durableRecord(journal, record, "wx");
|
|
@@ -487,6 +837,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain) {
|
|
|
487
837
|
}
|
|
488
838
|
try {
|
|
489
839
|
verifyPlanContext(plan);
|
|
840
|
+
const organizationPolicy = options.apply && plan.approvalRoutes.length ? qualify(root, options.pack, plan.installationConsent) : null;
|
|
490
841
|
if (options.apply) {
|
|
491
842
|
const dispatched = { ...marker, dispatchState: "may-have-effects" };
|
|
492
843
|
const temporaryMarker = resolve(root, "claude/native-dispatch-" + operationId + ".json");
|
|
@@ -518,6 +869,8 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain) {
|
|
|
518
869
|
}
|
|
519
870
|
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]))))
|
|
520
871
|
throw new Error("Native outgoing archive result does not match the reviewed roster.");
|
|
872
|
+
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))
|
|
873
|
+
throw new Error("Complete native intake routing acknowledgement is unavailable.");
|
|
521
874
|
const activePath = resolve(root, "claude/active.json");
|
|
522
875
|
const current = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
523
876
|
const active = { operationId: plan.operationId, pack: options.pack, keys: plan.members.map((m) => m.key), sourceRevision: plan.sourceRevision };
|
|
@@ -530,6 +883,23 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain) {
|
|
|
530
883
|
renameSync(temporary, activePath);
|
|
531
884
|
syncDirectory(resolve(root, "claude"));
|
|
532
885
|
durableRecord(journal, { kind: "context-verified", context: plan.context, active });
|
|
886
|
+
if ((options.apply || recovering) && plan.approvalRoutes.length) {
|
|
887
|
+
const target = resolve(root, "claude/approval-connection.json");
|
|
888
|
+
const temporaryConnection = resolve(root, "claude/approval-connection-" + plan.operationId + ".json");
|
|
889
|
+
durableRecord(temporaryConnection, {
|
|
890
|
+
pid: options.pid,
|
|
891
|
+
socket: plan.control.socket,
|
|
892
|
+
secretPath: plan.control.secretPath,
|
|
893
|
+
identity: result.approvalRouting.control.identity,
|
|
894
|
+
pack: options.pack,
|
|
895
|
+
installationConsent: plan.installationConsent,
|
|
896
|
+
routes: plan.approvalRoutes,
|
|
897
|
+
journal,
|
|
898
|
+
organizationPolicy
|
|
899
|
+
}, "wx");
|
|
900
|
+
renameSync(temporaryConnection, target);
|
|
901
|
+
syncDirectory(resolve(root, "claude"));
|
|
902
|
+
}
|
|
533
903
|
if (JSON.stringify(JSON.parse(readFileSync(markerPath, "utf8"))) !== JSON.stringify(marker)) throw new Error("Recovery marker identity changed.");
|
|
534
904
|
unlinkSync(markerPath);
|
|
535
905
|
syncDirectory(resolve(root, "claude"));
|
|
@@ -543,13 +913,75 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain) {
|
|
|
543
913
|
await release();
|
|
544
914
|
}
|
|
545
915
|
}
|
|
916
|
+
function requestClaudeApprovalControl(connection, operation, token, connect = createConnection) {
|
|
917
|
+
const parent = lstatSync(dirname(connection.socket));
|
|
918
|
+
const socket = lstatSync(connection.socket);
|
|
919
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || parent.mode & 63 || !socket.isSocket() || socket.uid !== process.getuid?.() || socket.mode & 63)
|
|
920
|
+
throw new Error("Unowned native control socket.");
|
|
921
|
+
return new Promise((done, fail) => {
|
|
922
|
+
const client = connect({ path: connection.socket });
|
|
923
|
+
let reply = "";
|
|
924
|
+
let finished = false;
|
|
925
|
+
const finish = (error, result) => {
|
|
926
|
+
if (finished) return;
|
|
927
|
+
finished = true;
|
|
928
|
+
client.destroy();
|
|
929
|
+
error ? fail(error) : done(result);
|
|
930
|
+
};
|
|
931
|
+
client.setEncoding("utf8");
|
|
932
|
+
client.setTimeout(1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
933
|
+
client.on("connect", () => client.write(JSON.stringify({ token, pid: connection.pid, identity: connection.identity, operation }) + "\n"));
|
|
934
|
+
client.on("data", (chunk) => {
|
|
935
|
+
reply += chunk;
|
|
936
|
+
if (Buffer.byteLength(reply) > 262144) return finish(new Error("Native approval response exceeds bounded transport."));
|
|
937
|
+
if (!reply.includes("\n")) return;
|
|
938
|
+
try {
|
|
939
|
+
const result = JSON.parse(reply);
|
|
940
|
+
finish(result.error ? new Error(result.error) : void 0, result.result);
|
|
941
|
+
} catch {
|
|
942
|
+
finish(new Error("Invalid native approval response."));
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
client.on("error", () => finish(new Error("Existing native approval control is unavailable.")));
|
|
946
|
+
client.on("close", () => finish(new Error("Native approval response was lost; reconcile without replay.")));
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
async function operateClaudeApprovals(options, request = requestClaudeApprovalControl, qualify = verifyClaudeOrganizationPolicy) {
|
|
950
|
+
const root = realpathSync(options.store);
|
|
951
|
+
const file = resolve(root, "claude/approval-connection.json");
|
|
952
|
+
if (lstatSync(file).isSymbolicLink()) throw new Error("Unowned approval connection.");
|
|
953
|
+
const connection = JSON.parse(readFileSync(file, "utf8"));
|
|
954
|
+
if (!Number.isSafeInteger(connection.pid) || connection.pid < 1 || !Array.isArray(connection.routes))
|
|
955
|
+
throw new Error("Unqualified native approval connection.");
|
|
956
|
+
if (options.kind === "respond" && options.decision === "once")
|
|
957
|
+
qualify(root, connection.pack, connection.installationConsent);
|
|
958
|
+
const journal = realpathSync(connection.journal);
|
|
959
|
+
if (!journal.startsWith(realpathSync(resolve(root, "claude/journals")) + sep)) throw new Error("Unowned approval journal.");
|
|
960
|
+
const privateDirectory = resolve(root, "claude/private");
|
|
961
|
+
const secretPath = realpathSync(connection.secretPath);
|
|
962
|
+
const stat = lstatSync(connection.secretPath);
|
|
963
|
+
if (!secretPath.startsWith(realpathSync(privateDirectory) + sep) || !stat.isFile() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63) throw new Error("Unowned native control secret.");
|
|
964
|
+
const secret = JSON.parse(readFileSync(secretPath, "utf8"));
|
|
965
|
+
if (!/^[a-f0-9]{64}$/.test(secret.token) || !/^[a-f0-9]{64}$/.test(connection.identity)) throw new Error("Native control identity is invalid.");
|
|
966
|
+
return request(connection, {
|
|
967
|
+
kind: options.kind,
|
|
968
|
+
key: options.key,
|
|
969
|
+
intake: options.intake,
|
|
970
|
+
fingerprint: options.fingerprint,
|
|
971
|
+
decision: options.decision,
|
|
972
|
+
humanDecisionRecorded: options.humanDecisionRecorded
|
|
973
|
+
}, secret.token);
|
|
974
|
+
}
|
|
546
975
|
export {
|
|
547
976
|
CLAUDE_NATIVE_QUALIFIER,
|
|
548
977
|
configureClaudePack,
|
|
549
978
|
evaluateClaudeMain,
|
|
550
979
|
lockClaudeLibrary,
|
|
551
980
|
nativePackExpression,
|
|
981
|
+
operateClaudeApprovals,
|
|
552
982
|
prepareClaudeNativePlan,
|
|
983
|
+
requestClaudeApprovalControl,
|
|
553
984
|
snapshotClaudeMarkdown,
|
|
554
|
-
validateInspectorEndpoint
|
|
985
|
+
validateInspectorEndpoint,
|
|
986
|
+
verifyClaudeOrganizationPolicy
|
|
555
987
|
};
|