cool-workflow 0.1.80 → 0.1.82
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +42 -2
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/agent-config.js +21 -7
- package/dist/candidate-scoring.js +62 -48
- package/dist/capability-core.js +152 -96
- package/dist/capability-registry.js +160 -174
- package/dist/cli.js +90 -100
- package/dist/collaboration.js +5 -6
- package/dist/commit.js +232 -209
- package/dist/compare.js +18 -0
- package/dist/coordinator/classify.js +45 -0
- package/dist/coordinator/paths.js +42 -0
- package/dist/coordinator/util.js +126 -0
- package/dist/coordinator.js +127 -300
- package/dist/dispatch.js +46 -3
- package/dist/drive.js +7 -7
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +5 -2
- package/dist/execution-backend/agent.js +294 -0
- package/dist/execution-backend/probes.js +96 -0
- package/dist/execution-backend/util.js +47 -0
- package/dist/execution-backend.js +77 -450
- package/dist/gates.js +48 -0
- package/dist/mcp-server.js +34 -173
- package/dist/multi-agent/graph.js +84 -0
- package/dist/multi-agent/helpers.js +141 -0
- package/dist/multi-agent/ids.js +20 -0
- package/dist/multi-agent/paths.js +22 -0
- package/dist/multi-agent-eval/format.js +194 -0
- package/dist/multi-agent-eval/normalize.js +51 -0
- package/dist/multi-agent-eval.js +63 -242
- package/dist/multi-agent-host.js +53 -40
- package/dist/multi-agent-operator-ux.js +2 -1
- package/dist/multi-agent-trust.js +5 -5
- package/dist/multi-agent.js +125 -314
- package/dist/node-projection.js +59 -0
- package/dist/node-snapshot.js +11 -21
- package/dist/observability/format.js +61 -0
- package/dist/observability/intake.js +98 -0
- package/dist/observability.js +14 -160
- package/dist/operator-ux/format.js +364 -0
- package/dist/operator-ux.js +22 -363
- package/dist/orchestrator/lifecycle-operations.js +22 -1
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +41 -10
- package/dist/reclamation.js +32 -55
- package/dist/run-export.js +138 -14
- package/dist/run-registry/derive.js +172 -0
- package/dist/run-registry/format.js +124 -0
- package/dist/run-registry/gc.js +251 -0
- package/dist/run-registry/policy.js +16 -0
- package/dist/run-registry/queue.js +116 -0
- package/dist/run-registry.js +78 -593
- package/dist/run-state-schema.js +1 -0
- package/dist/sandbox-profile.js +43 -2
- package/dist/scheduler.js +34 -4
- package/dist/state-explosion/format.js +159 -0
- package/dist/state-explosion/helpers.js +82 -0
- package/dist/state-explosion.js +65 -283
- package/dist/state-node.js +19 -4
- package/dist/telemetry-attestation.js +55 -0
- package/dist/telemetry-demo.js +15 -3
- package/dist/telemetry-ledger.js +60 -15
- package/dist/topology.js +50 -12
- package/dist/triggers.js +33 -14
- package/dist/trust-audit.js +215 -71
- package/dist/validation.js +328 -0
- package/dist/version.js +1 -1
- package/dist/worker-isolation/helpers.js +51 -0
- package/dist/worker-isolation/paths.js +46 -0
- package/dist/worker-isolation.js +182 -173
- package/docs/agent-delegation-drive.7.md +14 -0
- package/docs/cli-mcp-parity.7.md +5 -0
- package/docs/contract-migration-tooling.7.md +3 -0
- package/docs/control-plane-scheduling.7.md +3 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +5 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +3 -0
- package/docs/execution-backends.7.md +3 -0
- package/docs/index.md +1 -0
- package/docs/launch/launch-kit.md +46 -23
- package/docs/launch/pre-launch-checklist.md +14 -14
- package/docs/multi-agent-cli-mcp-surface.7.md +5 -0
- package/docs/multi-agent-eval-replay-harness.7.md +3 -0
- package/docs/multi-agent-operator-ux.7.md +3 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +3 -0
- package/docs/observability-cost-accounting.7.md +3 -0
- package/docs/project-index.md +23 -6
- package/docs/real-execution-backends.7.md +3 -0
- package/docs/release-and-migration.7.md +5 -0
- package/docs/release-tooling.7.md +35 -2
- package/docs/run-registry-control-plane.7.md +55 -8
- package/docs/run-retention-reclamation.7.md +5 -0
- package/docs/state-explosion-management.7.md +3 -0
- package/docs/team-collaboration.7.md +3 -0
- package/docs/trust-model.md +267 -0
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +3 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +4 -2
- package/scripts/agents/builtin-templates.json +7 -0
- package/scripts/bump-version.js +5 -11
- package/scripts/canonical-apps-list.js +64 -0
- package/scripts/canonical-apps.js +19 -4
- package/scripts/children/batch-delegate-child.js +58 -0
- package/scripts/children/http-delegate-child.js +39 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +5 -0
- package/scripts/release-check.js +5 -1
- package/scripts/release-flow.js +181 -5
- package/scripts/version-sync-check.js +5 -8
- package/dist/capability-dispatcher.js +0 -86
package/dist/reclamation.js
CHANGED
|
@@ -29,7 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
29
29
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
30
30
|
};
|
|
31
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS =
|
|
32
|
+
exports.ReclamationError = exports.ReclamationAbort = exports.SKELETON_REQUIRED_KEYS = void 0;
|
|
33
33
|
exports.sha256OfString = sha256OfString;
|
|
34
34
|
exports.sha256OfFile = sha256OfFile;
|
|
35
35
|
exports.dirBytes = dirBytes;
|
|
@@ -53,10 +53,11 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
53
53
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
54
54
|
const node_path_1 = __importDefault(require("node:path"));
|
|
55
55
|
const multi_agent_eval_1 = require("./multi-agent-eval");
|
|
56
|
+
const node_projection_1 = require("./node-projection");
|
|
56
57
|
const node_snapshot_1 = require("./node-snapshot");
|
|
57
58
|
const state_1 = require("./state");
|
|
58
59
|
const trust_audit_1 = require("./trust-audit");
|
|
59
|
-
|
|
60
|
+
const compare_1 = require("./compare");
|
|
60
61
|
/** The skeleton schema is the contract for what MUST survive every reclamation.
|
|
61
62
|
* Machine-checkable via validateSkeleton(). If extraction can't produce all of
|
|
62
63
|
* these, reclamation fails closed and frees nothing. */
|
|
@@ -135,7 +136,7 @@ function contentDigest(p) {
|
|
|
135
136
|
return sha256OfFile(p);
|
|
136
137
|
const parts = [];
|
|
137
138
|
const walk = (dir, rel) => {
|
|
138
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name
|
|
139
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (0, compare_1.compareBytes)(a.name, b.name))) {
|
|
139
140
|
const abs = node_path_1.default.join(dir, entry.name);
|
|
140
141
|
const r = node_path_1.default.join(rel, entry.name);
|
|
141
142
|
if (entry.isDirectory())
|
|
@@ -261,7 +262,7 @@ function extractSkeleton(run) {
|
|
|
261
262
|
}
|
|
262
263
|
const evidenceDigests = [...evidenceMap.entries()]
|
|
263
264
|
.map(([ref, digest]) => ({ ref, digest }))
|
|
264
|
-
.sort((a, b) => a.ref
|
|
265
|
+
.sort((a, b) => (0, compare_1.compareBytes)(a.ref, b.ref));
|
|
265
266
|
const eventLog = auditEventLogPath(run);
|
|
266
267
|
const auditLogDigest = node_fs_1.default.existsSync(eventLog) ? sha256OfFile(eventLog) : sha256OfString("");
|
|
267
268
|
const events = node_fs_1.default.existsSync(eventLog)
|
|
@@ -287,7 +288,7 @@ function extractSkeleton(run) {
|
|
|
287
288
|
};
|
|
288
289
|
const collaboration = run.collaboration;
|
|
289
290
|
const collaborationLog = {
|
|
290
|
-
digest: sha256OfString((0, multi_agent_eval_1.
|
|
291
|
+
digest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(collaboration || {})),
|
|
291
292
|
approvals: collaboration?.approvals?.length || 0,
|
|
292
293
|
comments: collaboration?.comments?.length || 0,
|
|
293
294
|
handoffs: collaboration?.handoffs?.length || 0
|
|
@@ -397,46 +398,17 @@ function buildReferenceGraph(run) {
|
|
|
397
398
|
add(message.id);
|
|
398
399
|
return refs;
|
|
399
400
|
}
|
|
401
|
+
/** expectDigest of a node's deterministic projection. Re-uses the SHARED
|
|
402
|
+
* node-projection field set (node-projection.ts) so reconstruction matches
|
|
403
|
+
* node-snapshot.ts's body byte-for-byte — the projection can no longer drift. */
|
|
400
404
|
function snapshotProjectionDigest(node) {
|
|
401
|
-
|
|
402
|
-
const body = (0, multi_agent_eval_1.normalizeValue)({
|
|
403
|
-
id: node.id,
|
|
404
|
-
kind: node.kind,
|
|
405
|
-
status: node.status,
|
|
406
|
-
loopStage: node.loopStage,
|
|
407
|
-
inputs: node.inputs,
|
|
408
|
-
outputs: node.outputs,
|
|
409
|
-
artifacts: node.artifacts,
|
|
410
|
-
evidence: node.evidence,
|
|
411
|
-
errors: node.errors,
|
|
412
|
-
parents: node.parents,
|
|
413
|
-
children: node.children,
|
|
414
|
-
contractId: node.contractId,
|
|
415
|
-
metadata: node.metadata
|
|
416
|
-
});
|
|
417
|
-
return sha256OfString((0, multi_agent_eval_1.stableStringify)(body));
|
|
405
|
+
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
418
406
|
}
|
|
419
407
|
/** Body digest of the RETAINED node (lives in state.json). The reconstruction
|
|
420
|
-
* verifier re-derives the projection from this retained input.
|
|
408
|
+
* verifier re-derives the projection from this retained input. Same shared field
|
|
409
|
+
* set / canonical bytes as snapshotProjectionDigest. */
|
|
421
410
|
function nodeBodyDigest(node) {
|
|
422
|
-
return sha256OfString((0,
|
|
423
|
-
}
|
|
424
|
-
function rawNodeBody(node) {
|
|
425
|
-
return {
|
|
426
|
-
id: node.id,
|
|
427
|
-
kind: node.kind,
|
|
428
|
-
status: node.status,
|
|
429
|
-
loopStage: node.loopStage,
|
|
430
|
-
inputs: node.inputs,
|
|
431
|
-
outputs: node.outputs,
|
|
432
|
-
artifacts: node.artifacts,
|
|
433
|
-
evidence: node.evidence,
|
|
434
|
-
errors: node.errors,
|
|
435
|
-
parents: node.parents,
|
|
436
|
-
children: node.children,
|
|
437
|
-
contractId: node.contractId,
|
|
438
|
-
metadata: node.metadata
|
|
439
|
-
};
|
|
411
|
+
return sha256OfString((0, node_projection_1.nodeProjectionDigestInput)(node));
|
|
440
412
|
}
|
|
441
413
|
/** Build the retention plan: which paths are freeable under `policy`, of what
|
|
442
414
|
* kind, how many bytes, and the resulting capability downgrade. */
|
|
@@ -449,7 +421,6 @@ function planReclamation(run, policy = {}) {
|
|
|
449
421
|
// freeable once the result node's worker-result artifact is re-pointed.
|
|
450
422
|
let reclaimedScratch = false;
|
|
451
423
|
if (!policy.keepScratch) {
|
|
452
|
-
const workersDir = run.paths.workersDir || node_path_1.default.join(runDir, "workers");
|
|
453
424
|
for (const scope of run.workers || []) {
|
|
454
425
|
const workerDir = scope.workerDir;
|
|
455
426
|
if (!workerDir || !node_fs_1.default.existsSync(workerDir))
|
|
@@ -472,7 +443,6 @@ function planReclamation(run, policy = {}) {
|
|
|
472
443
|
});
|
|
473
444
|
reclaimedScratch = true;
|
|
474
445
|
}
|
|
475
|
-
void workersDir;
|
|
476
446
|
}
|
|
477
447
|
// A node whose scratch is being re-pointed THIS pass must NOT also have its
|
|
478
448
|
// snapshot freed in the same pass — re-pointing mutates the node body, which
|
|
@@ -515,7 +485,7 @@ function planReclamation(run, policy = {}) {
|
|
|
515
485
|
const recipe = {
|
|
516
486
|
recipeKind: "node-snapshot-projection",
|
|
517
487
|
inputDigests: [inputDigest],
|
|
518
|
-
inputsDigest: sha256OfString((0, multi_agent_eval_1.
|
|
488
|
+
inputsDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest])),
|
|
519
489
|
expectDigest: snapshotProjectionDigest(node),
|
|
520
490
|
sourceRef: node.id
|
|
521
491
|
};
|
|
@@ -531,6 +501,13 @@ function planReclamation(run, policy = {}) {
|
|
|
531
501
|
// retention, and we do not yet auto-capture reconstruction recipes for them.
|
|
532
502
|
// The reference graph is consulted so the door is closed, not merely unbuilt.
|
|
533
503
|
void buildReferenceGraph;
|
|
504
|
+
// Determinism (HARD constraint): the snapshot candidates above are gathered in
|
|
505
|
+
// fs.readdirSync order, which is filesystem-dependent. freeable feeds the freed
|
|
506
|
+
// manifest that buildTombstone binds into tombstoneHash (and the prevTombstoneHash
|
|
507
|
+
// chain), so an unsorted order makes the tombstone hash irreproducible across
|
|
508
|
+
// hosts. Sort by path — the same compareBytes discipline the directory reads at
|
|
509
|
+
// :128 and the reference list at :243 already use — before anything hashes it.
|
|
510
|
+
freeable.sort((a, b) => (0, compare_1.compareBytes)(a.path, b.path));
|
|
534
511
|
const byKind = {};
|
|
535
512
|
let bytesToFree = 0;
|
|
536
513
|
for (const entry of freeable) {
|
|
@@ -557,16 +534,16 @@ function planReclamation(run, policy = {}) {
|
|
|
557
534
|
return { freeable, bytesToFree, byKind, capability, capabilityReason };
|
|
558
535
|
}
|
|
559
536
|
function policyDigestOf(policy) {
|
|
560
|
-
return sha256OfString((0, multi_agent_eval_1.
|
|
537
|
+
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(policy));
|
|
561
538
|
}
|
|
562
539
|
/** genesis prevTombstoneHash = sha256 of the sealed skeleton. */
|
|
563
540
|
function genesisPrevHash(skeleton) {
|
|
564
|
-
return sha256OfString((0, multi_agent_eval_1.
|
|
541
|
+
return sha256OfString((0, multi_agent_eval_1.replayStableStringify)(skeleton));
|
|
565
542
|
}
|
|
566
543
|
/** The canonical bytes a tombstoneHash binds: freed-manifest + sealed skeleton +
|
|
567
544
|
* prevTombstoneHash + capability. Recomputed independently by `gc verify`. */
|
|
568
545
|
function tombstoneHashInput(t) {
|
|
569
|
-
return (0, multi_agent_eval_1.
|
|
546
|
+
return (0, multi_agent_eval_1.replayStableStringify)({
|
|
570
547
|
runId: t.runId,
|
|
571
548
|
tombstoneId: t.tombstoneId,
|
|
572
549
|
reclaimedAt: t.reclaimedAt,
|
|
@@ -574,7 +551,7 @@ function tombstoneHashInput(t) {
|
|
|
574
551
|
policyDigest: t.policyDigest,
|
|
575
552
|
freed: t.freed.map((f) => ({ path: f.path, kind: f.kind, bytes: f.bytes, sha256: f.sha256, recipe: f.recipe || null })),
|
|
576
553
|
bytesFreed: t.bytesFreed,
|
|
577
|
-
skeletonDigest: sha256OfString((0, multi_agent_eval_1.
|
|
554
|
+
skeletonDigest: sha256OfString((0, multi_agent_eval_1.replayStableStringify)(t.skeleton)),
|
|
578
555
|
capability: t.capability,
|
|
579
556
|
capabilityReason: t.capabilityReason,
|
|
580
557
|
prevTombstoneHash: t.prevTombstoneHash
|
|
@@ -583,11 +560,11 @@ function tombstoneHashInput(t) {
|
|
|
583
560
|
function computeTombstoneHash(t) {
|
|
584
561
|
return sha256OfString(tombstoneHashInput(t));
|
|
585
562
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
return `tomb-${
|
|
563
|
+
function tombstoneId(seq) {
|
|
564
|
+
// Deterministic (FreeBSD-audit L13): the chain POSITION, not a process-global
|
|
565
|
+
// counter or wall-clock stamp — tombstoneId is bound into the tombstoneHash
|
|
566
|
+
// chain that `gc verify` recomputes, so it must be reproducible.
|
|
567
|
+
return `tomb-${String(seq).padStart(3, "0")}`;
|
|
591
568
|
}
|
|
592
569
|
/** STEP 2: build the FULL tombstone (pre-deletion sha256 per freed path + the
|
|
593
570
|
* hash chain). Reads the freed files (still present); mutates nothing on disk. */
|
|
@@ -605,7 +582,7 @@ function buildTombstone(run, skeleton, plan, options = {}) {
|
|
|
605
582
|
const base = {
|
|
606
583
|
schemaVersion: 1,
|
|
607
584
|
runId: run.id,
|
|
608
|
-
tombstoneId: tombstoneId(
|
|
585
|
+
tombstoneId: tombstoneId(prior.length + 1),
|
|
609
586
|
reclaimedAt: now,
|
|
610
587
|
actor: options.actor,
|
|
611
588
|
policyDigest: policyDigestOf(options.policy || {}),
|
|
@@ -800,7 +777,7 @@ function reconstructArtifact(run, recipe) {
|
|
|
800
777
|
return { inputsDigest: sha256OfString("absent"), expectDigest: sha256OfString("absent") };
|
|
801
778
|
}
|
|
802
779
|
const inputDigest = nodeBodyDigest(node);
|
|
803
|
-
const inputsDigest = sha256OfString((0, multi_agent_eval_1.
|
|
780
|
+
const inputsDigest = sha256OfString((0, multi_agent_eval_1.replayStableStringify)([inputDigest]));
|
|
804
781
|
const expectDigest = snapshotProjectionDigest(node);
|
|
805
782
|
return { inputsDigest, expectDigest };
|
|
806
783
|
}
|
package/dist/run-export.js
CHANGED
|
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
14
14
|
exports.exportRun = exportRun;
|
|
15
15
|
exports.importRun = importRun;
|
|
16
16
|
exports.verifyImportedRun = verifyImportedRun;
|
|
17
|
+
exports.inspectArchive = inspectArchive;
|
|
17
18
|
exports.importManifestPath = importManifestPath;
|
|
18
19
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
20
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -21,6 +22,8 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
|
21
22
|
const state_1 = require("./state");
|
|
22
23
|
const version_1 = require("./version");
|
|
23
24
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
25
|
+
const trust_audit_1 = require("./trust-audit");
|
|
26
|
+
const compare_1 = require("./compare");
|
|
24
27
|
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
25
28
|
function exportRun(run, outputPath) {
|
|
26
29
|
const exportedAt = new Date().toISOString();
|
|
@@ -187,6 +190,17 @@ function verifyImportedRun(run) {
|
|
|
187
190
|
pass: telemetry.verified,
|
|
188
191
|
code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
|
|
189
192
|
});
|
|
193
|
+
// Re-prove the trust-audit hash chain on restore too. Telemetry was already
|
|
194
|
+
// re-proven above, but the decisions/sandbox/commit-gate audit chain — also
|
|
195
|
+
// exported under audit/ — was not, an asymmetry a tampered restore could slip
|
|
196
|
+
// through. An absent chain is verified:true (nothing to prove), so archives
|
|
197
|
+
// predating audit export append a PASSING check — no false-red.
|
|
198
|
+
const audit = (0, trust_audit_1.verifyTrustAudit)(run);
|
|
199
|
+
checks.push({
|
|
200
|
+
name: "trust-audit",
|
|
201
|
+
pass: audit.verified,
|
|
202
|
+
code: audit.verified ? undefined : "trust-audit-invalid"
|
|
203
|
+
});
|
|
190
204
|
return {
|
|
191
205
|
runId: run.id,
|
|
192
206
|
ok: checks.every((check) => check.pass),
|
|
@@ -195,6 +209,70 @@ function verifyImportedRun(run) {
|
|
|
195
209
|
checks
|
|
196
210
|
};
|
|
197
211
|
}
|
|
212
|
+
/** Read-only integrity inspection of a portable archive WITHOUT importing it. Never
|
|
213
|
+
* throws — a read error, bad JSON, unsupported schema, or any digest/size/count/
|
|
214
|
+
* manifest mismatch is reported as a structured check with ok:false. Writes nothing. */
|
|
215
|
+
function inspectArchive(archivePath) {
|
|
216
|
+
const base = {
|
|
217
|
+
schemaVersion: 1,
|
|
218
|
+
archivePath,
|
|
219
|
+
ok: false,
|
|
220
|
+
schemaSupported: false,
|
|
221
|
+
runId: null,
|
|
222
|
+
fileCount: 0,
|
|
223
|
+
manifestSha256: null,
|
|
224
|
+
archiveSha256: null,
|
|
225
|
+
checks: []
|
|
226
|
+
};
|
|
227
|
+
let bytes;
|
|
228
|
+
try {
|
|
229
|
+
bytes = node_fs_1.default.readFileSync(archivePath);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
|
|
233
|
+
}
|
|
234
|
+
base.archiveSha256 = sha256Bytes(bytes);
|
|
235
|
+
let raw;
|
|
236
|
+
try {
|
|
237
|
+
raw = JSON.parse(bytes.toString("utf8"));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return { ...base, checks: [{ name: "archive", pass: false, code: "archive-invalid-json", path: archivePath, actual: messageOf(error) }] };
|
|
241
|
+
}
|
|
242
|
+
if (raw.schemaVersion !== 1) {
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : base.schemaVersion,
|
|
246
|
+
checks: [{ name: "schema", pass: false, code: "unsupported-schema", expected: "1", actual: String(raw.schemaVersion) }]
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const files = normalizeArchiveFiles(raw);
|
|
251
|
+
const { checks } = collectArchiveDigestChecks(files, raw.integrity);
|
|
252
|
+
// Faithful preview of what `run import` would do under the same env: with
|
|
253
|
+
// CW_REQUIRE_ARCHIVE_INTEGRITY=1 a stripped-integrity archive is refused by
|
|
254
|
+
// import, so inspect must also report it as failing (ok:false / exit 1) rather
|
|
255
|
+
// than green — otherwise inspect-before-import is misleading in that policy.
|
|
256
|
+
// Default (env unset) is unchanged: inspection only reports the digest checks.
|
|
257
|
+
if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
258
|
+
checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
archivePath,
|
|
263
|
+
ok: checks.every((c) => c.pass),
|
|
264
|
+
schemaSupported: true,
|
|
265
|
+
runId: raw.run && raw.run.id ? raw.run.id : null,
|
|
266
|
+
fileCount: files.length,
|
|
267
|
+
manifestSha256: raw.integrity ? digestManifest(files) : null,
|
|
268
|
+
archiveSha256: base.archiveSha256,
|
|
269
|
+
checks
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return { ...base, schemaSupported: true, checks: [{ name: "archive", pass: false, code: "archive-malformed", path: archivePath, actual: messageOf(error) }] };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
198
276
|
function importManifestPath(run) {
|
|
199
277
|
return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
|
|
200
278
|
}
|
|
@@ -216,7 +294,7 @@ function collectArchiveFiles(run) {
|
|
|
216
294
|
if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
|
|
217
295
|
addExternalArtifactFile(entries, run, artifactPath);
|
|
218
296
|
}
|
|
219
|
-
return [...entries.values()].sort((left, right) => left.relativePath
|
|
297
|
+
return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
|
|
220
298
|
}
|
|
221
299
|
function addFile(entries, run, file, role) {
|
|
222
300
|
const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
|
|
@@ -318,34 +396,80 @@ function normalizeArchiveFiles(raw) {
|
|
|
318
396
|
};
|
|
319
397
|
});
|
|
320
398
|
}
|
|
321
|
-
|
|
399
|
+
/** NON-throwing digest/size/count/manifest verification: one structured check per
|
|
400
|
+
* file (in import order), then the integrity file-count + manifest checks. Shared
|
|
401
|
+
* by the throwing import path (verifyArchiveFileDigests) and the read-only
|
|
402
|
+
* inspectArchive, so a single offender list has one source of truth. */
|
|
403
|
+
function collectArchiveDigestChecks(files, integrity) {
|
|
404
|
+
const checks = [];
|
|
322
405
|
for (const file of files) {
|
|
323
406
|
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
324
407
|
const actual = sha256Bytes(bytes);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
408
|
+
const digestOk = actual === file.sha256;
|
|
409
|
+
checks.push(digestOk
|
|
410
|
+
? { name: "archive-file", pass: true, path: file.relativePath }
|
|
411
|
+
: { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
|
|
412
|
+
const sizeOk = bytes.length === file.sizeBytes;
|
|
413
|
+
checks.push(sizeOk
|
|
414
|
+
? { name: "archive-file", pass: true, path: file.relativePath }
|
|
415
|
+
: { name: "archive-file", pass: false, code: "size-mismatch", path: file.relativePath, expected: String(file.sizeBytes), actual: String(bytes.length) });
|
|
329
416
|
}
|
|
330
417
|
if (integrity) {
|
|
418
|
+
const countOk = integrity.fileCount === files.length;
|
|
419
|
+
checks.push(countOk
|
|
420
|
+
? { name: "archive-file-count", pass: true }
|
|
421
|
+
: { name: "archive-file-count", pass: false, code: "file-count-mismatch", expected: String(integrity.fileCount), actual: String(files.length) });
|
|
331
422
|
const actualManifest = digestManifest(files);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
423
|
+
const manifestOk = integrity.manifestSha256 === actualManifest;
|
|
424
|
+
checks.push(manifestOk
|
|
425
|
+
? { name: "archive-manifest", pass: true }
|
|
426
|
+
: { name: "archive-manifest", pass: false, code: "manifest-digest-mismatch", expected: integrity.manifestSha256, actual: actualManifest });
|
|
427
|
+
}
|
|
428
|
+
return { checks, ok: checks.every((c) => c.pass) };
|
|
429
|
+
}
|
|
430
|
+
/** Reconstruct the legacy throw message for a failing check, so the throwing import
|
|
431
|
+
* path stays BYTE-IDENTICAL after the collector refactor. */
|
|
432
|
+
function archiveCheckMessage(check) {
|
|
433
|
+
switch (check.code) {
|
|
434
|
+
case "digest-mismatch": return `Archive digest mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
|
|
435
|
+
case "size-mismatch": return `Archive size mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
|
|
436
|
+
case "file-count-mismatch": return `Archive file count mismatch: expected ${check.expected}, got ${check.actual}`;
|
|
437
|
+
case "manifest-digest-mismatch": return `Archive manifest digest mismatch: expected ${check.expected}, got ${check.actual}`;
|
|
438
|
+
default: return `Archive verification failed: ${check.name}`;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function verifyArchiveFileDigests(files, integrity) {
|
|
442
|
+
// Opt-in hardening (CW_REQUIRE_ARCHIVE_INTEGRITY=1): refuse an archive whose
|
|
443
|
+
// top-level integrity block (manifest digest + file count) is absent, closing the
|
|
444
|
+
// legacy fail-open seam where a stripped-integrity archive imported unverified.
|
|
445
|
+
// Same env-boolish convention as CW_REQUIRE_RESOLVABLE_EVIDENCE (evidence-grounding.ts:57).
|
|
446
|
+
// Default (unset) keeps legacy integrity-less archives byte-identical.
|
|
447
|
+
if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
448
|
+
throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
|
|
337
449
|
}
|
|
450
|
+
// Throw-before-write preserved: throw on the FIRST failing check, in the same
|
|
451
|
+
// order (per-file digest then size, then file-count, then manifest) and with the
|
|
452
|
+
// same message the inline checks produced.
|
|
453
|
+
const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
|
|
454
|
+
if (failed)
|
|
455
|
+
throw new Error(archiveCheckMessage(failed));
|
|
338
456
|
}
|
|
339
457
|
function digestManifest(files) {
|
|
340
458
|
const manifest = files
|
|
459
|
+
// sourcePath is deliberately EXCLUDED: it is a host-absolute bookkeeping path
|
|
460
|
+
// (for externalPathMap), not integrity-bearing content — the file's bytes are
|
|
461
|
+
// already bound by sha256 + sizeBytes. Including it would make the digest
|
|
462
|
+
// differ across hosts for byte-identical content, defeating cross-host repro.
|
|
341
463
|
.map((file) => ({
|
|
342
464
|
relativePath: file.relativePath,
|
|
343
465
|
role: file.role,
|
|
344
466
|
sha256: file.sha256,
|
|
345
|
-
sizeBytes: file.sizeBytes
|
|
346
|
-
sourcePath: file.sourcePath
|
|
467
|
+
sizeBytes: file.sizeBytes
|
|
347
468
|
}))
|
|
348
|
-
|
|
469
|
+
// Codepoint order, NOT localeCompare: this manifest feeds a sha256 integrity
|
|
470
|
+
// digest. Locale-sensitive collation would order identical bytes differently
|
|
471
|
+
// across hosts/locales, making the digest non-reproducible cross-host.
|
|
472
|
+
.sort((left, right) => (left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0));
|
|
349
473
|
return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
|
|
350
474
|
}
|
|
351
475
|
function rebaseRun(source, context) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LIFECYCLE_STATES = void 0;
|
|
7
|
+
exports.compareRecords = compareRecords;
|
|
8
|
+
exports.compareHistory = compareHistory;
|
|
9
|
+
exports.compareQueue = compareQueue;
|
|
10
|
+
exports.matchesQuery = matchesQuery;
|
|
11
|
+
exports.distinctBackends = distinctBackends;
|
|
12
|
+
exports.digestInputs = digestInputs;
|
|
13
|
+
exports.countRecords = countRecords;
|
|
14
|
+
exports.optionalLower = optionalLower;
|
|
15
|
+
exports.clampInt = clampInt;
|
|
16
|
+
exports.queueId = queueId;
|
|
17
|
+
exports.isRunLifecycleState = isRunLifecycleState;
|
|
18
|
+
exports.loadReclaimedFromDir = loadReclaimedFromDir;
|
|
19
|
+
// Pure, stateless helpers for the run registry — comparison, query matching,
|
|
20
|
+
// input digesting, counting, and small utilities. Carved out of run-registry.ts
|
|
21
|
+
// (FreeBSD-audit R2) so the stateful RunRegistry class no longer bundles the pure
|
|
22
|
+
// derivation layer. Nothing here touches `this`; everything is a pure function of
|
|
23
|
+
// its arguments (queueId is the lone exception — a process-local counter, kept as
|
|
24
|
+
// it was; making ID minting deterministic is a separate tracked item).
|
|
25
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
26
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
27
|
+
const compare_1 = require("../compare");
|
|
28
|
+
exports.LIFECYCLE_STATES = [
|
|
29
|
+
"queued",
|
|
30
|
+
"running",
|
|
31
|
+
"blocked",
|
|
32
|
+
"completed",
|
|
33
|
+
"failed",
|
|
34
|
+
"archived",
|
|
35
|
+
"reclaimed"
|
|
36
|
+
];
|
|
37
|
+
function compareRecords(a, b) {
|
|
38
|
+
if (a.createdAt !== b.createdAt)
|
|
39
|
+
return a.createdAt < b.createdAt ? -1 : 1;
|
|
40
|
+
return (0, compare_1.compareBytes)(a.runId, b.runId);
|
|
41
|
+
}
|
|
42
|
+
function compareHistory(a, b) {
|
|
43
|
+
// Newest first.
|
|
44
|
+
if (a.createdAt !== b.createdAt)
|
|
45
|
+
return a.createdAt < b.createdAt ? 1 : -1;
|
|
46
|
+
return (0, compare_1.compareBytes)(a.runId, b.runId);
|
|
47
|
+
}
|
|
48
|
+
function compareQueue(a, b) {
|
|
49
|
+
if (a.priority !== b.priority)
|
|
50
|
+
return a.priority - b.priority;
|
|
51
|
+
if (a.enqueuedAt !== b.enqueuedAt)
|
|
52
|
+
return a.enqueuedAt < b.enqueuedAt ? -1 : 1;
|
|
53
|
+
return (0, compare_1.compareBytes)(a.id, b.id);
|
|
54
|
+
}
|
|
55
|
+
function matchesQuery(record, query) {
|
|
56
|
+
if (query.app && !(record.appId || record.workflowId || "").toLowerCase().includes(query.app))
|
|
57
|
+
return false;
|
|
58
|
+
if (query.status && record.lifecycle !== query.status && record.derivedLifecycle !== query.status)
|
|
59
|
+
return false;
|
|
60
|
+
if (query.repo && node_path_1.default.resolve(record.repo) !== query.repo)
|
|
61
|
+
return false;
|
|
62
|
+
if (query.since && record.createdAt < query.since)
|
|
63
|
+
return false;
|
|
64
|
+
if (query.until && record.createdAt > query.until)
|
|
65
|
+
return false;
|
|
66
|
+
if (query.text) {
|
|
67
|
+
const haystack = [
|
|
68
|
+
record.runId,
|
|
69
|
+
record.appId,
|
|
70
|
+
record.workflowId,
|
|
71
|
+
record.title,
|
|
72
|
+
record.repo,
|
|
73
|
+
record.lifecycle,
|
|
74
|
+
record.loopStage,
|
|
75
|
+
record.inputsDigest
|
|
76
|
+
]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.join(" ")
|
|
79
|
+
.toLowerCase();
|
|
80
|
+
if (!haystack.includes(query.text))
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/** Bounded, deterministic stringification of run inputs for free-text search.
|
|
86
|
+
* Descriptive intent keys (question, prompt, ...) come first so they survive
|
|
87
|
+
* truncation; the rest follow alphabetically. Deterministic and compact. */
|
|
88
|
+
const DIGEST_PRIORITY_KEYS = ["question", "prompt", "task", "summary", "title", "objective", "focus", "topic"];
|
|
89
|
+
/** Distinct execution backends used by a run's dispatches/tasks, recomputed from
|
|
90
|
+
* source state. Sorted; empty for pre-v0.1.29 / default-only runs that never
|
|
91
|
+
* recorded a backend. The registry stays backend-agnostic — this is metadata. */
|
|
92
|
+
function distinctBackends(run) {
|
|
93
|
+
const backends = new Set();
|
|
94
|
+
for (const dispatch of run.dispatches || []) {
|
|
95
|
+
if (dispatch.backendId)
|
|
96
|
+
backends.add(dispatch.backendId);
|
|
97
|
+
}
|
|
98
|
+
for (const task of run.tasks || []) {
|
|
99
|
+
if (task.backendId)
|
|
100
|
+
backends.add(task.backendId);
|
|
101
|
+
}
|
|
102
|
+
return [...backends].sort();
|
|
103
|
+
}
|
|
104
|
+
function digestInputs(inputs) {
|
|
105
|
+
if (!inputs || typeof inputs !== "object")
|
|
106
|
+
return undefined;
|
|
107
|
+
const keys = Object.keys(inputs);
|
|
108
|
+
const ordered = [
|
|
109
|
+
...DIGEST_PRIORITY_KEYS.filter((k) => keys.includes(k)),
|
|
110
|
+
...keys.filter((k) => !DIGEST_PRIORITY_KEYS.includes(k)).sort()
|
|
111
|
+
];
|
|
112
|
+
const parts = [];
|
|
113
|
+
for (const key of ordered) {
|
|
114
|
+
const value = inputs[key];
|
|
115
|
+
if (value === undefined || value === null)
|
|
116
|
+
continue;
|
|
117
|
+
const rendered = Array.isArray(value) ? value.join(",") : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
118
|
+
parts.push(`${key}=${rendered}`);
|
|
119
|
+
}
|
|
120
|
+
const joined = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
121
|
+
return joined.length > 360 ? `${joined.slice(0, 357)}...` : joined;
|
|
122
|
+
}
|
|
123
|
+
function countRecords(records) {
|
|
124
|
+
const counts = {
|
|
125
|
+
total: records.length,
|
|
126
|
+
queued: 0,
|
|
127
|
+
running: 0,
|
|
128
|
+
blocked: 0,
|
|
129
|
+
completed: 0,
|
|
130
|
+
failed: 0,
|
|
131
|
+
archived: 0,
|
|
132
|
+
reclaimed: 0
|
|
133
|
+
};
|
|
134
|
+
for (const record of records) {
|
|
135
|
+
counts[record.lifecycle] = (counts[record.lifecycle] || 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return counts;
|
|
138
|
+
}
|
|
139
|
+
function optionalLower(value) {
|
|
140
|
+
if (value === undefined || value === null || value === "")
|
|
141
|
+
return undefined;
|
|
142
|
+
return String(value).toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
function clampInt(value, fallback, min) {
|
|
145
|
+
const n = Number(value);
|
|
146
|
+
if (!Number.isFinite(n))
|
|
147
|
+
return fallback;
|
|
148
|
+
return Math.max(min, Math.floor(n));
|
|
149
|
+
}
|
|
150
|
+
let queueCounter = 0;
|
|
151
|
+
function queueId() {
|
|
152
|
+
queueCounter += 1;
|
|
153
|
+
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
154
|
+
return `q-${stamp}-${String(queueCounter).padStart(3, "0")}`;
|
|
155
|
+
}
|
|
156
|
+
function isRunLifecycleState(value) {
|
|
157
|
+
return typeof value === "string" && exports.LIFECYCLE_STATES.includes(value);
|
|
158
|
+
}
|
|
159
|
+
/** Read a run dir's `reclaimed.json` overlay (v0.1.39). Fail-closed to an empty
|
|
160
|
+
* chain on absence/corruption — a malformed overlay must never brick the run. */
|
|
161
|
+
function loadReclaimedFromDir(runDir) {
|
|
162
|
+
const file = node_path_1.default.join(runDir, "reclaimed.json");
|
|
163
|
+
if (!node_fs_1.default.existsSync(file))
|
|
164
|
+
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(node_fs_1.default.readFileSync(file, "utf8"));
|
|
167
|
+
return { schemaVersion: 1, runId: parsed.runId || "", tombstones: Array.isArray(parsed.tombstones) ? parsed.tombstones : [] };
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return { schemaVersion: 1, runId: "", tombstones: [] };
|
|
171
|
+
}
|
|
172
|
+
}
|