cool-workflow 0.1.98 → 0.2.1
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 +18 -7
- package/apps/architecture-review/app.json +2 -2
- package/apps/architecture-review/workflow.js +12 -12
- 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/cli/dispatch.js +236 -0
- package/dist/cli/entry.js +120 -0
- package/dist/cli/io.js +21 -4
- package/dist/cli/parseargv.js +157 -0
- package/dist/cli.js +6 -33
- package/dist/core/capability-table.js +3518 -0
- package/dist/core/format/help.js +314 -0
- package/dist/{state-explosion/format.js → core/format/state-explosion-text.js} +10 -1
- package/dist/core/hash.js +137 -0
- package/dist/core/multi-agent/candidate-scoring.js +219 -0
- package/dist/core/multi-agent/collaboration.js +481 -0
- package/dist/core/multi-agent/coordinator.js +515 -0
- package/dist/core/multi-agent/eval-replay.js +306 -0
- package/dist/core/multi-agent/runtime.js +929 -0
- package/dist/core/multi-agent/topology.js +298 -0
- package/dist/core/multi-agent/trust-policy.js +197 -0
- package/dist/core/pipeline/commit-gate.js +320 -0
- package/dist/{pipeline-contract.js → core/pipeline/contract.js} +24 -37
- package/dist/core/pipeline/dispatch.js +103 -0
- package/dist/core/pipeline/drive-decide.js +227 -0
- package/dist/core/pipeline/error-feedback.js +161 -0
- package/dist/core/pipeline/loop-expansion.js +124 -0
- package/dist/{result-normalize.js → core/pipeline/result-normalize.js} +14 -37
- package/dist/core/pipeline/runner.js +230 -0
- package/dist/{contract-migration.js → core/state/contract-migration.js} +68 -92
- package/dist/{state-migrations.js → core/state/migrations.js} +103 -96
- package/dist/core/state/node-projection.js +95 -0
- package/dist/core/state/node-snapshot.js +230 -0
- package/dist/core/state/run-paths.js +93 -0
- package/dist/{schema-validate.js → core/state/schema-validate.js} +35 -28
- package/dist/core/state/schema.js +50 -0
- package/dist/core/state/state-explosion/digest.js +243 -0
- package/dist/core/state/state-explosion/graph.js +527 -0
- package/dist/{state-explosion → core/state/state-explosion}/helpers.js +34 -3
- package/dist/core/state/state-explosion/report.js +187 -0
- package/dist/{state-explosion → core/state/state-explosion}/size.js +25 -7
- package/dist/{state-node.js → core/state/state-node.js} +90 -113
- package/dist/core/state/types.js +19 -0
- package/dist/{validation.js → core/state/validation.js} +64 -131
- package/dist/core/trust/evidence-grounding.js +134 -0
- package/dist/core/trust/ledger.js +199 -0
- package/dist/{telemetry-attestation.js → core/trust/telemetry-attestation.js} +94 -68
- package/dist/core/trust/telemetry-ledger.js +127 -0
- package/dist/core/types.js +20 -0
- package/dist/core/version.js +16 -0
- package/dist/{workflow-app-framework.js → core/workflow-apps/app-schema.js} +337 -426
- package/dist/mcp/dispatch.js +104 -0
- package/dist/mcp/server.js +144 -0
- package/dist/mcp-server.js +6 -84
- package/dist/{agent-config.js → shell/agent-config.js} +118 -71
- package/dist/shell/app-run-cli.js +88 -0
- package/dist/shell/audit-cli.js +259 -0
- package/dist/shell/audit-provenance.js +83 -0
- package/dist/shell/candidate-scoring-io.js +459 -0
- package/dist/shell/collaboration-io.js +264 -0
- package/dist/shell/commit-summary.js +104 -0
- package/dist/shell/commit.js +290 -0
- package/dist/shell/coordinator-io.js +476 -0
- package/dist/shell/demo-cli.js +19 -0
- package/dist/shell/dispatch.js +162 -0
- package/dist/shell/doctor.js +292 -0
- package/dist/shell/drive.js +885 -0
- package/dist/shell/error-feedback-io.js +305 -0
- package/dist/shell/eval-io.js +473 -0
- package/dist/{multi-agent-eval/format.js → shell/eval-text.js} +78 -49
- package/dist/{evidence-reasoning.js → shell/evidence-reasoning.js} +124 -255
- package/dist/shell/exec-backend-cli.js +88 -0
- package/dist/shell/execution-backend/agent.js +507 -0
- package/dist/shell/execution-backend/ci.js +15 -0
- package/dist/shell/execution-backend/container.js +69 -0
- package/dist/shell/execution-backend/envelopes.js +55 -0
- package/dist/shell/execution-backend/local.js +113 -0
- package/dist/shell/execution-backend/probes.js +175 -0
- package/dist/shell/execution-backend/registry.js +402 -0
- package/dist/shell/execution-backend/remote.js +128 -0
- package/dist/shell/execution-backend/types.js +11 -0
- package/dist/shell/feedback-cli.js +81 -0
- package/dist/shell/feedback-operations.js +48 -0
- package/dist/shell/fs-atomic.js +276 -0
- package/dist/shell/harness.js +98 -0
- package/dist/shell/ledger-cli.js +212 -0
- package/dist/shell/ledger-io.js +169 -0
- package/dist/shell/man-cli.js +89 -0
- package/dist/shell/metrics-cli.js +100 -0
- package/dist/shell/multi-agent-cli.js +1002 -0
- package/dist/shell/multi-agent-host.js +563 -0
- package/dist/shell/multi-agent-io.js +387 -0
- package/dist/{multi-agent-operator-ux.js → shell/multi-agent-operator-ux.js} +234 -191
- package/dist/shell/node-store.js +124 -0
- package/dist/{observability/format.js → shell/observability-format.js} +7 -1
- package/dist/{observability/intake.js → shell/observability-intake.js} +79 -56
- package/dist/{observability.js → shell/observability.js} +159 -332
- package/dist/{onramp.js → shell/onramp.js} +11 -0
- package/dist/{operator-ux/format.js → shell/operator-ux-text.js} +250 -239
- package/dist/shell/operator-ux.js +431 -0
- package/dist/shell/orchestrator.js +231 -0
- package/dist/shell/pipeline-cli.js +667 -0
- package/dist/shell/pipeline.js +217 -0
- package/dist/shell/reclamation-io.js +1366 -0
- package/dist/shell/registry-cli.js +344 -0
- package/dist/{remote-source.js → shell/remote-source.js} +2 -2
- package/dist/shell/report-cli.js +101 -0
- package/dist/shell/report-view-cli.js +117 -0
- package/dist/{orchestrator → shell}/report.js +289 -282
- package/dist/shell/reporter.js +62 -0
- package/dist/shell/run-export-cli.js +106 -0
- package/dist/shell/run-export.js +680 -0
- package/dist/shell/run-registry-io.js +1014 -0
- package/dist/shell/run-store.js +164 -0
- package/dist/{sandbox-profile.js → shell/sandbox-profile.js} +134 -95
- package/dist/{scheduler.js → shell/scheduler-io.js} +257 -48
- package/dist/shell/scheduling-io.js +321 -0
- package/dist/shell/state-cli.js +181 -0
- package/dist/shell/state-explosion-cli.js +197 -0
- package/dist/shell/telemetry-cli.js +85 -0
- package/dist/{telemetry-demo.js → shell/telemetry-demo.js} +149 -119
- package/dist/shell/telemetry-ledger-io.js +132 -0
- package/dist/{term.js → shell/term.js} +36 -46
- package/dist/shell/topology-io.js +361 -0
- package/dist/shell/trust-audit.js +472 -0
- package/dist/{multi-agent-trust.js → shell/trust-policy-io.js} +67 -205
- package/dist/shell/verifier.js +48 -0
- package/dist/shell/workbench-host.js +258 -0
- package/dist/shell/workbench-text.js +18 -0
- package/dist/shell/workbench.js +170 -0
- package/dist/shell/worker-cli.js +124 -0
- package/dist/shell/worker-isolation.js +852 -0
- package/dist/shell/workflow-app-loader.js +650 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/cli-mcp-parity.7.md +282 -219
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +6 -1
- package/docs/project-index.md +137 -71
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/remote-source-review.7.md +4 -4
- package/docs/report-verifiable-bundle.7.md +1 -1
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +25 -0
- package/docs/security-trust-hardening.7.md +3 -1
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +15 -1
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +9 -13
- package/package.json +1 -1
- package/scripts/agents/agent-adapter-core.js +22 -2
- package/scripts/agents/claude-p-agent.js +8 -3
- package/scripts/agents/cw-attest-wrap.js +2 -2
- package/scripts/agents/gemini-agent.js +5 -1
- package/scripts/agents/opencode-agent.js +5 -1
- package/scripts/bump-version.js +4 -3
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-architecture-review.js +2 -3
- package/scripts/dogfood-release.js +3 -3
- package/scripts/gen-parity-doc.js +15 -2
- package/scripts/golden-path.js +4 -4
- package/scripts/onramp-check.js +1 -1
- package/scripts/parity-check.js +38 -21
- package/scripts/release-flow.js +2 -2
- package/scripts/sync-project-index.js +51 -27
- package/scripts/validate-run-state-schema.js +2 -2
- package/scripts/version-sync-check.js +30 -30
- package/dist/candidate-scoring.js +0 -729
- package/dist/capability-core.js +0 -1189
- package/dist/capability-registry.js +0 -885
- package/dist/cli/command-surface.js +0 -494
- package/dist/cli/format.js +0 -56
- package/dist/cli/handlers/audit.js +0 -82
- package/dist/cli/handlers/blackboard.js +0 -81
- package/dist/cli/handlers/candidate.js +0 -40
- package/dist/cli/handlers/clones.js +0 -34
- package/dist/cli/handlers/collaboration.js +0 -61
- package/dist/cli/handlers/eval.js +0 -40
- package/dist/cli/handlers/ledger.js +0 -169
- package/dist/cli/handlers/maintenance.js +0 -107
- package/dist/cli/handlers/multi-agent.js +0 -165
- package/dist/cli/handlers/node.js +0 -41
- package/dist/cli/handlers/operational.js +0 -155
- package/dist/cli/handlers/operator.js +0 -146
- package/dist/cli/handlers/registry.js +0 -68
- package/dist/cli/handlers/run.js +0 -153
- package/dist/cli/handlers/scheduling.js +0 -132
- package/dist/cli/handlers/workbench.js +0 -41
- package/dist/cli/handlers/worker.js +0 -45
- package/dist/cli/run-summary.js +0 -45
- package/dist/clones.js +0 -162
- package/dist/collaboration.js +0 -726
- package/dist/commit.js +0 -592
- package/dist/compare.js +0 -18
- package/dist/coordinator/classify.js +0 -45
- package/dist/coordinator/paths.js +0 -42
- package/dist/coordinator/util.js +0 -126
- package/dist/coordinator.js +0 -990
- package/dist/daemon.js +0 -44
- package/dist/dispatch.js +0 -250
- package/dist/doctor.js +0 -212
- package/dist/drive.js +0 -864
- package/dist/error-feedback.js +0 -419
- package/dist/evidence-grounding.js +0 -184
- package/dist/execution-backend/agent.js +0 -354
- package/dist/execution-backend/probes.js +0 -112
- package/dist/execution-backend/util.js +0 -47
- package/dist/execution-backend.js +0 -961
- package/dist/gates.js +0 -48
- package/dist/harness.js +0 -61
- package/dist/ledger.js +0 -313
- package/dist/loop-expansion.js +0 -60
- package/dist/mcp/tool-call.js +0 -470
- package/dist/mcp/tool-definitions.js +0 -1066
- package/dist/mcp-surface.js +0 -30
- package/dist/multi-agent/graph.js +0 -84
- package/dist/multi-agent/helpers.js +0 -141
- package/dist/multi-agent/ids.js +0 -20
- package/dist/multi-agent/paths.js +0 -22
- package/dist/multi-agent-eval/normalize.js +0 -51
- package/dist/multi-agent-eval.js +0 -678
- package/dist/multi-agent-host.js +0 -777
- package/dist/multi-agent.js +0 -984
- package/dist/node-projection.js +0 -59
- package/dist/node-snapshot.js +0 -260
- package/dist/operator-ux.js +0 -631
- package/dist/orchestrator/app-operations.js +0 -211
- package/dist/orchestrator/audit-operations.js +0 -182
- package/dist/orchestrator/candidate-operations.js +0 -117
- package/dist/orchestrator/cli-options.js +0 -294
- package/dist/orchestrator/collaboration-operations.js +0 -86
- package/dist/orchestrator/feedback-operations.js +0 -81
- package/dist/orchestrator/host-operations.js +0 -78
- package/dist/orchestrator/lifecycle-operations.js +0 -650
- package/dist/orchestrator/migration-operations.js +0 -44
- package/dist/orchestrator/multi-agent-operations.js +0 -362
- package/dist/orchestrator/topology-operations.js +0 -84
- package/dist/orchestrator.js +0 -925
- package/dist/pipeline-runner.js +0 -285
- package/dist/reclamation/hash.js +0 -72
- package/dist/reclamation.js +0 -812
- package/dist/reporter.js +0 -67
- package/dist/run-export.js +0 -815
- package/dist/run-registry/derive.js +0 -175
- package/dist/run-registry/format.js +0 -124
- package/dist/run-registry/gc.js +0 -251
- package/dist/run-registry/policy.js +0 -16
- package/dist/run-registry/queue.js +0 -115
- package/dist/run-registry.js +0 -850
- package/dist/run-state-schema.js +0 -68
- package/dist/scheduling.js +0 -184
- package/dist/state-explosion.js +0 -1014
- package/dist/state.js +0 -367
- package/dist/telemetry-ledger.js +0 -196
- package/dist/topology.js +0 -565
- package/dist/triggers.js +0 -184
- package/dist/trust-audit.js +0 -644
- package/dist/types/blackboard.js +0 -2
- package/dist/types/candidate.js +0 -2
- package/dist/types/collaboration.js +0 -2
- package/dist/types/core.js +0 -2
- package/dist/types/drive.js +0 -10
- package/dist/types/error-feedback.js +0 -2
- package/dist/types/evidence-reasoning.js +0 -2
- package/dist/types/execution-backend.js +0 -2
- package/dist/types/multi-agent.js +0 -2
- package/dist/types/observability.js +0 -2
- package/dist/types/pipeline.js +0 -2
- package/dist/types/reclamation.js +0 -8
- package/dist/types/report-bundle.js +0 -6
- package/dist/types/result.js +0 -2
- package/dist/types/run-registry.js +0 -2
- package/dist/types/run.js +0 -2
- package/dist/types/sandbox.js +0 -2
- package/dist/types/schedule.js +0 -2
- package/dist/types/state-node.js +0 -2
- package/dist/types/topology.js +0 -2
- package/dist/types/trust.js +0 -2
- package/dist/types/workbench.js +0 -2
- package/dist/types/worker.js +0 -2
- package/dist/types/workflow-app.js +0 -2
- package/dist/types.js +0 -44
- package/dist/util/fingerprint.js +0 -19
- package/dist/util/fingerprint.test.js +0 -27
- package/dist/verifier.js +0 -78
- package/dist/version.js +0 -8
- package/dist/workbench-host.js +0 -192
- package/dist/workbench.js +0 -192
- package/dist/worker-accept/acceptance.js +0 -114
- package/dist/worker-accept/blackboard-fanout.js +0 -80
- package/dist/worker-accept/blackboard-linkage.js +0 -19
- package/dist/worker-accept/context.js +0 -2
- package/dist/worker-accept/telemetry-ledger.js +0 -126
- package/dist/worker-accept/validation.js +0 -77
- package/dist/worker-accept/verifier-completion.js +0 -73
- package/dist/worker-isolation/helpers.js +0 -51
- package/dist/worker-isolation/paths.js +0 -46
- package/dist/worker-isolation.js +0 -656
- package/dist/workflow-api.js +0 -131
- /package/dist/{types → core/types}/boundary.js +0 -0
package/dist/run-export.js
DELETED
|
@@ -1,815 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Run Export / Import — portable run archive format (Track B).
|
|
3
|
-
//
|
|
4
|
-
// BSD discipline: explicit state, portable format. Export serializes a run plus
|
|
5
|
-
// its run-local files (artifacts, audit overlays, telemetry ledger, reports,
|
|
6
|
-
// worker files, commit snapshots) to a single JSON archive. Import restores those
|
|
7
|
-
// bytes into a new .cw/runs/<id>/ tree, rebases paths, writes a restore manifest,
|
|
8
|
-
// and exposes a deterministic verification pass. No hidden database, no trust in
|
|
9
|
-
// paths from the archive without containment checks.
|
|
10
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
-
};
|
|
13
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.exportRun = exportRun;
|
|
15
|
-
exports.importRun = importRun;
|
|
16
|
-
exports.verifyImportedRun = verifyImportedRun;
|
|
17
|
-
exports.inspectArchive = inspectArchive;
|
|
18
|
-
exports.importManifestPath = importManifestPath;
|
|
19
|
-
exports.verifyReportBundle = verifyReportBundle;
|
|
20
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
22
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
24
|
-
const state_1 = require("./state");
|
|
25
|
-
const version_1 = require("./version");
|
|
26
|
-
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
27
|
-
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
28
|
-
const trust_audit_1 = require("./trust-audit");
|
|
29
|
-
const execution_backend_1 = require("./execution-backend");
|
|
30
|
-
const compare_1 = require("./compare");
|
|
31
|
-
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
32
|
-
function exportRun(run, outputPath, options = {}) {
|
|
33
|
-
const exportedAt = new Date().toISOString();
|
|
34
|
-
const files = collectArchiveFiles(run);
|
|
35
|
-
const manifestSha256 = digestManifest(files);
|
|
36
|
-
// Embed ONLY a public key. resolveTrustPublicKey normalizes an inline PEM or a
|
|
37
|
-
// file path down to inline PEM bytes so the archive is self-contained; a value
|
|
38
|
-
// that resolves to nothing (bad path) yields no trust block rather than a throw.
|
|
39
|
-
const trustPublicKeyPem = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.trustPublicKey);
|
|
40
|
-
const exported = {
|
|
41
|
-
schemaVersion: 1,
|
|
42
|
-
exportedAt,
|
|
43
|
-
sourceVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION,
|
|
44
|
-
run,
|
|
45
|
-
files,
|
|
46
|
-
integrity: {
|
|
47
|
-
fileCount: files.length,
|
|
48
|
-
manifestSha256
|
|
49
|
-
},
|
|
50
|
-
...(trustPublicKeyPem ? { trust: { publicKeyPem: trustPublicKeyPem, algorithm: "ed25519" } } : {}),
|
|
51
|
-
// Legacy field retained so old readers still find an artifact-ish list.
|
|
52
|
-
artifacts: files
|
|
53
|
-
.filter((file) => file.role === "artifact")
|
|
54
|
-
.map((file) => ({
|
|
55
|
-
path: file.relativePath,
|
|
56
|
-
contentBase64: file.contentBase64,
|
|
57
|
-
sha256: file.sha256,
|
|
58
|
-
sizeBytes: file.sizeBytes
|
|
59
|
-
})),
|
|
60
|
-
audit: files.filter((file) => file.role === "audit").map((file) => file.relativePath)
|
|
61
|
-
};
|
|
62
|
-
(0, state_1.writeJson)(outputPath, exported);
|
|
63
|
-
const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(outputPath));
|
|
64
|
-
return {
|
|
65
|
-
runId: run.id,
|
|
66
|
-
exportedAt,
|
|
67
|
-
path: outputPath,
|
|
68
|
-
taskCount: run.tasks.length,
|
|
69
|
-
commitCount: run.commits.length,
|
|
70
|
-
fileCount: files.length,
|
|
71
|
-
artifactCount: files.filter((file) => file.role === "artifact").length,
|
|
72
|
-
auditFileCount: files.filter((file) => file.role === "audit").length,
|
|
73
|
-
telemetryIncluded: files.some((file) => file.role === "telemetry"),
|
|
74
|
-
trustKeyEmbedded: Boolean(trustPublicKeyPem),
|
|
75
|
-
manifestSha256,
|
|
76
|
-
archiveSha256
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/** Import a run from a portable JSON file into a target directory.
|
|
80
|
-
* Rebuilds run paths relative to the target dir. */
|
|
81
|
-
function importRun(exportPath, targetDir) {
|
|
82
|
-
const raw = (0, state_1.readJson)(exportPath);
|
|
83
|
-
if (raw.schemaVersion !== 1)
|
|
84
|
-
throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
|
|
85
|
-
const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
|
|
86
|
-
const files = normalizeArchiveFiles(raw);
|
|
87
|
-
verifyArchiveFileDigests(files, raw.integrity);
|
|
88
|
-
if (!raw.run || typeof raw.run !== "object") {
|
|
89
|
-
throw new Error("Invalid run export: missing run object");
|
|
90
|
-
}
|
|
91
|
-
// The run id from the archive becomes a directory name under the target's
|
|
92
|
-
// runs root; a crafted id like "../../etc" would otherwise escape it. Refuse
|
|
93
|
-
// any id that is not a single safe path segment, then assert containment as
|
|
94
|
-
// defense-in-depth (catches a symlinked runs root too) before any write.
|
|
95
|
-
const runId = (0, state_1.assertSafeRunId)(raw.run.id);
|
|
96
|
-
const runsRoot = node_path_1.default.join(targetDir, ".cw", "runs");
|
|
97
|
-
const runDir = node_path_1.default.join(runsRoot, runId);
|
|
98
|
-
if (!(0, state_1.isContainedPath)(runDir, runsRoot)) {
|
|
99
|
-
throw new Error(`Run id escapes the runs directory: ${JSON.stringify(raw.run.id)}`);
|
|
100
|
-
}
|
|
101
|
-
const oldRunDir = raw.run.paths.runDir;
|
|
102
|
-
const oldCwd = raw.run.cwd;
|
|
103
|
-
const paths = (0, state_1.createRunPaths)(runDir);
|
|
104
|
-
(0, state_1.ensureRunDirs)(paths);
|
|
105
|
-
for (const file of files) {
|
|
106
|
-
const destination = node_path_1.default.join(runDir, file.relativePath);
|
|
107
|
-
if (!(0, state_1.isContainedPath)(destination, runDir)) {
|
|
108
|
-
throw new Error(`Archive file escapes restore directory: ${file.relativePath}`);
|
|
109
|
-
}
|
|
110
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
|
|
111
|
-
node_fs_1.default.writeFileSync(destination, decodeBase64Strict(file.contentBase64, file.relativePath));
|
|
112
|
-
}
|
|
113
|
-
const externalPathMap = new Map();
|
|
114
|
-
for (const file of files) {
|
|
115
|
-
if (file.sourcePath)
|
|
116
|
-
externalPathMap.set(file.sourcePath, node_path_1.default.join(runDir, file.relativePath));
|
|
117
|
-
}
|
|
118
|
-
const run = rebaseRun(raw.run, {
|
|
119
|
-
oldRunDir,
|
|
120
|
-
newRunDir: runDir,
|
|
121
|
-
oldCwd,
|
|
122
|
-
newCwd: targetDir,
|
|
123
|
-
paths,
|
|
124
|
-
externalPathMap
|
|
125
|
-
});
|
|
126
|
-
(0, state_1.saveCheckpoint)(run);
|
|
127
|
-
const manifest = {
|
|
128
|
-
schemaVersion: 1,
|
|
129
|
-
runId: run.id,
|
|
130
|
-
importedAt: new Date().toISOString(),
|
|
131
|
-
sourceVersion: raw.sourceVersion,
|
|
132
|
-
archiveSha256,
|
|
133
|
-
manifestSha256: digestManifest(files),
|
|
134
|
-
files: files.map(({ contentBase64: _contentBase64, ...file }) => file)
|
|
135
|
-
};
|
|
136
|
-
const manifestPath = importManifestPath(run);
|
|
137
|
-
(0, state_1.writeJson)(manifestPath, manifest, { durable: true });
|
|
138
|
-
const verification = verifyImportedRun(run);
|
|
139
|
-
return {
|
|
140
|
-
run,
|
|
141
|
-
runDir,
|
|
142
|
-
statePath: paths.state,
|
|
143
|
-
manifestPath,
|
|
144
|
-
verifyCommand: `cw run verify-import ${run.id} --cwd ${targetDir} --json`,
|
|
145
|
-
verification
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
/** Verify an imported run against its restore manifest and telemetry chain. */
|
|
149
|
-
function verifyImportedRun(run) {
|
|
150
|
-
const manifestPath = importManifestPath(run);
|
|
151
|
-
const checks = [];
|
|
152
|
-
if (!node_fs_1.default.existsSync(manifestPath)) {
|
|
153
|
-
return {
|
|
154
|
-
runId: run.id,
|
|
155
|
-
ok: false,
|
|
156
|
-
manifestPath,
|
|
157
|
-
checkedFiles: 0,
|
|
158
|
-
checks: [{ name: "import-manifest", pass: false, code: "missing-import-manifest", path: manifestPath }]
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
let manifest;
|
|
162
|
-
try {
|
|
163
|
-
manifest = (0, state_1.readJson)(manifestPath);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
return {
|
|
167
|
-
runId: run.id,
|
|
168
|
-
ok: false,
|
|
169
|
-
manifestPath,
|
|
170
|
-
checkedFiles: 0,
|
|
171
|
-
checks: [{ name: "import-manifest", pass: false, code: "invalid-import-manifest", path: manifestPath, actual: messageOf(error) }]
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const currentManifestDigest = digestManifest(manifest.files.map((file) => ({ ...file, contentBase64: "" })));
|
|
175
|
-
checks.push({
|
|
176
|
-
name: "import-manifest",
|
|
177
|
-
pass: manifest.runId === run.id && manifest.manifestSha256 === currentManifestDigest,
|
|
178
|
-
code: manifest.runId !== run.id ? "run-id-mismatch" : manifest.manifestSha256 === currentManifestDigest ? undefined : "manifest-digest-mismatch",
|
|
179
|
-
expected: manifest.manifestSha256,
|
|
180
|
-
actual: currentManifestDigest
|
|
181
|
-
});
|
|
182
|
-
let filesOk = true;
|
|
183
|
-
for (const file of manifest.files) {
|
|
184
|
-
const restoredPath = node_path_1.default.join(run.paths.runDir, file.relativePath);
|
|
185
|
-
if (!(0, state_1.isContainedPath)(restoredPath, run.paths.runDir)) {
|
|
186
|
-
filesOk = false;
|
|
187
|
-
checks.push({ name: "archive-file", pass: false, code: "path-escape", path: file.relativePath });
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
if (!node_fs_1.default.existsSync(restoredPath)) {
|
|
191
|
-
filesOk = false;
|
|
192
|
-
checks.push({ name: "archive-file", pass: false, code: "missing-file", path: file.relativePath, expected: file.sha256 });
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
const actual = sha256Bytes(node_fs_1.default.readFileSync(restoredPath));
|
|
196
|
-
const pass = actual === file.sha256;
|
|
197
|
-
if (!pass)
|
|
198
|
-
filesOk = false;
|
|
199
|
-
checks.push({
|
|
200
|
-
name: "archive-file",
|
|
201
|
-
pass,
|
|
202
|
-
code: pass ? undefined : "digest-mismatch",
|
|
203
|
-
path: file.relativePath,
|
|
204
|
-
expected: file.sha256,
|
|
205
|
-
actual
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
checks.push({ name: "archive-files", pass: filesOk, code: filesOk ? undefined : "archive-files-invalid" });
|
|
209
|
-
const telemetry = (0, telemetry_ledger_1.verifyTelemetryLedger)(run);
|
|
210
|
-
checks.push({
|
|
211
|
-
name: "telemetry-ledger",
|
|
212
|
-
pass: telemetry.verified,
|
|
213
|
-
code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
|
|
214
|
-
});
|
|
215
|
-
// Re-prove the trust-audit hash chain on restore too. Telemetry was already
|
|
216
|
-
// re-proven above, but the decisions/sandbox/commit-gate audit chain — also
|
|
217
|
-
// exported under audit/ — was not, an asymmetry a tampered restore could slip
|
|
218
|
-
// through. An absent chain is verified:true (nothing to prove), so archives
|
|
219
|
-
// predating audit export append a PASSING check — no false-red.
|
|
220
|
-
const audit = (0, trust_audit_1.verifyTrustAudit)(run);
|
|
221
|
-
checks.push({
|
|
222
|
-
name: "trust-audit",
|
|
223
|
-
pass: audit.verified,
|
|
224
|
-
code: audit.verified ? undefined : "trust-audit-invalid"
|
|
225
|
-
});
|
|
226
|
-
return {
|
|
227
|
-
runId: run.id,
|
|
228
|
-
ok: checks.every((check) => check.pass),
|
|
229
|
-
manifestPath,
|
|
230
|
-
checkedFiles: manifest.files.length,
|
|
231
|
-
checks
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/** Read-only integrity inspection of a portable archive WITHOUT importing it. Never
|
|
235
|
-
* throws — a read error, bad JSON, unsupported schema, or any digest/size/count/
|
|
236
|
-
* manifest mismatch is reported as a structured check with ok:false. Writes nothing. */
|
|
237
|
-
function inspectArchive(archivePath) {
|
|
238
|
-
const base = {
|
|
239
|
-
schemaVersion: 1,
|
|
240
|
-
archivePath,
|
|
241
|
-
ok: false,
|
|
242
|
-
schemaSupported: false,
|
|
243
|
-
runId: null,
|
|
244
|
-
fileCount: 0,
|
|
245
|
-
manifestSha256: null,
|
|
246
|
-
archiveSha256: null,
|
|
247
|
-
checks: []
|
|
248
|
-
};
|
|
249
|
-
let bytes;
|
|
250
|
-
try {
|
|
251
|
-
bytes = node_fs_1.default.readFileSync(archivePath);
|
|
252
|
-
}
|
|
253
|
-
catch (error) {
|
|
254
|
-
return { ...base, checks: [{ name: "archive", pass: false, code: "archive-unreadable", path: archivePath, actual: messageOf(error) }] };
|
|
255
|
-
}
|
|
256
|
-
base.archiveSha256 = sha256Bytes(bytes);
|
|
257
|
-
let raw;
|
|
258
|
-
try {
|
|
259
|
-
raw = JSON.parse(bytes.toString("utf8"));
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
return { ...base, checks: [{ name: "archive", pass: false, code: "archive-invalid-json", path: archivePath, actual: messageOf(error) }] };
|
|
263
|
-
}
|
|
264
|
-
if (raw.schemaVersion !== 1) {
|
|
265
|
-
return {
|
|
266
|
-
...base,
|
|
267
|
-
schemaVersion: typeof raw.schemaVersion === "number" ? raw.schemaVersion : base.schemaVersion,
|
|
268
|
-
checks: [{ name: "schema", pass: false, code: "unsupported-schema", expected: "1", actual: String(raw.schemaVersion) }]
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const files = normalizeArchiveFiles(raw);
|
|
273
|
-
const { checks } = collectArchiveDigestChecks(files, raw.integrity);
|
|
274
|
-
// Faithful preview of what `run import` would do under the same env: with
|
|
275
|
-
// CW_REQUIRE_ARCHIVE_INTEGRITY=1 a stripped-integrity archive is refused by
|
|
276
|
-
// import, so inspect must also report it as failing (ok:false / exit 1) rather
|
|
277
|
-
// than green — otherwise inspect-before-import is misleading in that policy.
|
|
278
|
-
// Default (env unset) is unchanged: inspection only reports the digest checks.
|
|
279
|
-
if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
280
|
-
checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
schemaVersion: 1,
|
|
284
|
-
archivePath,
|
|
285
|
-
ok: checks.every((c) => c.pass),
|
|
286
|
-
schemaSupported: true,
|
|
287
|
-
runId: raw.run && raw.run.id ? raw.run.id : null,
|
|
288
|
-
fileCount: files.length,
|
|
289
|
-
manifestSha256: raw.integrity ? digestManifest(files) : null,
|
|
290
|
-
archiveSha256: base.archiveSha256,
|
|
291
|
-
checks
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
return { ...base, schemaSupported: true, checks: [{ name: "archive", pass: false, code: "archive-malformed", path: archivePath, actual: messageOf(error) }] };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
function importManifestPath(run) {
|
|
299
|
-
return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
|
|
300
|
-
}
|
|
301
|
-
/** Verify a portable run bundle OFFLINE and SELF-CONTAINED: prove the archive bytes,
|
|
302
|
-
* the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded
|
|
303
|
-
* public key) the ed25519 signatures — WITHOUT a source repo, a pre-existing .cw
|
|
304
|
-
* tree, or an out-of-band key. Reuses the existing import + restore-verify path: it
|
|
305
|
-
* restores into an auto-cleaned tmpdir so a stranger's machine is left untouched.
|
|
306
|
-
* Never throws — every failure is a structured check and a false `ok` (exit-1 worthy).
|
|
307
|
-
*
|
|
308
|
-
* Key precedence is bundle > argument > environment, so the artifact verifies the
|
|
309
|
-
* same on any machine; only when the bundle omits a key do the override/env apply. */
|
|
310
|
-
/** True when report.md embeds `expected` (the trimmed result) at the task's OWN
|
|
311
|
-
* section, exactly as orchestrator/report.ts renderResults emits it: a
|
|
312
|
-
* `### <taskId>` heading, a `Result: <path>` line, then the result body — and the
|
|
313
|
-
* body STARTS WITH `expected`. Anchoring to the section (not a whole-file substring)
|
|
314
|
-
* means a decoy copy buried elsewhere does not satisfy it; matching from the heading
|
|
315
|
-
* forward (rather than to the next heading) means a result body that itself contains
|
|
316
|
-
* `###` cannot break the bound. The `Result:` path is matched loosely since it is
|
|
317
|
-
* host-specific (rebased on import). */
|
|
318
|
-
function reportSectionEmbedsResult(reportMd, taskId, expected) {
|
|
319
|
-
const needle = `### ${taskId}\n`;
|
|
320
|
-
// Walk EVERY `### <taskId>` occurrence — not just the first — so a stray heading
|
|
321
|
-
// inside an earlier task's result body (which is not followed by the `Result:`
|
|
322
|
-
// structure) is skipped rather than mis-anchoring the check (a false positive on a
|
|
323
|
-
// legitimate, fully-signed bundle whose findings contain markdown headings).
|
|
324
|
-
for (let from = reportMd.indexOf(needle); from >= 0; from = reportMd.indexOf(needle, from + 1)) {
|
|
325
|
-
const after = reportMd.slice(from);
|
|
326
|
-
const prefix = after.match(/^### [^\n]*\n\nResult: [^\n]*\n\n/);
|
|
327
|
-
if (prefix && after.slice(prefix[0].length).startsWith(expected))
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
function verifyReportBundle(archivePath, options = {}) {
|
|
333
|
-
const inspect = inspectArchive(archivePath);
|
|
334
|
-
const failedChecks = inspect.checks
|
|
335
|
-
.filter((check) => !check.pass)
|
|
336
|
-
.map((check) => ({ name: check.name, code: check.code }));
|
|
337
|
-
const base = {
|
|
338
|
-
schemaVersion: 1,
|
|
339
|
-
archivePath,
|
|
340
|
-
runId: inspect.runId,
|
|
341
|
-
ok: false,
|
|
342
|
-
archiveOk: inspect.ok,
|
|
343
|
-
telemetryVerified: false,
|
|
344
|
-
trustAuditVerified: false,
|
|
345
|
-
trustKeySource: "none",
|
|
346
|
-
signatureKeyProvided: false,
|
|
347
|
-
signaturesChecked: 0,
|
|
348
|
-
signaturesReverified: 0,
|
|
349
|
-
signaturesFailed: 0,
|
|
350
|
-
trustLevel: "unsigned",
|
|
351
|
-
reportFindingsVerified: false,
|
|
352
|
-
failedChecks
|
|
353
|
-
};
|
|
354
|
-
// A bundle that is not even a supported archive cannot be restored — report the
|
|
355
|
-
// inspection failure and stop (fail-closed, no tmpdir, nothing written).
|
|
356
|
-
if (!inspect.schemaSupported)
|
|
357
|
-
return base;
|
|
358
|
-
// Read the embedded trust key (and, if requested, the report bytes) straight from
|
|
359
|
-
// the archive JSON. inspectArchive already proved the bytes parse + digest-match.
|
|
360
|
-
let bundleKey;
|
|
361
|
-
let reportContent;
|
|
362
|
-
try {
|
|
363
|
-
const raw = JSON.parse(node_fs_1.default.readFileSync(archivePath, "utf8"));
|
|
364
|
-
bundleKey = raw.trust?.publicKeyPem;
|
|
365
|
-
if (options.extractReportTo) {
|
|
366
|
-
const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
|
|
367
|
-
if (reportFile) {
|
|
368
|
-
const decoded = decodeBase64StrictResult(reportFile.contentBase64, reportFile.relativePath);
|
|
369
|
-
if (decoded.ok)
|
|
370
|
-
reportContent = decoded.bytes.toString("utf8");
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
/* inspect already recorded the parse failure; treat key as absent */
|
|
376
|
-
}
|
|
377
|
-
const trustKeySource = bundleKey
|
|
378
|
-
? "bundle"
|
|
379
|
-
: options.pubkey
|
|
380
|
-
? "argument"
|
|
381
|
-
: process.env.CW_AGENT_ATTEST_PUBKEY
|
|
382
|
-
? "environment"
|
|
383
|
-
: "none";
|
|
384
|
-
const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
|
|
385
|
-
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-verify-bundle-"));
|
|
386
|
-
let telemetryVerified = false;
|
|
387
|
-
let trustAuditVerified = false;
|
|
388
|
-
let signaturesChecked = 0;
|
|
389
|
-
let signaturesReverified = 0;
|
|
390
|
-
let signaturesFailed = 0;
|
|
391
|
-
let signaturesResultBound = 0;
|
|
392
|
-
let reportFindingsOk = true;
|
|
393
|
-
let reportExtractedTo;
|
|
394
|
-
try {
|
|
395
|
-
// Restore into the throwaway tree. importRun digest-checks every file and throws
|
|
396
|
-
// on the first mismatch (or on a stripped integrity block under
|
|
397
|
-
// CW_REQUIRE_ARCHIVE_INTEGRITY=1) — caught below as a failed "restore" check.
|
|
398
|
-
const imported = importRun(archivePath, tmpDir);
|
|
399
|
-
for (const check of imported.verification.checks) {
|
|
400
|
-
if (check.name === "telemetry-ledger")
|
|
401
|
-
telemetryVerified = check.pass;
|
|
402
|
-
if (check.name === "trust-audit")
|
|
403
|
-
trustAuditVerified = check.pass;
|
|
404
|
-
if (!check.pass)
|
|
405
|
-
failedChecks.push({ name: check.name, code: check.code });
|
|
406
|
-
}
|
|
407
|
-
// Independent ed25519 re-verification over the restored ledger using the bundle's
|
|
408
|
-
// own key (or override/env). With no key, attested records degrade to
|
|
409
|
-
// informational (signaturesFailed stays 0); the chain check above still gates ok.
|
|
410
|
-
const ledger = (0, telemetry_ledger_1.verifyTelemetryLedger)(imported.run);
|
|
411
|
-
const sig = (0, telemetry_attestation_1.verifyTelemetrySignatures)(ledger.records, trustKey);
|
|
412
|
-
signaturesChecked = sig.checked;
|
|
413
|
-
signaturesReverified = sig.reverified;
|
|
414
|
-
signaturesFailed = sig.failed;
|
|
415
|
-
signaturesResultBound = sig.resultBound.length;
|
|
416
|
-
for (const check of sig.checks)
|
|
417
|
-
if (!check.pass)
|
|
418
|
-
failedChecks.push({ name: check.name, code: check.code });
|
|
419
|
-
// Report ⇄ result ⇄ signature cross-check — three links so the agent's findings
|
|
420
|
-
// cannot be altered undetected on a signed bundle. CRUCIALLY this is driven by
|
|
421
|
-
// sig.resultBound (the records whose signature actually COVERED the result
|
|
422
|
-
// digest), NOT the run.tasks list — run.tasks is in the archive but bound by
|
|
423
|
-
// nothing (not the manifest digest, the chain, or the signature), so iterating it
|
|
424
|
-
// would let an attacker silence the check by dropping/un-completing a task. The
|
|
425
|
-
// obligation comes from the chained+signed ledger record instead:
|
|
426
|
-
// 1. each bound record's resultDigest is anchored by the executor signature
|
|
427
|
-
// (coversResult re-verified above) — a 4-field signature is excluded, so an
|
|
428
|
-
// injected resultDigest is never trusted;
|
|
429
|
-
// 2. the matching completed task's RESTORED result file must hash to that signed
|
|
430
|
-
// digest (a missing/un-completed task or edited/empty result is a forgery);
|
|
431
|
-
// 3. report.md must embed that result at the task's own `### <taskId>` section.
|
|
432
|
-
// Editing the report breaks link 3; editing the result breaks link 2; editing both
|
|
433
|
-
// to one consistent lie still breaks link 2 (the signed digest does not move);
|
|
434
|
-
// dropping the task fails link 2 (the signed obligation remains).
|
|
435
|
-
const reportPath = imported.run.paths.report;
|
|
436
|
-
const reportMd = reportPath && node_fs_1.default.existsSync(reportPath) ? node_fs_1.default.readFileSync(reportPath, "utf8") : "";
|
|
437
|
-
const completedById = new Map(imported.run.tasks.filter((task) => task.status === "completed").map((task) => [task.id, task]));
|
|
438
|
-
for (const bound of sig.resultBound) {
|
|
439
|
-
const failBound = (code) => {
|
|
440
|
-
reportFindingsOk = false;
|
|
441
|
-
failedChecks.push({ name: "report-findings", code: `${code}:${bound.taskId}` });
|
|
442
|
-
};
|
|
443
|
-
const task = completedById.get(bound.taskId);
|
|
444
|
-
if (!task || !task.resultPath || !node_fs_1.default.existsSync(task.resultPath)) {
|
|
445
|
-
failBound("result-missing");
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
const resultRaw = node_fs_1.default.readFileSync(task.resultPath, "utf8");
|
|
449
|
-
if ((0, execution_backend_1.sha256)(resultRaw) !== bound.resultDigest) {
|
|
450
|
-
failBound("result-digest-mismatch");
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
if (!reportSectionEmbedsResult(reportMd, bound.taskId, resultRaw.trim())) {
|
|
454
|
-
failBound("report-result-mismatch");
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (options.extractReportTo && reportContent !== undefined) {
|
|
458
|
-
reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
|
|
459
|
-
if (options.cwd) {
|
|
460
|
-
const baseCwd = node_path_1.default.resolve(options.cwd);
|
|
461
|
-
if (!(0, state_1.isContainedPath)(reportExtractedTo, baseCwd)) {
|
|
462
|
-
failedChecks.push({ name: "extract-report", code: "path-outside-working-directory" });
|
|
463
|
-
reportExtractedTo = undefined;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (reportExtractedTo) {
|
|
467
|
-
node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
catch (error) {
|
|
472
|
-
failedChecks.push({ name: "restore", code: messageOf(error) });
|
|
473
|
-
}
|
|
474
|
-
finally {
|
|
475
|
-
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
476
|
-
}
|
|
477
|
-
// Strict mode: a bundle that claims attested telemetry but offers no key to check
|
|
478
|
-
// it is unverifiable — refuse rather than green it.
|
|
479
|
-
const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
|
|
480
|
-
if (strictShortfall)
|
|
481
|
-
failedChecks.push({ name: "signatures", code: "signature-key-required" });
|
|
482
|
-
// Trust level + requireSignatures — closes the prior fail-open. "signed" means the
|
|
483
|
-
// agent's SIGNED findings are present and unaltered: at least one result-COVERING
|
|
484
|
-
// signature re-verified against a key, none failed, and each signed result is
|
|
485
|
-
// faithfully embedded in report.md (the forward cross-check held). A usage-only
|
|
486
|
-
// (4-field) signature, an unverifiable one (no key), or a tampered signed finding
|
|
487
|
-
// all yield "unsigned". requireSignatures refuses "unsigned".
|
|
488
|
-
//
|
|
489
|
-
// SCOPE (honest): "signed" attests the SIGNED findings, NOT that the report is
|
|
490
|
-
// exhaustively signed. CW holds no key to sign the rendered report and the ledger
|
|
491
|
-
// chain is self-recomputable, so the report MAY carry additional unsigned content
|
|
492
|
-
// (prose, or extra sections), and a determined re-chainer can OMIT a signed
|
|
493
|
-
// finding. Verify findings against the signed results; full report-completeness
|
|
494
|
-
// needs an external anchor. See report-verifiable-bundle.7.md / trust-model.md.
|
|
495
|
-
const trustLevel = signaturesResultBound > 0 && signaturesFailed === 0 && reportFindingsOk ? "signed" : "unsigned";
|
|
496
|
-
const unsignedShortfall = Boolean(options.requireSignatures) && trustLevel === "unsigned";
|
|
497
|
-
if (unsignedShortfall)
|
|
498
|
-
failedChecks.push({ name: "signatures", code: "signatures-required" });
|
|
499
|
-
// Extraction was requested but could not be fulfilled (no report.md in the bundle,
|
|
500
|
-
// or the write failed): fail closed rather than silently green a missing artifact —
|
|
501
|
-
// otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
|
|
502
|
-
// nothing (or a stale file) with exit 0. A requested-but-absent output is a failure,
|
|
503
|
-
// not a no-op (distinct from extraction never being requested).
|
|
504
|
-
const extractShortfall = Boolean(options.extractReportTo) && !reportExtractedTo;
|
|
505
|
-
if (extractShortfall)
|
|
506
|
-
failedChecks.push({ name: "extract-report", code: "report-md-unavailable" });
|
|
507
|
-
return {
|
|
508
|
-
schemaVersion: 1,
|
|
509
|
-
archivePath,
|
|
510
|
-
runId: inspect.runId,
|
|
511
|
-
ok: inspect.ok &&
|
|
512
|
-
telemetryVerified &&
|
|
513
|
-
trustAuditVerified &&
|
|
514
|
-
signaturesFailed === 0 &&
|
|
515
|
-
reportFindingsOk &&
|
|
516
|
-
!strictShortfall &&
|
|
517
|
-
!extractShortfall &&
|
|
518
|
-
!unsignedShortfall,
|
|
519
|
-
archiveOk: inspect.ok,
|
|
520
|
-
telemetryVerified,
|
|
521
|
-
trustAuditVerified,
|
|
522
|
-
trustKeySource,
|
|
523
|
-
signatureKeyProvided: Boolean(trustKey),
|
|
524
|
-
signaturesChecked,
|
|
525
|
-
signaturesReverified,
|
|
526
|
-
signaturesFailed,
|
|
527
|
-
trustLevel,
|
|
528
|
-
reportFindingsVerified: reportFindingsOk,
|
|
529
|
-
reportExtractedTo,
|
|
530
|
-
failedChecks
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
function collectArchiveFiles(run) {
|
|
534
|
-
const entries = new Map();
|
|
535
|
-
for (const file of walkFiles(run.paths.runDir)) {
|
|
536
|
-
const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
|
|
537
|
-
if (!relativePath || relativePath === "state.json" || relativePath === "import-manifest.json" || relativePath.endsWith(".lock"))
|
|
538
|
-
continue;
|
|
539
|
-
addFile(entries, run, file, roleForRelativePath(relativePath));
|
|
540
|
-
}
|
|
541
|
-
for (const artifactPath of collectReferencedArtifactPaths(run)) {
|
|
542
|
-
if (!artifactPath || !node_fs_1.default.existsSync(artifactPath) || !node_fs_1.default.statSync(artifactPath).isFile())
|
|
543
|
-
continue;
|
|
544
|
-
if ((0, state_1.isContainedPath)(artifactPath, run.paths.runDir)) {
|
|
545
|
-
addFile(entries, run, artifactPath, "artifact");
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
|
|
549
|
-
addExternalArtifactFile(entries, run, artifactPath);
|
|
550
|
-
}
|
|
551
|
-
return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
|
|
552
|
-
}
|
|
553
|
-
function addFile(entries, run, file, role) {
|
|
554
|
-
const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
|
|
555
|
-
if (relativePath === "state.json" || relativePath === "import-manifest.json")
|
|
556
|
-
return;
|
|
557
|
-
if (!relativePath || relativePath.startsWith("../"))
|
|
558
|
-
return;
|
|
559
|
-
const bytes = node_fs_1.default.readFileSync(file);
|
|
560
|
-
entries.set(relativePath, {
|
|
561
|
-
relativePath,
|
|
562
|
-
role,
|
|
563
|
-
contentBase64: bytes.toString("base64"),
|
|
564
|
-
sha256: sha256Bytes(bytes),
|
|
565
|
-
sizeBytes: bytes.length
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
function addExternalArtifactFile(entries, run, file) {
|
|
569
|
-
const sourcePath = node_path_1.default.resolve(file);
|
|
570
|
-
const bytes = node_fs_1.default.readFileSync(sourcePath);
|
|
571
|
-
const relativePath = `external-artifacts/${sha256Bytes(Buffer.from(sourcePath, "utf8")).slice(0, 16)}-${safeArchiveBasename(node_path_1.default.basename(sourcePath))}`;
|
|
572
|
-
entries.set(relativePath, {
|
|
573
|
-
relativePath,
|
|
574
|
-
role: "artifact",
|
|
575
|
-
contentBase64: bytes.toString("base64"),
|
|
576
|
-
sha256: sha256Bytes(bytes),
|
|
577
|
-
sizeBytes: bytes.length,
|
|
578
|
-
sourcePath
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
function collectReferencedArtifactPaths(run) {
|
|
582
|
-
const paths = new Set();
|
|
583
|
-
for (const node of run.nodes || []) {
|
|
584
|
-
for (const artifact of node.artifacts || [])
|
|
585
|
-
addArtifactPath(paths, run, artifact.path);
|
|
586
|
-
}
|
|
587
|
-
for (const candidate of run.candidates || []) {
|
|
588
|
-
for (const artifact of candidate.artifacts || [])
|
|
589
|
-
addArtifactPath(paths, run, artifact.path);
|
|
590
|
-
}
|
|
591
|
-
for (const selection of run.candidateSelections || []) {
|
|
592
|
-
for (const artifact of selection.artifacts || [])
|
|
593
|
-
addArtifactPath(paths, run, artifact.path);
|
|
594
|
-
}
|
|
595
|
-
for (const artifact of run.blackboard?.artifacts || [])
|
|
596
|
-
addArtifactPath(paths, run, artifact.path);
|
|
597
|
-
return [...paths].sort();
|
|
598
|
-
}
|
|
599
|
-
function addArtifactPath(paths, run, value) {
|
|
600
|
-
if (!value)
|
|
601
|
-
return;
|
|
602
|
-
paths.add(node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(run.cwd, value));
|
|
603
|
-
}
|
|
604
|
-
function walkFiles(root) {
|
|
605
|
-
if (!node_fs_1.default.existsSync(root))
|
|
606
|
-
return [];
|
|
607
|
-
const found = [];
|
|
608
|
-
for (const name of node_fs_1.default.readdirSync(root)) {
|
|
609
|
-
const file = node_path_1.default.join(root, name);
|
|
610
|
-
const stat = node_fs_1.default.lstatSync(file);
|
|
611
|
-
if (stat.isSymbolicLink())
|
|
612
|
-
continue;
|
|
613
|
-
if (stat.isDirectory())
|
|
614
|
-
found.push(...walkFiles(file));
|
|
615
|
-
else if (stat.isFile())
|
|
616
|
-
found.push(file);
|
|
617
|
-
}
|
|
618
|
-
return found;
|
|
619
|
-
}
|
|
620
|
-
function roleForRelativePath(relativePath) {
|
|
621
|
-
if (relativePath === "telemetry.json")
|
|
622
|
-
return "telemetry";
|
|
623
|
-
if (relativePath === "audit" || relativePath.startsWith("audit/"))
|
|
624
|
-
return "audit";
|
|
625
|
-
if (relativePath === "artifacts" || relativePath.startsWith("artifacts/"))
|
|
626
|
-
return "artifact";
|
|
627
|
-
return "run-file";
|
|
628
|
-
}
|
|
629
|
-
function normalizeArchiveFiles(raw) {
|
|
630
|
-
const modern = raw.files || [];
|
|
631
|
-
if (modern.length) {
|
|
632
|
-
return modern.map((file) => ({
|
|
633
|
-
relativePath: cleanArchiveRelativePath(file.relativePath),
|
|
634
|
-
role: file.role,
|
|
635
|
-
contentBase64: file.contentBase64,
|
|
636
|
-
sha256: file.sha256,
|
|
637
|
-
sizeBytes: file.sizeBytes,
|
|
638
|
-
sourcePath: file.sourcePath
|
|
639
|
-
}));
|
|
640
|
-
}
|
|
641
|
-
return (raw.artifacts || []).map((artifact) => {
|
|
642
|
-
const contentBase64 = artifact.contentBase64 || Buffer.from(artifact.content || "", "utf8").toString("base64");
|
|
643
|
-
const bytes = Buffer.from(contentBase64, "base64");
|
|
644
|
-
return {
|
|
645
|
-
relativePath: cleanArchiveRelativePath(artifact.path),
|
|
646
|
-
role: "artifact",
|
|
647
|
-
contentBase64,
|
|
648
|
-
sha256: artifact.sha256 || sha256Bytes(bytes),
|
|
649
|
-
sizeBytes: artifact.sizeBytes ?? bytes.length
|
|
650
|
-
};
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
function decodeBase64StrictResult(value, relativePath) {
|
|
654
|
-
if (typeof value !== "string") {
|
|
655
|
-
return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "contentBase64 is not a string" } };
|
|
656
|
-
}
|
|
657
|
-
const compact = value.replace(/\s+/g, "");
|
|
658
|
-
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(compact)) {
|
|
659
|
-
return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "invalid base64 encoding" } };
|
|
660
|
-
}
|
|
661
|
-
const bytes = Buffer.from(compact, "base64");
|
|
662
|
-
const expected = compact.replace(/=+$/, "");
|
|
663
|
-
const actual = bytes.toString("base64").replace(/=+$/, "");
|
|
664
|
-
if (actual !== expected) {
|
|
665
|
-
return { ok: false, check: { name: "archive-file", pass: false, code: "archive-bad-base64", path: relativePath, actual: "non-canonical base64 encoding" } };
|
|
666
|
-
}
|
|
667
|
-
return { ok: true, bytes };
|
|
668
|
-
}
|
|
669
|
-
function decodeBase64Strict(value, relativePath) {
|
|
670
|
-
const decoded = decodeBase64StrictResult(value, relativePath);
|
|
671
|
-
if (!decoded.ok)
|
|
672
|
-
throw new Error(archiveCheckMessage(decoded.check));
|
|
673
|
-
return decoded.bytes;
|
|
674
|
-
}
|
|
675
|
-
/** NON-throwing digest/size/count/manifest verification: one structured check per
|
|
676
|
-
* file (in import order), then the integrity file-count + manifest checks. Shared
|
|
677
|
-
* by the throwing import path (verifyArchiveFileDigests) and the read-only
|
|
678
|
-
* inspectArchive, so a single offender list has one source of truth. */
|
|
679
|
-
function collectArchiveDigestChecks(files, integrity) {
|
|
680
|
-
const checks = [];
|
|
681
|
-
for (const file of files) {
|
|
682
|
-
const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
|
|
683
|
-
if (!decoded.ok) {
|
|
684
|
-
checks.push(decoded.check);
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
const bytes = decoded.bytes;
|
|
688
|
-
const actual = sha256Bytes(bytes);
|
|
689
|
-
const digestOk = actual === file.sha256;
|
|
690
|
-
checks.push(digestOk
|
|
691
|
-
? { name: "archive-file", pass: true, path: file.relativePath }
|
|
692
|
-
: { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
|
|
693
|
-
const sizeOk = bytes.length === file.sizeBytes;
|
|
694
|
-
checks.push(sizeOk
|
|
695
|
-
? { name: "archive-file", pass: true, path: file.relativePath }
|
|
696
|
-
: { name: "archive-file", pass: false, code: "size-mismatch", path: file.relativePath, expected: String(file.sizeBytes), actual: String(bytes.length) });
|
|
697
|
-
}
|
|
698
|
-
if (integrity) {
|
|
699
|
-
const countOk = integrity.fileCount === files.length;
|
|
700
|
-
checks.push(countOk
|
|
701
|
-
? { name: "archive-file-count", pass: true }
|
|
702
|
-
: { name: "archive-file-count", pass: false, code: "file-count-mismatch", expected: String(integrity.fileCount), actual: String(files.length) });
|
|
703
|
-
const actualManifest = digestManifest(files);
|
|
704
|
-
const manifestOk = integrity.manifestSha256 === actualManifest;
|
|
705
|
-
checks.push(manifestOk
|
|
706
|
-
? { name: "archive-manifest", pass: true }
|
|
707
|
-
: { name: "archive-manifest", pass: false, code: "manifest-digest-mismatch", expected: integrity.manifestSha256, actual: actualManifest });
|
|
708
|
-
}
|
|
709
|
-
return { checks, ok: checks.every((c) => c.pass) };
|
|
710
|
-
}
|
|
711
|
-
/** Reconstruct the legacy throw message for a failing check, so the throwing import
|
|
712
|
-
* path stays BYTE-IDENTICAL after the collector refactor. */
|
|
713
|
-
function archiveCheckMessage(check) {
|
|
714
|
-
switch (check.code) {
|
|
715
|
-
case "digest-mismatch": return `Archive digest mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
|
|
716
|
-
case "size-mismatch": return `Archive size mismatch for ${check.path}: expected ${check.expected}, got ${check.actual}`;
|
|
717
|
-
case "file-count-mismatch": return `Archive file count mismatch: expected ${check.expected}, got ${check.actual}`;
|
|
718
|
-
case "manifest-digest-mismatch": return `Archive manifest digest mismatch: expected ${check.expected}, got ${check.actual}`;
|
|
719
|
-
case "archive-bad-base64": return `Archive base64 invalid for ${check.path}: ${check.actual}`;
|
|
720
|
-
default: return `Archive verification failed: ${check.name}`;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
function verifyArchiveFileDigests(files, integrity) {
|
|
724
|
-
// Opt-in hardening (CW_REQUIRE_ARCHIVE_INTEGRITY=1): refuse an archive whose
|
|
725
|
-
// top-level integrity block (manifest digest + file count) is absent, closing the
|
|
726
|
-
// legacy fail-open seam where a stripped-integrity archive imported unverified.
|
|
727
|
-
// Same env-boolish convention as CW_REQUIRE_RESOLVABLE_EVIDENCE (evidence-grounding.ts:57).
|
|
728
|
-
// Default (unset) keeps legacy integrity-less archives byte-identical.
|
|
729
|
-
if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
730
|
-
throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
|
|
731
|
-
}
|
|
732
|
-
// Throw-before-write preserved: throw on the FIRST failing check, in the same
|
|
733
|
-
// order (per-file digest then size, then file-count, then manifest) and with the
|
|
734
|
-
// same message the inline checks produced.
|
|
735
|
-
const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
|
|
736
|
-
if (failed)
|
|
737
|
-
throw new Error(archiveCheckMessage(failed));
|
|
738
|
-
}
|
|
739
|
-
function digestManifest(files) {
|
|
740
|
-
const manifest = files
|
|
741
|
-
// sourcePath is deliberately EXCLUDED: it is a host-absolute bookkeeping path
|
|
742
|
-
// (for externalPathMap), not integrity-bearing content — the file's bytes are
|
|
743
|
-
// already bound by sha256 + sizeBytes. Including it would make the digest
|
|
744
|
-
// differ across hosts for byte-identical content, defeating cross-host repro.
|
|
745
|
-
.map((file) => ({
|
|
746
|
-
relativePath: file.relativePath,
|
|
747
|
-
role: file.role,
|
|
748
|
-
sha256: file.sha256,
|
|
749
|
-
sizeBytes: file.sizeBytes
|
|
750
|
-
}))
|
|
751
|
-
// Codepoint order, NOT localeCompare: this manifest feeds a sha256 integrity
|
|
752
|
-
// digest. Locale-sensitive collation would order identical bytes differently
|
|
753
|
-
// across hosts/locales, making the digest non-reproducible cross-host.
|
|
754
|
-
.sort((left, right) => (left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0));
|
|
755
|
-
return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
|
|
756
|
-
}
|
|
757
|
-
function rebaseRun(source, context) {
|
|
758
|
-
const cloned = deepRebase(JSON.parse(JSON.stringify(source)), context);
|
|
759
|
-
cloned.cwd = context.newCwd;
|
|
760
|
-
cloned.paths = context.paths;
|
|
761
|
-
cloned.updatedAt = new Date().toISOString();
|
|
762
|
-
cloned.audit = cloned.audit
|
|
763
|
-
? {
|
|
764
|
-
schemaVersion: 1,
|
|
765
|
-
eventLogPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "events.jsonl"),
|
|
766
|
-
summaryPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "summary.json"),
|
|
767
|
-
indexPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "index.json")
|
|
768
|
-
}
|
|
769
|
-
: cloned.audit;
|
|
770
|
-
return cloned;
|
|
771
|
-
}
|
|
772
|
-
function deepRebase(value, context) {
|
|
773
|
-
if (typeof value === "string")
|
|
774
|
-
return rebaseString(value, context);
|
|
775
|
-
if (Array.isArray(value))
|
|
776
|
-
return value.map((entry) => deepRebase(entry, context));
|
|
777
|
-
if (value && typeof value === "object") {
|
|
778
|
-
const out = {};
|
|
779
|
-
for (const [key, entry] of Object.entries(value))
|
|
780
|
-
out[key] = deepRebase(entry, context);
|
|
781
|
-
return out;
|
|
782
|
-
}
|
|
783
|
-
return value;
|
|
784
|
-
}
|
|
785
|
-
function rebaseString(value, context) {
|
|
786
|
-
const archivedExternal = context.externalPathMap?.get(value);
|
|
787
|
-
if (archivedExternal)
|
|
788
|
-
return archivedExternal;
|
|
789
|
-
if (value === context.oldRunDir || value.startsWith(context.oldRunDir + node_path_1.default.sep)) {
|
|
790
|
-
return context.newRunDir + value.slice(context.oldRunDir.length);
|
|
791
|
-
}
|
|
792
|
-
if (value === context.oldCwd || value.startsWith(context.oldCwd + node_path_1.default.sep)) {
|
|
793
|
-
return context.newCwd + value.slice(context.oldCwd.length);
|
|
794
|
-
}
|
|
795
|
-
return value;
|
|
796
|
-
}
|
|
797
|
-
function cleanArchiveRelativePath(value) {
|
|
798
|
-
const cleaned = toArchivePath(value).replace(/^\/+/, "");
|
|
799
|
-
if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
|
|
800
|
-
throw new Error(`Invalid archive relative path: ${value}`);
|
|
801
|
-
}
|
|
802
|
-
return cleaned;
|
|
803
|
-
}
|
|
804
|
-
function toArchivePath(value) {
|
|
805
|
-
return value.split(node_path_1.default.sep).join("/");
|
|
806
|
-
}
|
|
807
|
-
function safeArchiveBasename(value) {
|
|
808
|
-
return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
|
|
809
|
-
}
|
|
810
|
-
function messageOf(error) {
|
|
811
|
-
return error instanceof Error ? error.message : String(error);
|
|
812
|
-
}
|
|
813
|
-
function sha256Bytes(bytes) {
|
|
814
|
-
return node_crypto_1.default.createHash("sha256").update(bytes).digest("hex");
|
|
815
|
-
}
|