@topolo/cli 0.13.9 → 0.13.11
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 +46 -5
- package/agent-packs/README.md +128 -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 +42 -5
- package/agent-packs/src/topolo_agent_pack/execution.py +70 -2
- package/agent-packs/src/topolo_agent_pack/lanes.py +902 -0
- package/agent-packs/src/topolo_agent_pack/live.py +982 -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 +172 -8
- package/dist/{claude-pack-backend-4F4VHMI5.js → claude-pack-backend-YEVPAOID.js} +969 -16
- package/dist/index.js +65 -8
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/lib/claude-pack-backend.ts
|
|
4
|
-
import { createHash, randomBytes, randomUUID } from "crypto";
|
|
4
|
+
import { createHash as createHash2, 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";
|
|
@@ -13,6 +13,7 @@ import { createConnection } from "net";
|
|
|
13
13
|
import { WebSocket } from "undici";
|
|
14
14
|
|
|
15
15
|
// src/lib/claude-native-pack.ts
|
|
16
|
+
import { createHash } from "crypto";
|
|
16
17
|
async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoint, verifyContext = () => {
|
|
17
18
|
}) {
|
|
18
19
|
const fenceKey = /* @__PURE__ */ Symbol.for("topolo.agent-pack.native-operation.v1");
|
|
@@ -362,6 +363,259 @@ async function nativeClaudePackOperation(manager, sidebar, plan, apply, checkpoi
|
|
|
362
363
|
throw error;
|
|
363
364
|
}
|
|
364
365
|
}
|
|
366
|
+
async function nativeClaudeDynamicLaneOperation(manager, plan, operation, checkpoint, verifyContext, qualifyOrganization, verifyHistory) {
|
|
367
|
+
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)
|
|
368
|
+
throw new Error("Exact child, source role and originating intake plan required.");
|
|
369
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-lane.operation.v1");
|
|
370
|
+
if (manager[symbol]) throw new Error("An unresolved child operation retains native custody.");
|
|
371
|
+
const fence = () => {
|
|
372
|
+
if (Date.now() >= plan.deadline) throw new Error("Child operation deadline expired; no retry.");
|
|
373
|
+
verifyContext();
|
|
374
|
+
};
|
|
375
|
+
const fixed = async (role) => {
|
|
376
|
+
const raw = await manager.getSessionWhenLoaded(role.sessionId, "topolo-dynamic-source", { warnOnMiss: false });
|
|
377
|
+
const dto = await manager.getSession(role.sessionId);
|
|
378
|
+
fence();
|
|
379
|
+
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)
|
|
380
|
+
throw new Error("Source role or intake raw native identity differs.");
|
|
381
|
+
return { raw, dto };
|
|
382
|
+
};
|
|
383
|
+
const child = async (allowArchived = false) => {
|
|
384
|
+
await fixed(plan.source);
|
|
385
|
+
await fixed(plan.intake);
|
|
386
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-child", { warnOnMiss: false });
|
|
387
|
+
const dto = await manager.getSession(plan.nativeId);
|
|
388
|
+
fence();
|
|
389
|
+
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)
|
|
390
|
+
throw new Error("Actual child raw descriptor or original native parent differs.");
|
|
391
|
+
const busy = raw.isRunning || raw.starting || dto.isRunning || dto.starting || raw.pendingToolPermissions?.length || dto.pendingToolPermissions?.length || manager.hasLosableWork(plan.nativeId);
|
|
392
|
+
return {
|
|
393
|
+
nativeId: plan.nativeId,
|
|
394
|
+
cwd: dto.cwd,
|
|
395
|
+
model: dto.model,
|
|
396
|
+
effort: dto.effort,
|
|
397
|
+
permissionMode: dto.permissionMode,
|
|
398
|
+
nativeParentId: raw.spawnedFrom.sessionId,
|
|
399
|
+
archived: raw.isArchived === true && dto.isArchived === true,
|
|
400
|
+
busy: !!busy,
|
|
401
|
+
pendingApprovals: dto.pendingToolPermissions?.length ?? 0,
|
|
402
|
+
backend: "local",
|
|
403
|
+
foreignWorkspace: false
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
fence();
|
|
407
|
+
const before = await child();
|
|
408
|
+
if (!manager.getAvailableCodeModels().includes(plan.model)) throw new Error("Exact task model is unavailable; no parent fallback.");
|
|
409
|
+
if (operation === "read") return { state: "native-child-read", ...before, settingsACK: false, contextACK: false };
|
|
410
|
+
if (before.busy) throw new Error("Own child has active or pending work.");
|
|
411
|
+
await qualifyOrganization();
|
|
412
|
+
fence();
|
|
413
|
+
let attempted = false;
|
|
414
|
+
manager[symbol] = { laneId: plan.laneId, planDigest: plan.planDigest, attempted: false };
|
|
415
|
+
try {
|
|
416
|
+
if (operation === "retire") {
|
|
417
|
+
if (plan.lifecycleState !== "settled") throw new Error("Only a settled child with verified operation outcome may retire.");
|
|
418
|
+
const history = await verifyHistory(plan.nativeId);
|
|
419
|
+
if (typeof manager.archiveCascadeClosureOf !== "function" || typeof manager.archiveSession !== "function")
|
|
420
|
+
throw new Error("Qualified preserved-history native archive API unavailable.");
|
|
421
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-retire", { warnOnMiss: false });
|
|
422
|
+
const closure = manager.archiveCascadeClosureOf(raw);
|
|
423
|
+
if (!Array.isArray(closure) || closure.length) throw new Error("Child archive would affect another native session.");
|
|
424
|
+
const current = await child();
|
|
425
|
+
if (current.busy) throw new Error("Child became busy before archive.");
|
|
426
|
+
fence();
|
|
427
|
+
checkpoint({ state: "archive-may-have-effects", laneId: plan.laneId, nativeId: plan.nativeId });
|
|
428
|
+
attempted = true;
|
|
429
|
+
manager[symbol].attempted = true;
|
|
430
|
+
await manager.archiveSession(plan.nativeId, { cleanupWorktree: false, forceWorktreeCleanup: false, discardUntracked: false, isAutomatic: true });
|
|
431
|
+
const after2 = await child(true);
|
|
432
|
+
if (!after2.archived || after2.busy || await verifyHistory(plan.nativeId, history) !== true)
|
|
433
|
+
throw new Error("Native child archive or transcript preservation is unproven.");
|
|
434
|
+
checkpoint({ state: "retired", laneId: plan.laneId, nativeId: plan.nativeId, historyPreserved: true });
|
|
435
|
+
delete manager[symbol];
|
|
436
|
+
return { state: "retired", ...after2, historyPreserved: true };
|
|
437
|
+
}
|
|
438
|
+
for (const field of ["model", "effort", "permissionMode"]) {
|
|
439
|
+
const current = await child();
|
|
440
|
+
if (current.busy) throw new Error("Child became busy before native settings effect.");
|
|
441
|
+
if (current[field] === plan[field]) continue;
|
|
442
|
+
await qualifyOrganization();
|
|
443
|
+
fence();
|
|
444
|
+
if (!manager.getAvailableCodeModels().includes(plan.model)) throw new Error("Native account catalog changed.");
|
|
445
|
+
checkpoint({ state: "settings-may-have-effects", laneId: plan.laneId, nativeId: plan.nativeId, field });
|
|
446
|
+
attempted = true;
|
|
447
|
+
manager[symbol].attempted = true;
|
|
448
|
+
if (field === "model") await manager.setModel(plan.nativeId, plan.model);
|
|
449
|
+
else if (field === "effort") await manager.setEffort(plan.nativeId, plan.effort);
|
|
450
|
+
else if (!await manager.setPermissionMode(plan.nativeId, plan.permissionMode, "picker")) throw new Error("Child permission setting refused.");
|
|
451
|
+
const actual = await child();
|
|
452
|
+
if (actual[field] !== plan[field]) throw new Error("Actual child " + field + " differs; no fallback or task dispatch.");
|
|
453
|
+
checkpoint({ state: "native-field-ACK", laneId: plan.laneId, nativeId: plan.nativeId, field, value: actual[field] });
|
|
454
|
+
}
|
|
455
|
+
const after = await child();
|
|
456
|
+
const processRecord = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-process-settings", { warnOnMiss: false });
|
|
457
|
+
fence();
|
|
458
|
+
if (!processRecord?.query || typeof manager.getEffort !== "function" || typeof manager.readRunningModel !== "function")
|
|
459
|
+
throw new Error("Actual child process settings are unavailable; settings ACK is unproven.");
|
|
460
|
+
const query = processRecord.query;
|
|
461
|
+
const actualModel = await manager.readRunningModel(processRecord, query);
|
|
462
|
+
const actualEffort = await manager.getEffort(plan.nativeId);
|
|
463
|
+
const reread = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-process-settings", { warnOnMiss: false });
|
|
464
|
+
const finalChild = await child();
|
|
465
|
+
if (reread !== processRecord || reread.query !== query || actualModel !== plan.model || actualEffort !== plan.effort || finalChild.busy)
|
|
466
|
+
throw new Error("Actual child process model/effort differs or is unavailable; settings ACK is unproven.");
|
|
467
|
+
await qualifyOrganization();
|
|
468
|
+
fence();
|
|
469
|
+
if (after.model !== plan.model || after.effort !== plan.effort || after.permissionMode !== plan.permissionMode || after.busy)
|
|
470
|
+
throw new Error("Actual child settings are not the complete requested tuple.");
|
|
471
|
+
checkpoint({ state: "configured-settings-ACK", laneId: plan.laneId, nativeId: plan.nativeId, after });
|
|
472
|
+
delete manager[symbol];
|
|
473
|
+
return { state: "configured-settings-ACK", ...after, settingsACK: true, contextACK: false, modelTurnsStarted: false };
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (!attempted) delete manager[symbol];
|
|
476
|
+
else checkpoint({ state: "uncertain-effect-hold", laneId: plan.laneId, nativeId: plan.nativeId });
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function nativeClaudeDynamicContext(manager, plan, operation, helper, fingerprint, verifyContext, qualifyOrganization) {
|
|
481
|
+
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))
|
|
482
|
+
throw new Error("Closed own-child context operation required.");
|
|
483
|
+
const ready = async () => {
|
|
484
|
+
verifyContext();
|
|
485
|
+
const raw = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-context", { warnOnMiss: false });
|
|
486
|
+
const view = await manager.getSession(plan.nativeId);
|
|
487
|
+
verifyContext();
|
|
488
|
+
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))
|
|
489
|
+
throw new Error("Exact configured child is not idle on the original native parent.");
|
|
490
|
+
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)
|
|
491
|
+
throw new Error("Current actual child process model/effort ACK differs.");
|
|
492
|
+
await qualifyOrganization();
|
|
493
|
+
verifyContext();
|
|
494
|
+
};
|
|
495
|
+
await ready();
|
|
496
|
+
if (typeof manager.getTranscriptForCallerElsewhere !== "function") throw new Error("Qualified own-child transcript resolver unavailable.");
|
|
497
|
+
if (operation.kind === "context-read") {
|
|
498
|
+
const history = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
499
|
+
await ready();
|
|
500
|
+
if (!Array.isArray(history) || !history.length) throw new Error("Own-child context transcript is missing; no acknowledgement.");
|
|
501
|
+
return helper("context-ack", { laneId: plan.laneId, operationId: operation.operationId, messages: history });
|
|
502
|
+
}
|
|
503
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-context.v1");
|
|
504
|
+
if (manager[symbol]) throw new Error("An unresolved child bootstrap owns delivery; no replay.");
|
|
505
|
+
manager[symbol] = { laneId: plan.laneId, operationId: operation.operationId };
|
|
506
|
+
let armed = false;
|
|
507
|
+
try {
|
|
508
|
+
const bootstrap = await helper("context-prepare", { laneId: plan.laneId, operationId: operation.operationId });
|
|
509
|
+
const history = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
510
|
+
if (!Array.isArray(history)) throw new Error("Exact own-child pre-bootstrap history is unavailable.");
|
|
511
|
+
await ready();
|
|
512
|
+
await helper("context-mark", {
|
|
513
|
+
laneId: plan.laneId,
|
|
514
|
+
operationId: operation.operationId,
|
|
515
|
+
state: "start-may-have-effects",
|
|
516
|
+
historyCount: history.length,
|
|
517
|
+
historyDigest: fingerprint(history)
|
|
518
|
+
});
|
|
519
|
+
armed = true;
|
|
520
|
+
const result = await manager.sendMessage(
|
|
521
|
+
plan.nativeId,
|
|
522
|
+
bootstrap.text,
|
|
523
|
+
void 0,
|
|
524
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
525
|
+
);
|
|
526
|
+
const state = result?.delivered === true ? "native-delivered" : result?.delivered === false ? "native-refused" : "uncertain";
|
|
527
|
+
await helper("context-mark", { laneId: plan.laneId, operationId: operation.operationId, state });
|
|
528
|
+
if (state === "uncertain") throw new Error("Child context delivery is uncertain; never replay.");
|
|
529
|
+
delete manager[symbol];
|
|
530
|
+
return { state, laneId: plan.laneId, operationId: operation.operationId, contextACK: false };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (!armed) delete manager[symbol];
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function nativeClaudeDynamicFixture(manager, plan, operation, helper, verifyContext, qualifyOrganization) {
|
|
537
|
+
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 ?? ""))
|
|
538
|
+
throw new Error("Closed one-child native fixture operation required.");
|
|
539
|
+
const read = async () => {
|
|
540
|
+
const raw2 = await manager.getSessionWhenLoaded(plan.nativeId, "topolo-dynamic-fixture", { warnOnMiss: false });
|
|
541
|
+
const dto = await manager.getSession(plan.nativeId);
|
|
542
|
+
verifyContext();
|
|
543
|
+
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.");
|
|
544
|
+
return { raw: raw2, native: {
|
|
545
|
+
nativeId: plan.nativeId,
|
|
546
|
+
busy: !!(raw2.isRunning || raw2.starting || dto.isRunning || dto.starting || manager.hasLosableWork(plan.nativeId)),
|
|
547
|
+
pendingApprovals: dto.pendingToolPermissions?.length ?? 0
|
|
548
|
+
} };
|
|
549
|
+
};
|
|
550
|
+
const { raw, native } = await read();
|
|
551
|
+
if (operation.kind === "fixture-read") {
|
|
552
|
+
if (native.busy || native.pendingApprovals) return { state: "native-fixture-pending", ...native };
|
|
553
|
+
const messages = await manager.getTranscriptForCallerElsewhere(plan.nativeId);
|
|
554
|
+
const latest = await read();
|
|
555
|
+
if (latest.native.busy || latest.native.pendingApprovals) return { state: "native-fixture-pending", ...latest.native };
|
|
556
|
+
if (!Array.isArray(messages)) throw new Error("Own native fixture transcript is unavailable.");
|
|
557
|
+
return helper("fixture-outcome", { laneId: plan.laneId, messages, native: latest.native });
|
|
558
|
+
}
|
|
559
|
+
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.");
|
|
560
|
+
await qualifyOrganization();
|
|
561
|
+
verifyContext();
|
|
562
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-fixture.v1");
|
|
563
|
+
if (manager[symbol]) throw new Error("An unresolved native fixture retains delivery custody.");
|
|
564
|
+
manager[symbol] = { laneId: plan.laneId, operationId: operation.operationId };
|
|
565
|
+
let armed = false;
|
|
566
|
+
try {
|
|
567
|
+
const fixture = await helper("fixture-prepare", { laneId: plan.laneId, operationId: operation.operationId });
|
|
568
|
+
const latest = await read();
|
|
569
|
+
if (latest.native.busy || latest.native.pendingApprovals) throw new Error("Own child became busy before fixture dispatch.");
|
|
570
|
+
await qualifyOrganization();
|
|
571
|
+
verifyContext();
|
|
572
|
+
await helper("fixture-mark", { laneId: plan.laneId, operationId: operation.operationId, state: "start-may-have-effects" });
|
|
573
|
+
armed = true;
|
|
574
|
+
const result = await manager.sendMessage(
|
|
575
|
+
plan.nativeId,
|
|
576
|
+
fixture.text,
|
|
577
|
+
void 0,
|
|
578
|
+
{ messageUuid: operation.operationId, initiator: "agent", origin: { kind: "task-notification" } }
|
|
579
|
+
);
|
|
580
|
+
const state = result?.delivered === true ? "native-delivered" : result?.delivered === false ? "native-refused" : "uncertain";
|
|
581
|
+
await helper("fixture-mark", { laneId: plan.laneId, operationId: operation.operationId, state });
|
|
582
|
+
if (state === "uncertain") throw new Error("Native fixture delivery is uncertain; no replay.");
|
|
583
|
+
delete manager[symbol];
|
|
584
|
+
return { state, laneId: plan.laneId, operationId: operation.operationId, operationEffectVerified: false };
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (!armed) delete manager[symbol];
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function nativeClaudeReadEmptyCreation(manager, planned, verifyContext) {
|
|
591
|
+
const pending = planned.creationNativeId ? { nativeId: planned.creationNativeId } : manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.native-creation.v1")]?.get(planned.laneId);
|
|
592
|
+
if (!pending?.nativeId) throw new Error("Own actual created native identity is unresolved; no replay.");
|
|
593
|
+
const raw = await manager.getSessionWhenLoaded(pending.nativeId, "topolo-dynamic-empty-process", { warnOnMiss: false });
|
|
594
|
+
const view = await manager.getSession(pending.nativeId);
|
|
595
|
+
verifyContext();
|
|
596
|
+
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.");
|
|
597
|
+
if (raw.isRunning || view.isRunning || raw.messageBuffer?.length || raw.heldAcrossLoss?.length || raw.pendingToolPermissions?.length || view.pendingToolPermissions?.length || manager.hasLosableWork(pending.nativeId))
|
|
598
|
+
throw new Error("Fresh child has held/input/active work before configuration-only ACK.");
|
|
599
|
+
if (!raw.query || raw.pendingFirstStart || raw.startResumeInFlight) return { state: "created-initializing", nativeId: pending.nativeId, requestedTaskStarted: false, privateMCPVerified: false };
|
|
600
|
+
return {
|
|
601
|
+
state: "ready",
|
|
602
|
+
nativeId: pending.nativeId,
|
|
603
|
+
cwd: raw.cwd,
|
|
604
|
+
title: raw.title,
|
|
605
|
+
model: view.model,
|
|
606
|
+
effort: view.effort,
|
|
607
|
+
nativeParentId: raw.spawnedFrom.sessionId,
|
|
608
|
+
archived: false,
|
|
609
|
+
backend: "local",
|
|
610
|
+
foreignWorkspace: false,
|
|
611
|
+
busy: false,
|
|
612
|
+
pendingApprovals: 0,
|
|
613
|
+
requestedTaskStarted: false,
|
|
614
|
+
modelTurnsStarted: false,
|
|
615
|
+
privateMCPVerified: false,
|
|
616
|
+
proof: "current-native-descriptor-only"
|
|
617
|
+
};
|
|
618
|
+
}
|
|
365
619
|
|
|
366
620
|
// src/lib/claude-pack-approvals.ts
|
|
367
621
|
async function nativeClaudeApprovalControl(manager, descriptor, token, net, fs, invoke, qualify, checkpoint) {
|
|
@@ -579,10 +833,11 @@ async function nativeClaudeOwnedSmokeOperation(manager, members, operation, help
|
|
|
579
833
|
async function nativeClaudeApprovalOperation(manager, routes, operation, fingerprint, checkpoint, history = []) {
|
|
580
834
|
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1");
|
|
581
835
|
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.");
|
|
582
|
-
const
|
|
583
|
-
const
|
|
836
|
+
const existing = manager[symbol];
|
|
837
|
+
const byId = existing?.byId ?? new Map(routes.map((r) => [r.sessionId, r]));
|
|
838
|
+
const byKey = existing?.byKey ?? new Map(routes.map((r) => [r.key, r]));
|
|
584
839
|
for (const r of routes) {
|
|
585
|
-
const intake =
|
|
840
|
+
const intake = routes.find((candidate) => candidate.key === r.intakeKey);
|
|
586
841
|
if (!/^org:.+$/.test(r.contextKey) || !intake || !intake.key.includes("/intakes/") || intake.contextKey !== r.contextKey || intake.sessionId !== r.intakeSessionId)
|
|
587
842
|
throw new Error("Approval route has no same-context owned intake.");
|
|
588
843
|
}
|
|
@@ -595,7 +850,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
595
850
|
const session = await manager.getSession(route.sessionId);
|
|
596
851
|
if (!session || session.sessionId !== route.sessionId || session.cwd !== route.cwd || session.title !== route.title || session.isArchived)
|
|
597
852
|
throw new Error("Native approval session identity differs: " + route.key);
|
|
598
|
-
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 || descriptor.spawnedFrom)
|
|
853
|
+
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 ? (route.dynamic.continuityTransport === "native-cli" ? Boolean(descriptor.spawnedFrom) || route.dynamic.nativeParentId !== null : 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))
|
|
599
854
|
throw new Error("Native approval session descriptor identity differs: " + route.key);
|
|
600
855
|
return session;
|
|
601
856
|
};
|
|
@@ -624,7 +879,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
624
879
|
for (const r of routes) await owned(r);
|
|
625
880
|
if (!state) {
|
|
626
881
|
if (typeof manager.on !== "function" || typeof manager.sendMessage !== "function" || typeof manager.respondToToolPermission !== "function") throw new Error("Native approval transport unavailable.");
|
|
627
|
-
state = { identity, routes, entries: /* @__PURE__ */ new Map(), notifications: /* @__PURE__ */ new Set(), deliveryAttempts: 0 };
|
|
882
|
+
state = { identity, routes, byId, byKey, entries: /* @__PURE__ */ new Map(), notifications: /* @__PURE__ */ new Set(), deliveryAttempts: 0 };
|
|
628
883
|
manager[symbol] = state;
|
|
629
884
|
const save = (entry2) => checkpoint({ ...entry2.record });
|
|
630
885
|
const deliver = async (entry2) => {
|
|
@@ -644,7 +899,7 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
644
899
|
save(entry2);
|
|
645
900
|
return;
|
|
646
901
|
}
|
|
647
|
-
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." : "");
|
|
902
|
+
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." : "");
|
|
648
903
|
record.notificationState = "may-have-effects";
|
|
649
904
|
save(entry2);
|
|
650
905
|
state.deliveryAttempts++;
|
|
@@ -695,6 +950,8 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
695
950
|
state.entries.set(key, entry2);
|
|
696
951
|
save(entry2);
|
|
697
952
|
}
|
|
953
|
+
const lifecycle2 = state.dynamicLifecycle?.get(sessionId);
|
|
954
|
+
if (lifecycle2 && entry2.record.state === "pending") await lifecycle2.request(entry2.record, pending2);
|
|
698
955
|
if (entry2.record.state === "pending") await deliver(entry2);
|
|
699
956
|
};
|
|
700
957
|
state.listener = async (event) => {
|
|
@@ -737,6 +994,30 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
737
994
|
if (operation.kind === "list") return { requests: [...state.entries.values()].map((e) => ({ ...e.record })) };
|
|
738
995
|
const entry = state.entries.get(operation.key);
|
|
739
996
|
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.");
|
|
997
|
+
let cliFinalGuard;
|
|
998
|
+
if (operation.kind === "respond" && operation.decision === "once" && byId.get(entry.record.sourceSessionId)?.dynamic) {
|
|
999
|
+
const route = byId.get(entry.record.sourceSessionId);
|
|
1000
|
+
if (route.dynamic?.continuityTransport === "native-cli") {
|
|
1001
|
+
const producer = state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI;
|
|
1002
|
+
if (typeof producer !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1003
|
+
const admission = await producer(entry.record, entry.request, operation);
|
|
1004
|
+
if (!admission || !["bootstrap", "work"].includes(admission.phase) || admission.nativeId !== entry.record.sourceSessionId || admission.intakeNativeId !== entry.record.intakeSessionId || admission.fingerprint !== entry.record.fingerprint || admission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || !admission.query || typeof admission.verifyCurrent !== "function" || typeof admission.model !== "string" || !admission.model || typeof admission.effort !== "string" || !admission.effort)
|
|
1005
|
+
throw new Error("Exact current native CLI query/intake admission differs.");
|
|
1006
|
+
const raw = await manager.getSessionWhenLoaded(entry.record.sourceSessionId, "topolo-native-cli-permission", { warnOnMiss: false });
|
|
1007
|
+
if (raw?.query !== admission.query || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== admission.model || await manager.getEffort(raw.sessionId) !== admission.effort)
|
|
1008
|
+
throw new Error("Actual same-query native model/effort acknowledgement differs.");
|
|
1009
|
+
cliFinalGuard = () => {
|
|
1010
|
+
if (state.dynamicLifecycle?.get(entry.record.sourceSessionId)?.authorizeNativeCLI !== producer || raw.query !== admission.query || admission.verifyCurrent() !== void 0) throw new Error("Current native CLI admission changed before dispatch.");
|
|
1011
|
+
};
|
|
1012
|
+
if (admission.phase === "bootstrap") {
|
|
1013
|
+
if (entry.request.toolName !== "Bash" || entry.request.input?.command !== admission.command || admission.commandDigest !== fingerprint(admission.command))
|
|
1014
|
+
throw new Error("Only the exact native CLI bootstrap command may precede context acknowledgement.");
|
|
1015
|
+
} else if (!/^[a-f0-9]{64}$/.test(admission.contextReceiptDigest ?? "")) throw new Error("Native CLI work requires verified same-query context receipt.");
|
|
1016
|
+
} else throw Object.assign(
|
|
1017
|
+
new Error("native_private_query_prestart_contract_unqualified: dynamic private query authorization remains closed."),
|
|
1018
|
+
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
740
1021
|
const pending = await current(entry);
|
|
741
1022
|
if (manager[symbol] !== state || state.entries.get(operation.key) !== entry || entry.record.state !== "pending" || state.auditFailed)
|
|
742
1023
|
throw new Error("Exact pending request changed during native identity validation; reconcile without replay.");
|
|
@@ -744,16 +1025,134 @@ async function nativeClaudeApprovalOperation(manager, routes, operation, fingerp
|
|
|
744
1025
|
if (operation.kind !== "respond" || operation.humanDecisionRecorded !== true || !["once", "deny"].includes(operation.decision ?? ""))
|
|
745
1026
|
throw new Error("Recorded human one-request decision required; never auto-approve.");
|
|
746
1027
|
if (operation.decision !== "deny" && entry.record.cardOnly) throw new Error("Claude requires the original card; intake cannot impersonate its origin.");
|
|
1028
|
+
cliFinalGuard?.();
|
|
747
1029
|
entry.record.state = "response-may-have-effects";
|
|
748
1030
|
checkpoint({ ...entry.record });
|
|
749
1031
|
await manager.respondToToolPermission(pending.requestId, operation.decision, void 0, "elsewhere");
|
|
750
1032
|
const source = await owned(byId.get(entry.record.sourceSessionId));
|
|
751
1033
|
if (source.pendingToolPermissions?.some((r) => r.requestId === pending.requestId))
|
|
752
1034
|
throw new Error("Native decision did not settle; reconcile without replay.");
|
|
1035
|
+
const lifecycle = state.dynamicLifecycle?.get(entry.record.sourceSessionId);
|
|
1036
|
+
if (lifecycle) await lifecycle.decision(entry.record, pending, operation.decision);
|
|
753
1037
|
entry.record.state = "native-request-settled";
|
|
754
1038
|
checkpoint({ ...entry.record });
|
|
755
1039
|
return { state: entry.record.state, nativeApproved: false, operationEffectVerified: false };
|
|
756
1040
|
}
|
|
1041
|
+
async function nativeClaudeAppendDynamicRoute(manager, route, verify, checkpoint, lifecycle) {
|
|
1042
|
+
const state = manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")];
|
|
1043
|
+
if (!state || state.auditFailed || !state.byId || !state.byKey || !route.dynamic || !/^[a-f0-9]{64}$/.test(route.dynamic.planDigest) || (route.dynamic.continuityTransport === "native-cli" ? route.dynamic.nativeParentId !== null : route.dynamic.nativeParentId !== route.intakeSessionId))
|
|
1044
|
+
throw new Error("Current qualified original approval listener required.");
|
|
1045
|
+
let cliAdmission;
|
|
1046
|
+
if (route.dynamic.continuityTransport === "native-cli") {
|
|
1047
|
+
if (typeof lifecycle?.authorizeNativeCLI !== "function") throw Object.assign(new Error("native_cli_startup_unqualified: actual native CLI admission producer unavailable."), { code: "native_cli_startup_unqualified" });
|
|
1048
|
+
cliAdmission = await lifecycle.authorizeNativeCLI(null, null, { kind: "route-append" });
|
|
1049
|
+
}
|
|
1050
|
+
const intake = state.byId.get(route.intakeSessionId);
|
|
1051
|
+
if (!intake || intake.key !== route.intakeKey || intake.contextKey !== route.contextKey || !intake.key.includes("/intakes/"))
|
|
1052
|
+
throw new Error("Dynamic approval route has no exact same-context original intake.");
|
|
1053
|
+
await verify();
|
|
1054
|
+
const raw = await manager.getSessionWhenLoaded(route.sessionId, "topolo-dynamic-route", { warnOnMiss: false });
|
|
1055
|
+
const dto = await manager.getSession(route.sessionId);
|
|
1056
|
+
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 || (route.dynamic.continuityTransport === "native-cli" ? Boolean(raw.spawnedFrom) : raw.spawnedFrom?.sessionId !== route.intakeSessionId || raw.spawnedFrom?.sideSession !== true))
|
|
1057
|
+
throw new Error("Actual dynamic child descriptor differs before route append.");
|
|
1058
|
+
if (route.dynamic.continuityTransport === "native-cli" && (!cliAdmission?.query || raw.query !== cliAdmission.query || cliAdmission.nativeId !== route.sessionId || cliAdmission.intakeNativeId !== route.intakeSessionId || cliAdmission.generation !== route.dynamic.generation || typeof route.dynamic.generation !== "string" || !route.dynamic.generation || typeof cliAdmission.verifyCurrent !== "function" || typeof cliAdmission.model !== "string" || !cliAdmission.model || typeof cliAdmission.effort !== "string" || !cliAdmission.effort || typeof manager.readRunningModel !== "function" || typeof manager.getEffort !== "function" || await manager.readRunningModel(raw, raw.query) !== cliAdmission.model || await manager.getEffort(raw.sessionId) !== cliAdmission.effort))
|
|
1059
|
+
throw new Error("Actual native CLI route query/model/effort admission differs.");
|
|
1060
|
+
await verify();
|
|
1061
|
+
if (route.dynamic.continuityTransport === "native-cli" && (raw.query !== cliAdmission.query || cliAdmission.verifyCurrent() !== void 0))
|
|
1062
|
+
throw new Error("Current native CLI route admission changed before append.");
|
|
1063
|
+
if (manager[/* @__PURE__ */ Symbol.for("topolo.agent-pack.approvals.v1")] !== state || state.auditFailed)
|
|
1064
|
+
throw new Error("Original native permission connection changed.");
|
|
1065
|
+
const old = state.byId.get(route.sessionId);
|
|
1066
|
+
if (old) {
|
|
1067
|
+
if (JSON.stringify(old) !== JSON.stringify(route)) throw new Error("Native child route conflicts with an existing owner.");
|
|
1068
|
+
return { state: "dynamic-route-current", generation: state.identity };
|
|
1069
|
+
}
|
|
1070
|
+
if (state.byKey.has(route.key)) throw new Error("Dynamic role key is already owned.");
|
|
1071
|
+
checkpoint({ state: "dynamic-route-append", route });
|
|
1072
|
+
state.byId.set(route.sessionId, route);
|
|
1073
|
+
state.byKey.set(route.key, route);
|
|
1074
|
+
state.dynamicRoutes ??= /* @__PURE__ */ new Map();
|
|
1075
|
+
state.dynamicRoutes.set(route.sessionId, route);
|
|
1076
|
+
if (lifecycle) {
|
|
1077
|
+
state.dynamicLifecycle ??= /* @__PURE__ */ new Map();
|
|
1078
|
+
state.dynamicLifecycle.set(route.sessionId, lifecycle);
|
|
1079
|
+
}
|
|
1080
|
+
return { state: "dynamic-route-appended", generation: state.identity, pendingRequestsPreserved: true };
|
|
1081
|
+
}
|
|
1082
|
+
async function nativeClaudeLaneControl(net, fs, manager, descriptor, token, invoke, qualify, checkpoint) {
|
|
1083
|
+
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)
|
|
1084
|
+
throw new Error("Exact private lane native control identity required.");
|
|
1085
|
+
const symbol = /* @__PURE__ */ Symbol.for("topolo.agent-pack.dynamic-controls.v1");
|
|
1086
|
+
manager[symbol] ??= /* @__PURE__ */ new Map();
|
|
1087
|
+
const controls = manager[symbol];
|
|
1088
|
+
if (controls.has(descriptor.laneId) || fs.existsSync(descriptor.socket)) throw new Error("Own native lane control already exists; no takeover.");
|
|
1089
|
+
qualify();
|
|
1090
|
+
const control = { active: 0, descriptor, server: null, closed: false };
|
|
1091
|
+
const allowed = /* @__PURE__ */ new Set(["read", "context-start", "context-read", "fixture-start", "fixture-read", "retire", "approval-list", "approval-details", "approval-respond", "close"]);
|
|
1092
|
+
const server = net.createServer((connection) => {
|
|
1093
|
+
let text = "";
|
|
1094
|
+
let dispatched = false;
|
|
1095
|
+
connection.setEncoding("utf8");
|
|
1096
|
+
connection.setTimeout(24e4, () => connection.destroy());
|
|
1097
|
+
connection.on("error", () => connection.destroy());
|
|
1098
|
+
connection.on("data", async (chunk) => {
|
|
1099
|
+
if (dispatched) return;
|
|
1100
|
+
text += chunk;
|
|
1101
|
+
if (Buffer.byteLength(text) > 65536) {
|
|
1102
|
+
dispatched = true;
|
|
1103
|
+
connection.destroy();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
if (!text.includes("\n")) return;
|
|
1107
|
+
dispatched = true;
|
|
1108
|
+
control.active++;
|
|
1109
|
+
try {
|
|
1110
|
+
qualify();
|
|
1111
|
+
const request = JSON.parse(text);
|
|
1112
|
+
if (Object.keys(request).sort().join() !== "identity,operation,pid,token" || request.token !== token || request.pid !== descriptor.pid || request.identity !== descriptor.identity)
|
|
1113
|
+
throw new Error("Private native lane control authentication differs.");
|
|
1114
|
+
const operation = request.operation;
|
|
1115
|
+
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"];
|
|
1116
|
+
if (!operation || !allowed.has(operation.kind) || Object.keys(operation).some((k) => !fields.includes(k))) throw new Error("Closed private lane operation required.");
|
|
1117
|
+
if (operation.kind === "close" && control.active !== 1) throw new Error("Own native control has other in-flight operations.");
|
|
1118
|
+
const result = await invoke(operation);
|
|
1119
|
+
const encoded = JSON.stringify({ result }) + "\n";
|
|
1120
|
+
if (Buffer.byteLength(encoded) > 1048576) throw new Error("Bounded private native lane reply exceeded.");
|
|
1121
|
+
connection.end(encoded);
|
|
1122
|
+
if (operation.kind === "close") connection.once("close", () => server.close(() => {
|
|
1123
|
+
control.closed = true;
|
|
1124
|
+
controls.delete(descriptor.laneId);
|
|
1125
|
+
checkpoint({ state: "normal-own-lane-control-closed", laneId: descriptor.laneId });
|
|
1126
|
+
if (fs.existsSync(descriptor.socket)) {
|
|
1127
|
+
const current = fs.lstatSync(descriptor.socket);
|
|
1128
|
+
if (current.ino === control.inode && current.uid === control.uid) fs.unlinkSync(descriptor.socket);
|
|
1129
|
+
}
|
|
1130
|
+
}));
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
connection.end(JSON.stringify({ error: error instanceof Error ? error.message : "Native lane control refused." }) + "\n");
|
|
1133
|
+
} finally {
|
|
1134
|
+
control.active--;
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
control.server = server;
|
|
1139
|
+
await new Promise((done, fail) => {
|
|
1140
|
+
server.once("error", fail);
|
|
1141
|
+
server.listen(descriptor.socket, done);
|
|
1142
|
+
});
|
|
1143
|
+
try {
|
|
1144
|
+
fs.chmodSync(descriptor.socket, 384);
|
|
1145
|
+
const stat = fs.lstatSync(descriptor.socket);
|
|
1146
|
+
control.inode = stat.ino;
|
|
1147
|
+
control.uid = stat.uid;
|
|
1148
|
+
checkpoint({ state: "private-native-lane-control-ready", ...descriptor });
|
|
1149
|
+
controls.set(descriptor.laneId, control);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
await new Promise((done) => server.close(() => done()));
|
|
1152
|
+
throw error;
|
|
1153
|
+
}
|
|
1154
|
+
return { state: "private-native-lane-control-ready", ...descriptor };
|
|
1155
|
+
}
|
|
757
1156
|
|
|
758
1157
|
// src/lib/claude-pack-backend.ts
|
|
759
1158
|
var CLAUDE_NATIVE_QUALIFIER = {
|
|
@@ -819,8 +1218,8 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
819
1218
|
captureMarkdown(cwd);
|
|
820
1219
|
const charter = resolve(cwd, "CLAUDE.md");
|
|
821
1220
|
const transcript = realpathSync(record.transcript);
|
|
822
|
-
context[charter] =
|
|
823
|
-
context[transcript] =
|
|
1221
|
+
context[charter] = createHash2("sha256").update(readFileSync(charter)).digest("hex");
|
|
1222
|
+
context[transcript] = createHash2("sha256").update(readFileSync(transcript)).digest("hex");
|
|
824
1223
|
roleContextFiles[record.desktopSessionId] = [charter, transcript];
|
|
825
1224
|
members.push({
|
|
826
1225
|
key: record.key,
|
|
@@ -838,7 +1237,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
838
1237
|
});
|
|
839
1238
|
}
|
|
840
1239
|
for (const file of [resolve(root, "claude/state.json"), realpathSync(policyPath)])
|
|
841
|
-
context[file] =
|
|
1240
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
842
1241
|
const activePath = resolve(root, "claude/active.json");
|
|
843
1242
|
const active = existsSync(activePath) ? JSON.parse(readFileSync(activePath, "utf8")) : null;
|
|
844
1243
|
const target = new Set(members.map((m) => m.key));
|
|
@@ -855,7 +1254,7 @@ function prepareClaudeNativePlan(store, pack, policyPath, mode = "configure") {
|
|
|
855
1254
|
if (!cwd.startsWith(resolve(root, "claude/workspaces", record.pack) + sep)) throw new Error("Outgoing workspace is outside the owned pack.");
|
|
856
1255
|
captureMarkdown(cwd);
|
|
857
1256
|
for (const file of [resolve(cwd, "CLAUDE.md"), realpathSync(record.transcript)])
|
|
858
|
-
context[file] =
|
|
1257
|
+
context[file] = createHash2("sha256").update(readFileSync(file)).digest("hex");
|
|
859
1258
|
outgoing.push({ key, sessionId: record.desktopSessionId, cwd, title: record.title });
|
|
860
1259
|
}
|
|
861
1260
|
} else if (mode === "switch") throw new Error("Configure and verify an initial active roster before switching.");
|
|
@@ -1159,7 +1558,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1159
1558
|
if (!journal.startsWith(realpathSync(directory) + sep)) throw new Error("Recovery journal is outside the owned library.");
|
|
1160
1559
|
const lines = readFileSync(journal, "utf8").split("\n").filter(Boolean);
|
|
1161
1560
|
const line = lines[0];
|
|
1162
|
-
if (
|
|
1561
|
+
if (createHash2("sha256").update(line).digest("hex") !== marker.planHash) throw new Error("Recovery plan identity changed.");
|
|
1163
1562
|
const record = JSON.parse(line);
|
|
1164
1563
|
if (record.pack !== options.pack || record.operationId !== marker.operationId || !record.apply) throw new Error("Recovery pack or operation identity differs.");
|
|
1165
1564
|
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") };
|
|
@@ -1173,7 +1572,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1173
1572
|
throw new Error("Unowned native approval connection.");
|
|
1174
1573
|
const connection = JSON.parse(readFileSync(connectionPath, "utf8"));
|
|
1175
1574
|
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)) {
|
|
1176
|
-
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-" +
|
|
1575
|
+
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-" + createHash2("sha256").update(root).digest("hex").slice(0, 16)))
|
|
1177
1576
|
throw new Error("Native approval connection is outside owned control paths.");
|
|
1178
1577
|
const secret = lstatSync(connection.secretPath);
|
|
1179
1578
|
if (!secret.isFile() || secret.isSymbolicLink() || typeof process.getuid !== "function" || secret.uid !== process.getuid() || secret.mode & 63)
|
|
@@ -1184,7 +1583,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1184
1583
|
if (!plan.control) {
|
|
1185
1584
|
const directory2 = resolve(root, "claude/private");
|
|
1186
1585
|
mkdirSync(directory2, { recursive: true, mode: 448 });
|
|
1187
|
-
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" +
|
|
1586
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-pack-" + createHash2("sha256").update(root).digest("hex").slice(0, 16));
|
|
1188
1587
|
mkdirSync(socketDirectory, { recursive: true, mode: 448 });
|
|
1189
1588
|
for (const path of [directory2, socketDirectory]) {
|
|
1190
1589
|
const stat = lstatSync(path);
|
|
@@ -1199,7 +1598,7 @@ async function configureClaudePack(options, evaluate = evaluateClaudeMain, quali
|
|
|
1199
1598
|
journal = resolve(directory, operationId + ".jsonl");
|
|
1200
1599
|
const record = { kind: "plan", pack: options.pack, ...plan, apply: options.apply };
|
|
1201
1600
|
durableRecord(journal, record, "wx");
|
|
1202
|
-
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash:
|
|
1601
|
+
marker = { format: 2, dispatchState: "not-dispatched", operationId, journal, planHash: createHash2("sha256").update(JSON.stringify(record)).digest("hex") };
|
|
1203
1602
|
if (options.apply) durableRecord(markerPath, marker, "wx");
|
|
1204
1603
|
syncDirectory(directory);
|
|
1205
1604
|
syncDirectory(resolve(root, "claude"));
|
|
@@ -1396,7 +1795,7 @@ function requestClaudeApprovalControl(connection, operation, token, connect = cr
|
|
|
1396
1795
|
error ? fail(error) : done(result);
|
|
1397
1796
|
};
|
|
1398
1797
|
client.setEncoding("utf8");
|
|
1399
|
-
client.setTimeout(operation.kind === "smoke-start" ? 6e4 : 1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
1798
|
+
client.setTimeout(connection.laneId ? 24e4 : operation.kind === "smoke-start" ? 6e4 : 1e4, () => finish(new Error("Native approval response timed out; reconcile without replay.")));
|
|
1400
1799
|
client.on("connect", () => client.write(JSON.stringify({ token, pid: connection.pid, identity: connection.identity, operation }) + "\n"));
|
|
1401
1800
|
client.on("data", (chunk) => {
|
|
1402
1801
|
reply += chunk;
|
|
@@ -1454,16 +1853,570 @@ async function operateClaudeSmoke(options, request = requestClaudeApprovalContro
|
|
|
1454
1853
|
throw new Error("Exact closed native smoke fields required.");
|
|
1455
1854
|
return operateClaudeApprovals(options, request);
|
|
1456
1855
|
}
|
|
1856
|
+
function prepareClaudeDynamicLane(store, pack, policyPath, laneId) {
|
|
1857
|
+
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.");
|
|
1858
|
+
const root = realpathSync(store);
|
|
1859
|
+
const file = resolve(root, "dynamic-lanes/claude", laneId + ".json");
|
|
1860
|
+
const owned = (path) => {
|
|
1861
|
+
const info = lstatSync(path);
|
|
1862
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.mode & 18)
|
|
1863
|
+
throw new Error("Dynamic lane requires an owned regular context record.");
|
|
1864
|
+
return path;
|
|
1865
|
+
};
|
|
1866
|
+
owned(file);
|
|
1867
|
+
if (lstatSync(file).size > 131072) throw new Error("Dynamic lane record exceeds its schema bound.");
|
|
1868
|
+
const record = JSON.parse(readFileSync(file, "utf8"));
|
|
1869
|
+
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);
|
|
1870
|
+
const digest = (value) => createHash2("sha256").update(canonical(value)).digest("hex");
|
|
1871
|
+
const nativeCLI = record.schemaVersion === 2 && record.plan?.schemaVersion === 2 && record.plan.continuityTransport === "native-cli";
|
|
1872
|
+
if (record.format !== "topolo-dynamic-lane" || !(record.schemaVersion === 1 || nativeCLI) || 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 || (nativeCLI ? record.plan.nativeParentId !== null : record.plan.nativeParentId !== record.plan.intake.nativeId))
|
|
1873
|
+
throw new Error("Durable dynamic child identity or plan digest differs.");
|
|
1874
|
+
const stateFile = owned(resolve(root, "claude/state.json"));
|
|
1875
|
+
const state = JSON.parse(readFileSync(stateFile, "utf8"));
|
|
1876
|
+
const policyFile = owned(realpathSync(policyPath));
|
|
1877
|
+
const policy = JSON.parse(readFileSync(policyFile, "utf8"));
|
|
1878
|
+
if (state.harness !== "claude" || policy.format !== "topolo-agent-execution-policy" || policy.pack !== pack || policy.sourceRevision !== record.plan.role.packRevision || !policy.installationConsent)
|
|
1879
|
+
throw new Error("Dynamic child source policy or organization-bound installation is stale.");
|
|
1880
|
+
const context = {
|
|
1881
|
+
[stateFile]: createHash2("sha256").update(readFileSync(stateFile)).digest("hex"),
|
|
1882
|
+
[policyFile]: createHash2("sha256").update(readFileSync(policyFile)).digest("hex")
|
|
1883
|
+
};
|
|
1884
|
+
const inventories = {};
|
|
1885
|
+
const fixed = (descriptor) => {
|
|
1886
|
+
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");
|
|
1887
|
+
const settings = policy.agents.filter((r) => r.key === descriptor.key);
|
|
1888
|
+
if (rows.length !== 1 || settings.length !== 1) throw new Error("Dynamic source has no unique current adopted role.");
|
|
1889
|
+
const row = rows[0];
|
|
1890
|
+
const cwd = realpathSync(row.workspace);
|
|
1891
|
+
if (cwd !== descriptor.cwd || !cwd.startsWith(resolve(root, "claude/workspaces", pack) + sep) || canonical(settings[0].harnesses.claude.nativeSettings) !== canonical(descriptor.settings))
|
|
1892
|
+
throw new Error("Dynamic source workspace or current fixed settings differ.");
|
|
1893
|
+
const files = [resolve(cwd, "CLAUDE.md"), resolve(cwd, "topolo-connection.json"), resolve(cwd, ".mcp.json")];
|
|
1894
|
+
const hashes = {};
|
|
1895
|
+
for (const f of files) hashes[owned(f)] = createHash2("sha256").update(readFileSync(f)).digest("hex");
|
|
1896
|
+
const connection = JSON.parse(readFileSync(files[1], "utf8"));
|
|
1897
|
+
const expected = {
|
|
1898
|
+
environment: descriptor.environment,
|
|
1899
|
+
contextKey: descriptor.contextKey,
|
|
1900
|
+
pack,
|
|
1901
|
+
packRevision: descriptor.packRevision,
|
|
1902
|
+
nativeThreadId: descriptor.nativeId,
|
|
1903
|
+
agentId: descriptor.AgentLogicalId,
|
|
1904
|
+
agentVersionId: descriptor.AgentVersionId,
|
|
1905
|
+
agentWorkspaceId: descriptor.AgentWorkspaceId,
|
|
1906
|
+
bytesWorkspaceId: descriptor.BytesWorkspaceId,
|
|
1907
|
+
instanceId: descriptor.installationId
|
|
1908
|
+
};
|
|
1909
|
+
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))
|
|
1910
|
+
throw new Error("Current hosted Agent, Bytes, private MCP or loaded-source context descriptor differs.");
|
|
1911
|
+
Object.assign(context, hashes);
|
|
1912
|
+
inventories[cwd] = snapshotClaudeMarkdown(cwd, filesystem, cryptography, paths);
|
|
1913
|
+
return { key: row.key, sessionId: row.desktopSessionId, cwd, title: row.title };
|
|
1914
|
+
};
|
|
1915
|
+
const source = fixed(record.plan.role);
|
|
1916
|
+
const intake = fixed(record.plan.intake);
|
|
1917
|
+
const consent = policy.installationConsent;
|
|
1918
|
+
if (consent.contextKey !== record.plan.role.contextKey || consent.environment !== record.plan.role.environment || consent.intakeRoutes?.[source.key] !== intake.key || record.creationReceipt.cwd !== source.cwd)
|
|
1919
|
+
throw new Error("Dynamic child original intake or installation scope differs.");
|
|
1920
|
+
return {
|
|
1921
|
+
root,
|
|
1922
|
+
pack,
|
|
1923
|
+
record,
|
|
1924
|
+
context,
|
|
1925
|
+
contextInventories: inventories,
|
|
1926
|
+
consent,
|
|
1927
|
+
nativePlan: {
|
|
1928
|
+
laneId,
|
|
1929
|
+
planDigest: record.planDigest,
|
|
1930
|
+
nativeId: record.nativeId,
|
|
1931
|
+
cwd: record.creationReceipt.cwd,
|
|
1932
|
+
...nativeCLI ? { continuityTransport: "native-cli", continuity: record.plan.continuity } : {},
|
|
1933
|
+
title: record.nativeTitle,
|
|
1934
|
+
source,
|
|
1935
|
+
intake,
|
|
1936
|
+
lifecycleState: record.state,
|
|
1937
|
+
...record.plan.resolution.resolved,
|
|
1938
|
+
permissionMode: record.plan.verificationProfile === "manual" ? "default" : record.plan.role.settings.permissions.defaultMode,
|
|
1939
|
+
contextDigest: record.plan.role.contextDigest,
|
|
1940
|
+
deadline: Date.now() + 45e3
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
function privateClaudeHelperEnvironment(connection, fs, crypto, path, hostEnv) {
|
|
1945
|
+
const fields = ["format", "schemaVersion", "root", "pack", "sourceRoleKey", "policyFile", "bindingFile", "policyDigest", "bindingDigest", "stateDigest", "profile"];
|
|
1946
|
+
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)
|
|
1947
|
+
throw new Error("Explicit approved private helper connection required before client startup.");
|
|
1948
|
+
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);
|
|
1949
|
+
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
1950
|
+
const owned = (file) => {
|
|
1951
|
+
const info = fs.lstatSync(file);
|
|
1952
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.nlink !== 1 || info.mode & 18)
|
|
1953
|
+
throw new Error("Private helper metadata requires owned regular records.");
|
|
1954
|
+
};
|
|
1955
|
+
const read = (file, expected) => {
|
|
1956
|
+
owned(file);
|
|
1957
|
+
const bytes = fs.readFileSync(file);
|
|
1958
|
+
if (hash(bytes) !== expected) throw new Error("Private helper connection metadata changed before client startup.");
|
|
1959
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
1960
|
+
};
|
|
1961
|
+
const root = fs.realpathSync(connection.root);
|
|
1962
|
+
if (root !== connection.root) throw new Error("Private helper library identity changed.");
|
|
1963
|
+
const policy = read(connection.policyFile, connection.policyDigest), consent = policy.installationConsent;
|
|
1964
|
+
const state = read(path.resolve(root, "claude/state.json"), connection.stateDigest);
|
|
1965
|
+
const binding = read(connection.bindingFile, connection.bindingDigest), profile = connection.profile;
|
|
1966
|
+
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)
|
|
1967
|
+
throw new Error("Exact per-environment private profile identity reference required.");
|
|
1968
|
+
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)
|
|
1969
|
+
throw new Error("Private helper profile differs from the actual pack installation and policy.");
|
|
1970
|
+
const intakeKey = consent.intakeRoutes?.[connection.sourceRoleKey];
|
|
1971
|
+
if (typeof intakeKey !== "string" || !intakeKey.includes("/intakes/")) throw new Error("Private helper original intake is missing.");
|
|
1972
|
+
for (const key of [connection.sourceRoleKey, intakeKey]) {
|
|
1973
|
+
const logical = key.split("/").slice(1).join("/");
|
|
1974
|
+
const rows = state.agents?.filter((row) => row.key === key && row.pack === connection.pack && row.sourceRevision === binding.packRevision && row.importState === "desktop-adopted");
|
|
1975
|
+
const roles = binding.input.roles?.filter((row) => row.logicalId === logical);
|
|
1976
|
+
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)
|
|
1977
|
+
throw new Error("Private helper source or original intake is outside the current bound pack.");
|
|
1978
|
+
const cwd = fs.realpathSync(rows[0].workspace);
|
|
1979
|
+
if (!cwd.startsWith(path.resolve(root, "claude/workspaces", connection.pack) + path.sep)) throw new Error("Private helper role workspace is outside its pack.");
|
|
1980
|
+
const descriptorFile = path.resolve(cwd, "topolo-connection.json");
|
|
1981
|
+
owned(descriptorFile);
|
|
1982
|
+
const descriptor = JSON.parse(fs.readFileSync(descriptorFile, "utf8"));
|
|
1983
|
+
const expected = {
|
|
1984
|
+
harness: "claude",
|
|
1985
|
+
pack: connection.pack,
|
|
1986
|
+
packRevision: binding.packRevision,
|
|
1987
|
+
role: logical,
|
|
1988
|
+
nativeThreadId: rows[0].desktopSessionId,
|
|
1989
|
+
environment: profile.environment,
|
|
1990
|
+
contextKey: profile.contextKey,
|
|
1991
|
+
instanceId: binding.input.instanceId,
|
|
1992
|
+
agentId: roles[0].agentId,
|
|
1993
|
+
agentVersionId: roles[0].agentVersionId,
|
|
1994
|
+
agentWorkspaceId: binding.agentWorkspaceId,
|
|
1995
|
+
bytesWorkspaceId: binding.input.bytesWorkspaceId
|
|
1996
|
+
};
|
|
1997
|
+
if (Object.entries(expected).some(([name, value]) => descriptor[name] !== value) || canonical(descriptor.identity) !== canonical(profile.identity))
|
|
1998
|
+
throw new Error("Private helper role descriptor does not match the actual public connection binding.");
|
|
1999
|
+
}
|
|
2000
|
+
if (typeof profile.configPath !== "string" || !path.isAbsolute(profile.configPath) || fs.realpathSync(profile.configPath) !== profile.configPath)
|
|
2001
|
+
throw new Error("Explicit protected profile path must be absolute and must not follow links.");
|
|
2002
|
+
const leaf = fs.lstatSync(profile.configPath), parent = fs.lstatSync(path.dirname(profile.configPath));
|
|
2003
|
+
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)
|
|
2004
|
+
throw new Error("Private helper profile requires owned0600 single-link storage in an owned0700 directory.");
|
|
2005
|
+
const env = {};
|
|
2006
|
+
for (const key of ["HOME", "USER", "LOGNAME", "PATH", "LANG", "LC_ALL", "TMPDIR"]) if (typeof hostEnv[key] === "string") env[key] = hostEnv[key];
|
|
2007
|
+
return { ...env, TOPOLO_CONFIG_PATH: profile.configPath, TOPOLO_ENV: profile.environment, TOPOLO_CONTEXT: profile.contextKey };
|
|
2008
|
+
}
|
|
2009
|
+
function prepareClaudePrivateHelperConnection(store, pack, policyPath, sourceRoleKey) {
|
|
2010
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(pack) || typeof sourceRoleKey !== "string" || sourceRoleKey.split("/").length !== 3)
|
|
2011
|
+
throw new Error("Exact existing pack source role required for a private helper connection.");
|
|
2012
|
+
const root = realpathSync(store), policyFile = realpathSync(policyPath);
|
|
2013
|
+
const policyBytes = readFileSync(policyFile), policy = JSON.parse(policyBytes.toString("utf8")), consent = policy.installationConsent;
|
|
2014
|
+
if (!consent || !["development", "staging", "production"].includes(consent.environment) || typeof consent.contextKey !== "string")
|
|
2015
|
+
throw new Error("Existing approved per-environment private connection required.");
|
|
2016
|
+
const bindingFile = resolve(root, "connections", pack, consent.environment, createHash2("sha256").update(consent.contextKey).digest("hex") + ".json");
|
|
2017
|
+
const bindingBytes = readFileSync(bindingFile), binding = JSON.parse(bindingBytes.toString("utf8"));
|
|
2018
|
+
const connection = {
|
|
2019
|
+
format: "topolo-private-helper-connection",
|
|
2020
|
+
schemaVersion: 1,
|
|
2021
|
+
root,
|
|
2022
|
+
pack,
|
|
2023
|
+
sourceRoleKey,
|
|
2024
|
+
policyFile,
|
|
2025
|
+
bindingFile,
|
|
2026
|
+
policyDigest: createHash2("sha256").update(policyBytes).digest("hex"),
|
|
2027
|
+
bindingDigest: createHash2("sha256").update(bindingBytes).digest("hex"),
|
|
2028
|
+
stateDigest: createHash2("sha256").update(readFileSync(resolve(root, "claude/state.json"))).digest("hex"),
|
|
2029
|
+
profile: binding.privateCredentialProfile
|
|
2030
|
+
};
|
|
2031
|
+
privateClaudeHelperEnvironment(connection, filesystem, cryptography, paths, process.env);
|
|
2032
|
+
return connection;
|
|
2033
|
+
}
|
|
2034
|
+
function runClaudeLaneHelper(root, pack, policyPath, action, payload, source, connection, launch = spawn, runtime = { fs: filesystem, crypto: cryptography, path: paths, env: process.env }, qualifyEnvironment = privateClaudeHelperEnvironment) {
|
|
2035
|
+
const actions = [
|
|
2036
|
+
"peek",
|
|
2037
|
+
"plan",
|
|
2038
|
+
"creation-begin",
|
|
2039
|
+
"creation-record",
|
|
2040
|
+
"creation-read",
|
|
2041
|
+
"adopt",
|
|
2042
|
+
"configure-begin",
|
|
2043
|
+
"configure-ack",
|
|
2044
|
+
"context-prepare",
|
|
2045
|
+
"context-mark",
|
|
2046
|
+
"context-ack",
|
|
2047
|
+
"fixture-prepare",
|
|
2048
|
+
"fixture-mark",
|
|
2049
|
+
"fixture-outcome",
|
|
2050
|
+
"permission-request",
|
|
2051
|
+
"permission-decision",
|
|
2052
|
+
"retire-begin",
|
|
2053
|
+
"retire-ack",
|
|
2054
|
+
"read"
|
|
2055
|
+
];
|
|
2056
|
+
if (!actions.includes(action) || !/^[a-z0-9][a-z0-9-]*$/.test(pack) || !payload || typeof payload !== "object" || Array.isArray(payload))
|
|
2057
|
+
return Promise.reject(new Error("Closed typed native lifecycle helper required."));
|
|
2058
|
+
const encoded = JSON.stringify(payload);
|
|
2059
|
+
if (Buffer.byteLength(encoded) > 2097152) return Promise.reject(new Error("Own-child native transcript exceeds bounded transport; no broad history read."));
|
|
2060
|
+
try {
|
|
2061
|
+
requireClaudePrivateQueryEffectQualified("helper", payload);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
return Promise.reject(error);
|
|
2064
|
+
}
|
|
2065
|
+
const selectedRole = payload.sourceRoleKey ?? payload.request?.sourceRoleKey ?? payload.plan?.role?.key ?? payload.operation?.record?.plan?.role?.key;
|
|
2066
|
+
if (!connection || root !== connection.root || pack !== connection.pack || policyPath !== connection.policyFile || selectedRole !== void 0 && selectedRole !== connection.sourceRoleKey)
|
|
2067
|
+
return Promise.reject(new Error("Private helper arguments differ from the exact bound source connection."));
|
|
2068
|
+
let childEnv;
|
|
2069
|
+
try {
|
|
2070
|
+
childEnv = qualifyEnvironment(connection, runtime.fs, runtime.crypto, runtime.path, runtime.env);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
return Promise.reject(error);
|
|
2073
|
+
}
|
|
2074
|
+
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)))";
|
|
2075
|
+
return new Promise((done, fail) => {
|
|
2076
|
+
const child = launch("python3", ["-I", "-B", "-c", script, root, pack, policyPath, action, source], { stdio: ["pipe", "pipe", "pipe"], env: childEnv });
|
|
2077
|
+
let output = "";
|
|
2078
|
+
let settled = false;
|
|
2079
|
+
const finish = (error, value) => {
|
|
2080
|
+
if (settled) return;
|
|
2081
|
+
settled = true;
|
|
2082
|
+
clearTimeout(timer);
|
|
2083
|
+
error ? fail(error) : done(value);
|
|
2084
|
+
};
|
|
2085
|
+
const timer = setTimeout(() => {
|
|
2086
|
+
child.kill("SIGTERM");
|
|
2087
|
+
finish(new Error("Owned lifecycle helper timed out; preserve possible-effect custody."));
|
|
2088
|
+
}, 2e5);
|
|
2089
|
+
child.stdout.setEncoding("utf8");
|
|
2090
|
+
child.stderr.resume();
|
|
2091
|
+
child.stdout.on("data", (data) => {
|
|
2092
|
+
output += data;
|
|
2093
|
+
if (Buffer.byteLength(output) > 1048576) {
|
|
2094
|
+
child.kill("SIGTERM");
|
|
2095
|
+
finish(new Error("Bounded native lifecycle reply exceeded."));
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
child.on("error", () => finish(new Error("Bundled lifecycle helper unavailable.")));
|
|
2099
|
+
child.on("close", (code) => {
|
|
2100
|
+
if (settled) return;
|
|
2101
|
+
if (code !== 0) return finish(new Error("Current context/policy/native lifecycle helper refused; no fallback or replay."));
|
|
2102
|
+
try {
|
|
2103
|
+
finish(void 0, JSON.parse(output));
|
|
2104
|
+
} catch {
|
|
2105
|
+
finish(new Error("Typed lifecycle helper returned invalid JSON."));
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
child.stdin.on("error", () => finish(new Error("Own lifecycle helper input closed; reconcile original effect.")));
|
|
2109
|
+
child.stdin.end(encoded);
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
function requireClaudePrivateQueryEffectQualified(operation, input) {
|
|
2113
|
+
const candidates = [input, input?.request, input?.plan, input?.record?.plan, input?.operation?.record?.plan];
|
|
2114
|
+
const transports = candidates.map((value) => value?.continuityTransport).filter((value) => value !== void 0);
|
|
2115
|
+
if (transports.some((value) => !["native-cli", "private-mcp"].includes(value)) || new Set(transports).size > 1)
|
|
2116
|
+
throw new Error("Closed explicit continuity transport required; no fallback.");
|
|
2117
|
+
const transport = transports[0];
|
|
2118
|
+
if (transport === "native-cli" || candidates.some((value) => value?.schemaVersion === 2))
|
|
2119
|
+
throw Object.assign(new Error("native_cli_startup_unqualified: current startup, profile refresh authority and native result producers are unavailable."), { code: "native_cli_startup_unqualified" });
|
|
2120
|
+
if (["create", "configure", "context-start", "fixture-start", "retire", "approval-once"].includes(operation))
|
|
2121
|
+
throw Object.assign(
|
|
2122
|
+
new Error("native_private_query_prestart_contract_unqualified: private native query startup, exclusion and owned lifecycle are not qualified."),
|
|
2123
|
+
{ code: "native_private_query_prestart_contract_unqualified" }
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
function claudeLaneExpression(options, source, journal, prepared) {
|
|
2127
|
+
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))
|
|
2128
|
+
throw new Error("Closed typed Claude lane operation required.");
|
|
2129
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2130
|
+
if (!options.privateConnection) throw new Error("Explicit approved private helper connection required before native access.");
|
|
2131
|
+
return `(async()=>{
|
|
2132
|
+
const expected=${JSON.stringify({ ...options, qualifier: CLAUDE_NATIVE_QUALIFIER, source, journal })};
|
|
2133
|
+
const requireClaudePrivateQueryEffectQualified=(${requireClaudePrivateQueryEffectQualified.toString()});
|
|
2134
|
+
requireClaudePrivateQueryEffectQualified(expected.operation,expected.input);
|
|
2135
|
+
if(process.pid!==expected.pid || typeof process.getBuiltinModule!=='function')throw new Error('Native process identity changed.');
|
|
2136
|
+
const load=process.getBuiltinModule('module').createRequire(process.resourcesPath+'/app.asar/package.json');
|
|
2137
|
+
const fs=load('node:fs'),crypto=load('node:crypto'),path=load('node:path');
|
|
2138
|
+
const base=process.resourcesPath+'/app.asar/.vite/build/';
|
|
2139
|
+
const qualify=()=>{
|
|
2140
|
+
if(process.pid!==expected.pid || load('electron').app.getVersion()!==expected.qualifier.version)throw new Error('Native version/PID changed.');
|
|
2141
|
+
for(const [file,hash] of Object.entries(expected.qualifier.hashes))
|
|
2142
|
+
if(crypto.createHash('sha256').update(fs.readFileSync(base+file)).digest('hex')!==hash)throw new Error('Native module changed.');
|
|
2143
|
+
};
|
|
2144
|
+
qualify();const manager=load(base+expected.qualifier.manager).claudeCodeSessionManager;
|
|
2145
|
+
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);
|
|
2146
|
+
const fingerprint=value=>crypto.createHash('sha256').update(canonical(value)).digest('hex');
|
|
2147
|
+
const generation=fingerprint({pid:expected.pid,qualifier:expected.qualifier,root:expected.root,pack:expected.pack});
|
|
2148
|
+
const models=()=>{
|
|
2149
|
+
qualify();const rows=manager.getAvailableCodeModels();
|
|
2150
|
+
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.');
|
|
2151
|
+
return {harness:'claude',source:'native-account-model-ids',connectionGeneration:generation,models:[...rows].sort()};
|
|
2152
|
+
};
|
|
2153
|
+
const privateClaudeHelperEnvironment=(${privateClaudeHelperEnvironment.toString()});
|
|
2154
|
+
const helper=(action,payload)=>(${runClaudeLaneHelper.toString()})(expected.root,expected.pack,expected.policy,action,
|
|
2155
|
+
{...payload,...(action==='context-mark'?{historyCount:payload.historyCount??null,historyDigest:payload.historyDigest??null}:{}),models:models()},
|
|
2156
|
+
expected.source,expected.privateConnection,load('node:child_process').spawn,{fs,crypto,path,env:process.env},privateClaudeHelperEnvironment);
|
|
2157
|
+
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);}};
|
|
2158
|
+
const fixed=async role=>{
|
|
2159
|
+
const raw=await manager.getSessionWhenLoaded(role.nativeId,'topolo-dynamic-source',{warnOnMiss:false});
|
|
2160
|
+
const view=await manager.getSession(role.nativeId);qualify();
|
|
2161
|
+
if(!raw||!view||raw.sessionId!==role.nativeId||view.sessionId!==role.nativeId||raw.cwd!==role.cwd||view.cwd!==role.cwd||
|
|
2162
|
+
raw.isArchived||view.isArchived||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.spawnedFrom||raw.worktreePath||raw.worktreeLazy)
|
|
2163
|
+
throw new Error('Exact source/intake actual native descriptor unavailable.');
|
|
2164
|
+
return {raw,view};
|
|
2165
|
+
};
|
|
2166
|
+
if(expected.operation==='plan'){
|
|
2167
|
+
if(Object.keys(expected.input).join()!=='request')throw new Error('Closed intake lane request required.');
|
|
2168
|
+
const current=await helper('peek',{sourceRoleKey:expected.input.request.sourceRoleKey});
|
|
2169
|
+
await fixed(current.role);await fixed(current.intake);
|
|
2170
|
+
const rows=await manager.getAllSessions();qualify();
|
|
2171
|
+
if(!Array.isArray(rows))throw new Error('Actual native ID-only creation inventory unavailable.');
|
|
2172
|
+
return helper('plan',{request:expected.input.request,nativeIds:rows.map(r=>r.sessionId)});
|
|
2173
|
+
}
|
|
2174
|
+
if(expected.operation==='create-read'){
|
|
2175
|
+
if(Object.keys(expected.input).join()!=='laneId')throw new Error('Exact immutable owned creation lane required.');
|
|
2176
|
+
const laneId=expected.input.laneId;
|
|
2177
|
+
const pending=await helper('creation-read',{laneId});
|
|
2178
|
+
const p=pending.plan;
|
|
2179
|
+
await fixed(p.role);await fixed(p.intake);
|
|
2180
|
+
const current=async()=>{const proof=await helper('creation-read',{laneId});
|
|
2181
|
+
if(canonical(proof.plan)!==canonical(p))throw new Error('Current immutable creation plan differs.');};
|
|
2182
|
+
if(!pending.creation.nativeId)throw new Error('Own native creation identity is unresolved; no replay.');
|
|
2183
|
+
await current();
|
|
2184
|
+
const native=await (${nativeClaudeReadEmptyCreation.toString()})(manager,{...p,creationNativeId:pending.creation.nativeId},qualify);
|
|
2185
|
+
if(native.state!=='ready')return {...native,laneId};
|
|
2186
|
+
const receipt={format:'topolo-native-lane-created',schemaVersion:1,state:'ready',planDigest:fingerprint(p),
|
|
2187
|
+
harness:'claude',nativeId:native.nativeId,logicalOriginThreadId:p.logicalOriginThreadId,cwd:native.cwd,
|
|
2188
|
+
model:native.model,effort:native.effort,nativeParentId:native.nativeParentId,fresh:true,creationSource:'supported-native-host-tool'};
|
|
2189
|
+
const record=await helper('adopt',{plan:p,receipt,native});
|
|
2190
|
+
return {state:'created-adopted-unconfigured',receipt,record,modelTurnsStarted:false,requestedTaskStarted:false};
|
|
2191
|
+
}
|
|
2192
|
+
if(expected.operation==='adopt'){
|
|
2193
|
+
if(Object.keys(expected.input).sort().join()!=='plan,receipt')throw new Error('Exact own host plan and creation receipt required.');
|
|
2194
|
+
const p=expected.input.plan,r=expected.input.receipt;await fixed(p.role);await fixed(p.intake);
|
|
2195
|
+
const raw=await manager.getSessionWhenLoaded(r.nativeId,'topolo-dynamic-adopt',{warnOnMiss:false});
|
|
2196
|
+
const view=await manager.getSession(r.nativeId);qualify();
|
|
2197
|
+
if(!raw||!view||raw.sessionId!==r.nativeId||view.sessionId!==r.nativeId||raw.cwd!==r.cwd||view.cwd!==r.cwd||raw.title!==view.title||
|
|
2198
|
+
raw.isArchived||view.isArchived||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.worktreePath||raw.worktreeLazy||
|
|
2199
|
+
raw.spawnedFrom?.sessionId!==p.intake.nativeId||raw.spawnedFrom?.sideSession!==true)throw new Error('Actual child native raw parent or placement differs.');
|
|
2200
|
+
return helper('adopt',{plan:p,receipt:r,native:{nativeId:r.nativeId,cwd:view.cwd,title:view.title,model:view.model,effort:view.effort,
|
|
2201
|
+
nativeParentId:raw.spawnedFrom.sessionId,archived:false,backend:'local',foreignWorkspace:false,
|
|
2202
|
+
busy:!!(view.isRunning||view.starting||raw.isRunning||raw.starting||manager.hasLosableWork(r.nativeId)),pendingApprovals:view.pendingToolPermissions?.length??0}});
|
|
2203
|
+
}
|
|
2204
|
+
const prepared=${JSON.stringify(prepared ?? null)};
|
|
2205
|
+
if(!prepared)throw new Error('Exact registered own child preparation required.');
|
|
2206
|
+
const nativePlan={...prepared.nativePlan,deadline:Date.now()+200000};
|
|
2207
|
+
const snapshotMarkdown=(${snapshotClaudeMarkdown.toString()});
|
|
2208
|
+
const verifyContext=()=>{
|
|
2209
|
+
qualify();
|
|
2210
|
+
for(const [file,hash] of Object.entries(prepared.context))
|
|
2211
|
+
if(crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')!==hash)throw new Error('Current source/registration/policy context changed.');
|
|
2212
|
+
for(const [cwd,inventory] of Object.entries(prepared.contextInventories))
|
|
2213
|
+
if(canonical(snapshotMarkdown(cwd,fs,crypto,path))!==canonical(inventory))throw new Error('Current source charter inventory changed.');
|
|
2214
|
+
};
|
|
2215
|
+
const qualifyOrganization=async()=>{await helper('read',{laneId:nativePlan.laneId});verifyContext();};
|
|
2216
|
+
let historyBefore;
|
|
2217
|
+
const ownHistory=async()=>{
|
|
2218
|
+
const raw=await manager.getSessionWhenLoaded(nativePlan.nativeId,'topolo-dynamic-history',{warnOnMiss:false});
|
|
2219
|
+
if(!raw||raw.backend?.kind!=='local'||raw.backend.remoteTarget||raw.spawnedFrom?.sessionId!==nativePlan.intake.sessionId)throw new Error('Own child transcript eligibility changed.');
|
|
2220
|
+
const messages=await manager.getTranscriptForCallerElsewhere(nativePlan.nativeId);qualify();
|
|
2221
|
+
if(!Array.isArray(messages))throw new Error('Exact own child transcript resolver unavailable.');return messages;
|
|
2222
|
+
};
|
|
2223
|
+
const history=(id,before)=>{if(id!==nativePlan.nativeId)throw new Error('Unrelated transcript forbidden.');
|
|
2224
|
+
return before===undefined?historyBefore:fingerprint(historyBefore)===before;};
|
|
2225
|
+
if(expected.operation==='configure'){
|
|
2226
|
+
const operation=await helper('configure-begin',{laneId:nativePlan.laneId,generation});
|
|
2227
|
+
// Helper's owned marker changes the registration; verify immutable source inputs and that precise beforeimage.
|
|
2228
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2229
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'configure',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2230
|
+
const configured=await helper('configure-ack',{operation,native:result});
|
|
2231
|
+
const intakeRoute={key:prepared.record.plan.intake.key,sessionId:nativePlan.intake.sessionId,cwd:nativePlan.intake.cwd,title:nativePlan.intake.title,
|
|
2232
|
+
contextKey:prepared.record.plan.intake.contextKey,intakeKey:prepared.record.plan.intake.key,intakeSessionId:nativePlan.intake.sessionId};
|
|
2233
|
+
if(!manager[Symbol.for('topolo.agent-pack.approvals.v1')])
|
|
2234
|
+
await (${nativeClaudeApprovalOperation.toString()})(manager,[intakeRoute],{kind:'install'},fingerprint,checkpoint);
|
|
2235
|
+
const route={key:prepared.record.plan.role.key+'/dynamic/'+nativePlan.laneId,sessionId:nativePlan.nativeId,cwd:nativePlan.cwd,title:nativePlan.title,
|
|
2236
|
+
contextKey:prepared.record.plan.role.contextKey,intakeKey:prepared.record.plan.intake.key,intakeSessionId:nativePlan.intake.sessionId,
|
|
2237
|
+
dynamic:{laneId:nativePlan.laneId,planDigest:nativePlan.planDigest,nativeParentId:nativePlan.intake.sessionId,generation,connectionId:nativePlan.laneId}};
|
|
2238
|
+
await (${nativeClaudeAppendDynamicRoute.toString()})(manager,route,qualifyOrganization,checkpoint,{
|
|
2239
|
+
request:async(record,pending)=>{if(typeof pending.toolUseId!=='string')throw new Error('Actual native permission toolUseId is missing.');
|
|
2240
|
+
await helper('permission-request',{laneId:nativePlan.laneId,request:{nativeId:nativePlan.nativeId,fingerprint:record.fingerprint,requestId:pending.requestId,
|
|
2241
|
+
generation,intakeKey:record.intakeKey,intakeNativeId:record.intakeSessionId,toolUseId:pending.toolUseId}});},
|
|
2242
|
+
decision:async(record,pending,decision)=>helper('permission-decision',{laneId:nativePlan.laneId,decision:decision==='once'?'approved':'denied',
|
|
2243
|
+
intakeKey:record.intakeKey,fingerprint:record.fingerprint,generation})});
|
|
2244
|
+
const control=expected.control;
|
|
2245
|
+
if(!control)throw new Error('Exact owned per-lane native control setup required.');
|
|
2246
|
+
const secretInfo=fs.lstatSync(control.secretPath);
|
|
2247
|
+
if(!secretInfo.isFile()||secretInfo.isSymbolicLink()||secretInfo.uid!==process.getuid()||secretInfo.mode&63)throw new Error('Unowned private native control secret.');
|
|
2248
|
+
const secret=JSON.parse(fs.readFileSync(control.secretPath,'utf8'));
|
|
2249
|
+
const ownedOperation=async op=>{
|
|
2250
|
+
requireClaudePrivateQueryEffectQualified(op.kind==='approval-respond'&&op.decision==='once'?'approval-once':op.kind,{continuityTransport:nativePlan.continuityTransport});
|
|
2251
|
+
qualify();const current=await helper('read',{laneId:nativePlan.laneId});
|
|
2252
|
+
const scopedPlan={...nativePlan,lifecycleState:current.state,deadline:Date.now()+200000};
|
|
2253
|
+
if(op.kind.startsWith('approval-')){
|
|
2254
|
+
const state=manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
2255
|
+
if(!state)throw new Error('Original native approval listener unavailable.');
|
|
2256
|
+
if(op.kind!=='approval-list'){
|
|
2257
|
+
const entry=state.entries.get(op.key);
|
|
2258
|
+
if(!entry||entry.record.sourceSessionId!==nativePlan.nativeId||entry.record.intakeSessionId!==nativePlan.intake.sessionId)
|
|
2259
|
+
throw new Error('Native approval is outside the exact owned child/original intake.');
|
|
2260
|
+
}
|
|
2261
|
+
const result=await (${nativeClaudeApprovalOperation.toString()})(manager,state.routes,
|
|
2262
|
+
{...op,kind:op.kind.slice('approval-'.length)},fingerprint,checkpoint);
|
|
2263
|
+
return op.kind==='approval-list'?{requests:result.requests.filter(r=>r.sourceSessionId===nativePlan.nativeId)}:result;
|
|
2264
|
+
}
|
|
2265
|
+
if(op.kind==='context-start'||op.kind==='context-read')return (${nativeClaudeDynamicContext.toString()})(manager,scopedPlan,
|
|
2266
|
+
{kind:op.kind,operationId:op.operationId},helper,fingerprint,verifyContext,qualifyOrganization);
|
|
2267
|
+
if(op.kind==='fixture-start'||op.kind==='fixture-read')return (${nativeClaudeDynamicFixture.toString()})(manager,scopedPlan,
|
|
2268
|
+
{kind:op.kind,operationId:op.operationId},helper,verifyContext,qualifyOrganization);
|
|
2269
|
+
if(op.kind==='read')return (${nativeClaudeDynamicLaneOperation.toString()})(manager,scopedPlan,'read',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2270
|
+
if(op.kind==='retire'){
|
|
2271
|
+
const messages=await ownHistory();const before=fingerprint(messages);const retirement=await helper('retire-begin',{laneId:scopedPlan.laneId});
|
|
2272
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,scopedPlan,'retire',checkpoint,verifyContext,qualifyOrganization,
|
|
2273
|
+
async(id,saved)=>{if(id!==scopedPlan.nativeId)throw new Error('Unrelated transcript forbidden.');return saved===undefined?before:fingerprint(await ownHistory())===saved;});
|
|
2274
|
+
const retired=await helper('retire-ack',{operation:retirement,native:result});
|
|
2275
|
+
const state=manager[Symbol.for('topolo.agent-pack.approvals.v1')];
|
|
2276
|
+
if([...state.entries.values()].some(e=>e.record.sourceSessionId===scopedPlan.nativeId&&['pending','response-may-have-effects'].includes(e.record.state)))
|
|
2277
|
+
throw new Error('Own retired route still has an unresolved native request.');
|
|
2278
|
+
state.byId.delete(scopedPlan.nativeId);state.byKey.delete(route.key);state.dynamicRoutes?.delete(scopedPlan.nativeId);state.dynamicLifecycle?.delete(scopedPlan.nativeId);
|
|
2279
|
+
return retired;
|
|
2280
|
+
}
|
|
2281
|
+
if(op.kind==='close'){
|
|
2282
|
+
if(current.state!=='retired')throw new Error('Only a normally retired own child control may close.');
|
|
2283
|
+
return {state:'normal-own-lane-control-close-approved',laneId:scopedPlan.laneId};
|
|
2284
|
+
}
|
|
2285
|
+
throw new Error('Unsupported exact owned native lane operation.');
|
|
2286
|
+
};
|
|
2287
|
+
const connection=await (${nativeClaudeLaneControl.toString()})(load('node:net'),fs,manager,
|
|
2288
|
+
{socket:control.socket,pid:expected.pid,identity:control.identity,laneId:nativePlan.laneId},secret.token,
|
|
2289
|
+
ownedOperation,qualify,checkpoint);
|
|
2290
|
+
return {record:configured,connection};
|
|
2291
|
+
|
|
2292
|
+
}
|
|
2293
|
+
if(expected.operation==='read')return (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'read',checkpoint,verifyContext,qualifyOrganization,history);
|
|
2294
|
+
if(expected.operation==='context-start'||expected.operation==='context-read'){
|
|
2295
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2296
|
+
return (${nativeClaudeDynamicContext.toString()})(manager,nativePlan,{kind:expected.operation,operationId:expected.input.operationId},
|
|
2297
|
+
helper,fingerprint,verifyContext,qualifyOrganization);
|
|
2298
|
+
}
|
|
2299
|
+
if(expected.operation==='fixture-start'||expected.operation==='fixture-read'){
|
|
2300
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2301
|
+
return (${nativeClaudeDynamicFixture.toString()})(manager,nativePlan,{kind:expected.operation,operationId:expected.input.operationId},
|
|
2302
|
+
helper,verifyContext,qualifyOrganization);
|
|
2303
|
+
}
|
|
2304
|
+
if(expected.operation==='retire'){
|
|
2305
|
+
historyBefore=await ownHistory();const before=fingerprint(historyBefore);
|
|
2306
|
+
const operation=await helper('retire-begin',{laneId:nativePlan.laneId});
|
|
2307
|
+
delete prepared.context[path.resolve(expected.root,'dynamic-lanes/claude',nativePlan.laneId+'.json')];
|
|
2308
|
+
const result=await (${nativeClaudeDynamicLaneOperation.toString()})(manager,nativePlan,'retire',checkpoint,verifyContext,qualifyOrganization,
|
|
2309
|
+
async(id,saved)=>{if(id!==nativePlan.nativeId)throw new Error('Unrelated archive transcript forbidden.');if(saved===undefined)return before;return fingerprint(await ownHistory())===saved;});
|
|
2310
|
+
historyBefore=await ownHistory();if(fingerprint(historyBefore)!==before)throw new Error('Own child transcript preservation differs.');
|
|
2311
|
+
return helper('retire-ack',{operation,native:{...result,historyPreserved:true}});
|
|
2312
|
+
}
|
|
2313
|
+
throw new Error('Closed native fixture operation not yet qualified on this connection.');
|
|
2314
|
+
})()`;
|
|
2315
|
+
}
|
|
2316
|
+
async function operateClaudeLane(options, evaluate = evaluateClaudeMain) {
|
|
2317
|
+
requireClaudePrivateQueryEffectQualified(options.operation, options.input);
|
|
2318
|
+
validateInspectorEndpoint(options.endpoint);
|
|
2319
|
+
const root = realpathSync(options.store);
|
|
2320
|
+
const policy = realpathSync(options.policy);
|
|
2321
|
+
const release = await lockClaudeLibrary(root);
|
|
2322
|
+
try {
|
|
2323
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
2324
|
+
const source = [resolve(here, "../../agent-packs/src"), resolve(here, "../agent-packs/src")].find(existsSync);
|
|
2325
|
+
if (!source) throw new Error("Installed typed lifecycle helper source unavailable.");
|
|
2326
|
+
const directory = resolve(root, "claude/dynamic-journals");
|
|
2327
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2328
|
+
if (lstatSync(directory).isSymbolicLink()) throw new Error("Unowned native lifecycle journal directory.");
|
|
2329
|
+
const journal = resolve(directory, randomUUID() + ".jsonl");
|
|
2330
|
+
durableRecord(journal, { operation: options.operation, pid: options.pid, at: Date.now() }, "wx");
|
|
2331
|
+
const prepared = ["plan", "create", "create-read", "adopt"].includes(options.operation) ? void 0 : prepareClaudeDynamicLane(root, options.pack, policy, options.input.laneId);
|
|
2332
|
+
if (prepared?.record.plan.continuityTransport === "native-cli") requireClaudePrivateQueryEffectQualified(options.operation, prepared.record.plan);
|
|
2333
|
+
let sourceRoleKey = prepared?.record.plan.role.key ?? (options.operation === "plan" ? options.input.request?.sourceRoleKey : options.input.plan?.role?.key);
|
|
2334
|
+
if (["create", "create-read"].includes(options.operation)) {
|
|
2335
|
+
if (typeof options.input.laneId !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(options.input.laneId)) throw new Error("Exact existing creation lane required.");
|
|
2336
|
+
const file = resolve(root, "dynamic-lanes/claude/plans", options.input.laneId + ".json");
|
|
2337
|
+
const info = lstatSync(file);
|
|
2338
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.uid !== process.getuid?.() || info.mode & 18)
|
|
2339
|
+
throw new Error("Unowned immutable creation plan.");
|
|
2340
|
+
sourceRoleKey = JSON.parse(readFileSync(file, "utf8")).role?.key;
|
|
2341
|
+
}
|
|
2342
|
+
const privateConnection = prepareClaudePrivateHelperConnection(root, options.pack, policy, sourceRoleKey);
|
|
2343
|
+
let control;
|
|
2344
|
+
let connectionFile;
|
|
2345
|
+
if (options.operation === "configure") {
|
|
2346
|
+
if (!prepared) throw new Error("Exact fresh child preparation required.");
|
|
2347
|
+
const laneId = prepared.nativePlan.laneId;
|
|
2348
|
+
const privateRoot = resolve(root, "claude/dynamic-controls");
|
|
2349
|
+
mkdirSync(privateRoot, { recursive: true, mode: 448 });
|
|
2350
|
+
if (lstatSync(privateRoot).isSymbolicLink()) throw new Error("Unowned dynamic control directory.");
|
|
2351
|
+
const directory2 = resolve(privateRoot, laneId);
|
|
2352
|
+
mkdirSync(directory2, { mode: 448 });
|
|
2353
|
+
const secretPath = resolve(directory2, "secret.json");
|
|
2354
|
+
durableRecord(secretPath, { token: randomBytes(32).toString("hex") }, "wx");
|
|
2355
|
+
const identity = createHash2("sha256").update(JSON.stringify({ pid: options.pid, laneId, planDigest: prepared.record.planDigest, qualifier: CLAUDE_NATIVE_QUALIFIER })).digest("hex");
|
|
2356
|
+
const socketDirectory = resolve("/tmp", "topolo-claude-" + process.getuid?.() + "-" + createHash2("sha256").update(root + "\0" + laneId).digest("hex").slice(0, 24));
|
|
2357
|
+
mkdirSync(socketDirectory, { mode: 448 });
|
|
2358
|
+
const socket = resolve(socketDirectory, "control.sock");
|
|
2359
|
+
control = { socket, secretPath, identity };
|
|
2360
|
+
connectionFile = resolve(directory2, "connection.json");
|
|
2361
|
+
durableRecord(connectionFile, { state: "prepared", pid: options.pid, laneId, pack: options.pack, policy, root, journal, ...control }, "wx");
|
|
2362
|
+
}
|
|
2363
|
+
const result = await evaluate(options.endpoint, claudeLaneExpression({
|
|
2364
|
+
root,
|
|
2365
|
+
pack: options.pack,
|
|
2366
|
+
policy,
|
|
2367
|
+
pid: options.pid,
|
|
2368
|
+
operation: options.operation,
|
|
2369
|
+
input: options.input,
|
|
2370
|
+
privateConnection,
|
|
2371
|
+
...control ? { control } : {}
|
|
2372
|
+
}, realpathSync(source), journal, prepared));
|
|
2373
|
+
if (connectionFile) {
|
|
2374
|
+
const value = JSON.parse(readFileSync(connectionFile, "utf8"));
|
|
2375
|
+
writeFileSync(connectionFile, JSON.stringify({ ...value, state: "ready" }) + "\n", { mode: 384 });
|
|
2376
|
+
}
|
|
2377
|
+
return result;
|
|
2378
|
+
} finally {
|
|
2379
|
+
await release();
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function operateClaudeLaneControl(options, request = requestClaudeApprovalControl) {
|
|
2383
|
+
requireClaudePrivateQueryEffectQualified(options.operation.kind === "approval-respond" && options.operation.decision === "once" ? "approval-once" : options.operation.kind, options.operation);
|
|
2384
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.connection)) throw new Error("Exact owned native lane connection ID required.");
|
|
2385
|
+
const root = realpathSync(options.store);
|
|
2386
|
+
const directory = resolve(root, "claude/dynamic-controls", options.connection);
|
|
2387
|
+
const owned = (file2) => {
|
|
2388
|
+
const info = lstatSync(file2);
|
|
2389
|
+
if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 63)
|
|
2390
|
+
throw new Error("Unowned private native lane connection record.");
|
|
2391
|
+
};
|
|
2392
|
+
const file = resolve(directory, "connection.json");
|
|
2393
|
+
owned(file);
|
|
2394
|
+
const connection = JSON.parse(readFileSync(file, "utf8"));
|
|
2395
|
+
if (connection.state !== "ready" || connection.laneId !== options.connection || connection.root !== root || !Number.isSafeInteger(connection.pid))
|
|
2396
|
+
throw new Error("Exact current ready native lane connection required.");
|
|
2397
|
+
owned(connection.secretPath);
|
|
2398
|
+
if (realpathSync(connection.secretPath) !== resolve(directory, "secret.json")) throw new Error("Private native secret path differs.");
|
|
2399
|
+
const secret = JSON.parse(readFileSync(connection.secretPath, "utf8"));
|
|
2400
|
+
return request(connection, options.operation, secret.token);
|
|
2401
|
+
}
|
|
1457
2402
|
export {
|
|
1458
2403
|
CLAUDE_NATIVE_QUALIFIER,
|
|
2404
|
+
claudeLaneExpression,
|
|
1459
2405
|
configureClaudePack,
|
|
1460
2406
|
evaluateClaudeMain,
|
|
1461
2407
|
lockClaudeLibrary,
|
|
1462
2408
|
nativePackExpression,
|
|
1463
2409
|
operateClaudeApprovals,
|
|
2410
|
+
operateClaudeLane,
|
|
2411
|
+
operateClaudeLaneControl,
|
|
1464
2412
|
operateClaudeSmoke,
|
|
2413
|
+
prepareClaudeDynamicLane,
|
|
1465
2414
|
prepareClaudeNativePlan,
|
|
2415
|
+
prepareClaudePrivateHelperConnection,
|
|
2416
|
+
privateClaudeHelperEnvironment,
|
|
1466
2417
|
requestClaudeApprovalControl,
|
|
2418
|
+
requireClaudePrivateQueryEffectQualified,
|
|
2419
|
+
runClaudeLaneHelper,
|
|
1467
2420
|
runClaudeSmokeHelper,
|
|
1468
2421
|
snapshotClaudeMarkdown,
|
|
1469
2422
|
validateInspectorEndpoint,
|