cool-workflow 0.1.79 → 0.1.81
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 +51 -3
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +64 -0
- package/apps/architecture-review-fast/workflow.js +153 -0
- 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 +42 -22
- package/dist/capability-core.js +132 -17
- package/dist/capability-registry.js +138 -168
- package/dist/cli.js +97 -98
- package/dist/collaboration.js +5 -6
- package/dist/commit.js +20 -6
- 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 +129 -0
- package/dist/coordinator.js +127 -300
- package/dist/dispatch.js +35 -0
- package/dist/drive.js +79 -6
- package/dist/error-feedback.js +8 -4
- package/dist/evidence-reasoning.js +3 -3
- package/dist/execution-backend/agent.js +331 -0
- package/dist/execution-backend/probes.js +96 -0
- package/dist/execution-backend/util.js +47 -0
- package/dist/execution-backend.js +73 -421
- package/dist/mcp-server.js +79 -183
- package/dist/multi-agent/graph.js +84 -0
- package/dist/multi-agent/helpers.js +145 -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 +39 -244
- package/dist/multi-agent-host.js +0 -19
- package/dist/multi-agent.js +125 -314
- package/dist/node-snapshot.js +3 -3
- 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 +2 -1
- package/dist/orchestrator/report.js +8 -0
- package/dist/orchestrator.js +26 -9
- package/dist/reclamation.js +26 -21
- package/dist/run-export.js +494 -25
- 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 +89 -597
- package/dist/run-state-schema.js +1 -0
- package/dist/sandbox-profile.js +43 -2
- package/dist/state-explosion/format.js +159 -0
- package/dist/state-explosion/helpers.js +82 -0
- package/dist/state-explosion.js +165 -304
- 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 +25 -8
- package/dist/triggers.js +33 -14
- package/dist/trust-audit.js +145 -33
- 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 +39 -115
- package/docs/agent-delegation-drive.7.md +71 -0
- package/docs/canonical-workflow-apps.7.md +37 -0
- package/docs/cli-mcp-parity.7.md +16 -0
- package/docs/contract-migration-tooling.7.md +6 -0
- package/docs/control-plane-scheduling.7.md +6 -0
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +40 -0
- package/docs/durable-state-and-locking.7.md +8 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
- package/docs/execution-backends.7.md +6 -0
- package/docs/index.md +2 -0
- package/docs/launch/demo.tape +28 -0
- package/docs/launch/launch-kit.md +96 -17
- package/docs/launch/pre-launch-checklist.md +53 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +8 -0
- package/docs/multi-agent-eval-replay-harness.7.md +6 -0
- package/docs/multi-agent-operator-ux.7.md +6 -0
- package/docs/multi-agent-trust-policy-audit.7.md +27 -0
- package/docs/node-snapshot-diff-replay.7.md +6 -0
- package/docs/observability-cost-accounting.7.md +6 -0
- package/docs/project-index.md +27 -6
- package/docs/real-execution-backends.7.md +6 -0
- package/docs/release-and-migration.7.md +8 -0
- package/docs/release-tooling.7.md +6 -0
- package/docs/routines.md +23 -0
- package/docs/run-registry-control-plane.7.md +89 -2
- package/docs/run-retention-reclamation.7.md +8 -0
- package/docs/source-context-profiles.7.md +119 -0
- package/docs/state-explosion-management.7.md +13 -0
- package/docs/team-collaboration.7.md +6 -0
- package/docs/trust-model.md +267 -0
- package/docs/unix-principles.md +49 -1
- package/docs/vendor-manifest-loadability.7.md +43 -0
- package/docs/web-desktop-workbench.7.md +6 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +142 -0
- package/package.json +4 -1
- package/scripts/agents/builtin-templates.json +7 -0
- package/scripts/agents/claude-p-agent.js +129 -43
- package/scripts/architecture-review-fast.js +362 -0
- package/scripts/bump-version.js +5 -10
- package/scripts/canonical-apps-list.js +64 -0
- package/scripts/canonical-apps.js +36 -4
- package/scripts/coverage-gate.js +211 -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/source-context.js +291 -0
- package/scripts/version-sync-check.js +5 -7
- package/skills/ci-triage/SKILL.md +50 -0
- package/skills/ci-triage/agents/openai.yaml +4 -0
- package/skills/cool-workflow/SKILL.md +4 -1
- package/skills/deploy-check/SKILL.md +55 -0
- package/skills/deploy-check/agents/openai.yaml +4 -0
- package/skills/design-qa/SKILL.md +49 -0
- package/skills/design-qa/agents/openai.yaml +4 -0
- package/skills/pr-review/SKILL.md +45 -0
- package/skills/pr-review/agents/openai.yaml +4 -0
- package/dist/capability-dispatcher.js +0 -86
package/dist/run-export.js
CHANGED
|
@@ -1,64 +1,533 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Run Export / Import — portable run archive format (
|
|
2
|
+
// Run Export / Import — portable run archive format (Track B).
|
|
3
3
|
//
|
|
4
|
-
// BSD discipline: explicit state, portable format. Export serializes a run
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
9
10
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
11
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
12
|
};
|
|
12
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
14
|
exports.exportRun = exportRun;
|
|
14
15
|
exports.importRun = importRun;
|
|
16
|
+
exports.verifyImportedRun = verifyImportedRun;
|
|
17
|
+
exports.inspectArchive = inspectArchive;
|
|
18
|
+
exports.importManifestPath = importManifestPath;
|
|
15
19
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
20
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
17
22
|
const state_1 = require("./state");
|
|
18
23
|
const version_1 = require("./version");
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
25
|
+
const trust_audit_1 = require("./trust-audit");
|
|
26
|
+
const compare_1 = require("./compare");
|
|
27
|
+
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
21
28
|
function exportRun(run, outputPath) {
|
|
22
29
|
const exportedAt = new Date().toISOString();
|
|
30
|
+
const files = collectArchiveFiles(run);
|
|
31
|
+
const manifestSha256 = digestManifest(files);
|
|
23
32
|
const exported = {
|
|
24
33
|
schemaVersion: 1,
|
|
25
34
|
exportedAt,
|
|
26
35
|
sourceVersion: version_1.CURRENT_COOL_WORKFLOW_VERSION,
|
|
27
36
|
run,
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
files,
|
|
38
|
+
integrity: {
|
|
39
|
+
fileCount: files.length,
|
|
40
|
+
manifestSha256
|
|
41
|
+
},
|
|
42
|
+
// Legacy field retained so old readers still find an artifact-ish list.
|
|
43
|
+
artifacts: files
|
|
44
|
+
.filter((file) => file.role === "artifact")
|
|
45
|
+
.map((file) => ({
|
|
46
|
+
path: file.relativePath,
|
|
47
|
+
contentBase64: file.contentBase64,
|
|
48
|
+
sha256: file.sha256,
|
|
49
|
+
sizeBytes: file.sizeBytes
|
|
50
|
+
})),
|
|
51
|
+
audit: files.filter((file) => file.role === "audit").map((file) => file.relativePath)
|
|
30
52
|
};
|
|
31
53
|
(0, state_1.writeJson)(outputPath, exported);
|
|
54
|
+
const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(outputPath));
|
|
32
55
|
return {
|
|
33
56
|
runId: run.id,
|
|
34
57
|
exportedAt,
|
|
35
58
|
path: outputPath,
|
|
36
59
|
taskCount: run.tasks.length,
|
|
37
|
-
commitCount: run.commits.length
|
|
60
|
+
commitCount: run.commits.length,
|
|
61
|
+
fileCount: files.length,
|
|
62
|
+
artifactCount: files.filter((file) => file.role === "artifact").length,
|
|
63
|
+
auditFileCount: files.filter((file) => file.role === "audit").length,
|
|
64
|
+
telemetryIncluded: files.some((file) => file.role === "telemetry"),
|
|
65
|
+
manifestSha256,
|
|
66
|
+
archiveSha256
|
|
38
67
|
};
|
|
39
68
|
}
|
|
40
69
|
/** Import a run from a portable JSON file into a target directory.
|
|
41
70
|
* Rebuilds run paths relative to the target dir. */
|
|
42
71
|
function importRun(exportPath, targetDir) {
|
|
43
|
-
const raw =
|
|
72
|
+
const raw = (0, state_1.readJson)(exportPath);
|
|
44
73
|
if (raw.schemaVersion !== 1)
|
|
45
74
|
throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
75
|
+
const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
|
|
76
|
+
const files = normalizeArchiveFiles(raw);
|
|
77
|
+
verifyArchiveFileDigests(files, raw.integrity);
|
|
78
|
+
const oldRunDir = raw.run.paths.runDir;
|
|
79
|
+
const oldCwd = raw.run.cwd;
|
|
80
|
+
const runDir = node_path_1.default.join(targetDir, ".cw", "runs", raw.run.id);
|
|
48
81
|
const paths = (0, state_1.createRunPaths)(runDir);
|
|
49
82
|
(0, state_1.ensureRunDirs)(paths);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Rebase node artifact paths too
|
|
55
|
-
for (const node of run.nodes || []) {
|
|
56
|
-
for (const artifact of node.artifacts || []) {
|
|
57
|
-
if (artifact.path && artifact.path.includes(".cw/runs/")) {
|
|
58
|
-
// Keep the original path as-is — the artifact may not exist in new location
|
|
59
|
-
}
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
const destination = node_path_1.default.join(runDir, file.relativePath);
|
|
85
|
+
if (!(0, state_1.isContainedPath)(destination, runDir)) {
|
|
86
|
+
throw new Error(`Archive file escapes restore directory: ${file.relativePath}`);
|
|
60
87
|
}
|
|
88
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(destination), { recursive: true });
|
|
89
|
+
node_fs_1.default.writeFileSync(destination, Buffer.from(file.contentBase64, "base64"));
|
|
90
|
+
}
|
|
91
|
+
const externalPathMap = new Map();
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (file.sourcePath)
|
|
94
|
+
externalPathMap.set(file.sourcePath, node_path_1.default.join(runDir, file.relativePath));
|
|
61
95
|
}
|
|
96
|
+
const run = rebaseRun(raw.run, {
|
|
97
|
+
oldRunDir,
|
|
98
|
+
newRunDir: runDir,
|
|
99
|
+
oldCwd,
|
|
100
|
+
newCwd: targetDir,
|
|
101
|
+
paths,
|
|
102
|
+
externalPathMap
|
|
103
|
+
});
|
|
62
104
|
(0, state_1.saveCheckpoint)(run);
|
|
63
|
-
|
|
105
|
+
const manifest = {
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
runId: run.id,
|
|
108
|
+
importedAt: new Date().toISOString(),
|
|
109
|
+
sourceVersion: raw.sourceVersion,
|
|
110
|
+
archiveSha256,
|
|
111
|
+
manifestSha256: digestManifest(files),
|
|
112
|
+
files: files.map(({ contentBase64: _contentBase64, ...file }) => file)
|
|
113
|
+
};
|
|
114
|
+
const manifestPath = importManifestPath(run);
|
|
115
|
+
(0, state_1.writeJson)(manifestPath, manifest, { durable: true });
|
|
116
|
+
const verification = verifyImportedRun(run);
|
|
117
|
+
return {
|
|
118
|
+
run,
|
|
119
|
+
runDir,
|
|
120
|
+
statePath: paths.state,
|
|
121
|
+
manifestPath,
|
|
122
|
+
verifyCommand: `cw run verify-import ${run.id} --cwd ${targetDir} --json`,
|
|
123
|
+
verification
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Verify an imported run against its restore manifest and telemetry chain. */
|
|
127
|
+
function verifyImportedRun(run) {
|
|
128
|
+
const manifestPath = importManifestPath(run);
|
|
129
|
+
const checks = [];
|
|
130
|
+
if (!node_fs_1.default.existsSync(manifestPath)) {
|
|
131
|
+
return {
|
|
132
|
+
runId: run.id,
|
|
133
|
+
ok: false,
|
|
134
|
+
manifestPath,
|
|
135
|
+
checkedFiles: 0,
|
|
136
|
+
checks: [{ name: "import-manifest", pass: false, code: "missing-import-manifest", path: manifestPath }]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
let manifest;
|
|
140
|
+
try {
|
|
141
|
+
manifest = (0, state_1.readJson)(manifestPath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
runId: run.id,
|
|
146
|
+
ok: false,
|
|
147
|
+
manifestPath,
|
|
148
|
+
checkedFiles: 0,
|
|
149
|
+
checks: [{ name: "import-manifest", pass: false, code: "invalid-import-manifest", path: manifestPath, actual: messageOf(error) }]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const currentManifestDigest = digestManifest(manifest.files.map((file) => ({ ...file, contentBase64: "" })));
|
|
153
|
+
checks.push({
|
|
154
|
+
name: "import-manifest",
|
|
155
|
+
pass: manifest.runId === run.id && manifest.manifestSha256 === currentManifestDigest,
|
|
156
|
+
code: manifest.runId !== run.id ? "run-id-mismatch" : manifest.manifestSha256 === currentManifestDigest ? undefined : "manifest-digest-mismatch",
|
|
157
|
+
expected: manifest.manifestSha256,
|
|
158
|
+
actual: currentManifestDigest
|
|
159
|
+
});
|
|
160
|
+
let filesOk = true;
|
|
161
|
+
for (const file of manifest.files) {
|
|
162
|
+
const restoredPath = node_path_1.default.join(run.paths.runDir, file.relativePath);
|
|
163
|
+
if (!(0, state_1.isContainedPath)(restoredPath, run.paths.runDir)) {
|
|
164
|
+
filesOk = false;
|
|
165
|
+
checks.push({ name: "archive-file", pass: false, code: "path-escape", path: file.relativePath });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!node_fs_1.default.existsSync(restoredPath)) {
|
|
169
|
+
filesOk = false;
|
|
170
|
+
checks.push({ name: "archive-file", pass: false, code: "missing-file", path: file.relativePath, expected: file.sha256 });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const actual = sha256Bytes(node_fs_1.default.readFileSync(restoredPath));
|
|
174
|
+
const pass = actual === file.sha256;
|
|
175
|
+
if (!pass)
|
|
176
|
+
filesOk = false;
|
|
177
|
+
checks.push({
|
|
178
|
+
name: "archive-file",
|
|
179
|
+
pass,
|
|
180
|
+
code: pass ? undefined : "digest-mismatch",
|
|
181
|
+
path: file.relativePath,
|
|
182
|
+
expected: file.sha256,
|
|
183
|
+
actual
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
checks.push({ name: "archive-files", pass: filesOk, code: filesOk ? undefined : "archive-files-invalid" });
|
|
187
|
+
const telemetry = (0, telemetry_ledger_1.verifyTelemetryLedger)(run);
|
|
188
|
+
checks.push({
|
|
189
|
+
name: "telemetry-ledger",
|
|
190
|
+
pass: telemetry.verified,
|
|
191
|
+
code: telemetry.verified ? undefined : "telemetry-ledger-invalid"
|
|
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
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
runId: run.id,
|
|
206
|
+
ok: checks.every((check) => check.pass),
|
|
207
|
+
manifestPath,
|
|
208
|
+
checkedFiles: manifest.files.length,
|
|
209
|
+
checks
|
|
210
|
+
};
|
|
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
|
+
}
|
|
276
|
+
function importManifestPath(run) {
|
|
277
|
+
return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
|
|
278
|
+
}
|
|
279
|
+
function collectArchiveFiles(run) {
|
|
280
|
+
const entries = new Map();
|
|
281
|
+
for (const file of walkFiles(run.paths.runDir)) {
|
|
282
|
+
const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
|
|
283
|
+
if (!relativePath || relativePath === "state.json" || relativePath === "import-manifest.json" || relativePath.endsWith(".lock"))
|
|
284
|
+
continue;
|
|
285
|
+
addFile(entries, run, file, roleForRelativePath(relativePath));
|
|
286
|
+
}
|
|
287
|
+
for (const artifactPath of collectReferencedArtifactPaths(run)) {
|
|
288
|
+
if (!artifactPath || !node_fs_1.default.existsSync(artifactPath) || !node_fs_1.default.statSync(artifactPath).isFile())
|
|
289
|
+
continue;
|
|
290
|
+
if ((0, state_1.isContainedPath)(artifactPath, run.paths.runDir)) {
|
|
291
|
+
addFile(entries, run, artifactPath, "artifact");
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if ((0, state_1.isContainedPath)(artifactPath, run.cwd))
|
|
295
|
+
addExternalArtifactFile(entries, run, artifactPath);
|
|
296
|
+
}
|
|
297
|
+
return [...entries.values()].sort((left, right) => (0, compare_1.compareBytes)(left.relativePath, right.relativePath));
|
|
298
|
+
}
|
|
299
|
+
function addFile(entries, run, file, role) {
|
|
300
|
+
const relativePath = toArchivePath(node_path_1.default.relative(run.paths.runDir, file));
|
|
301
|
+
if (relativePath === "state.json" || relativePath === "import-manifest.json")
|
|
302
|
+
return;
|
|
303
|
+
if (!relativePath || relativePath.startsWith("../"))
|
|
304
|
+
return;
|
|
305
|
+
const bytes = node_fs_1.default.readFileSync(file);
|
|
306
|
+
entries.set(relativePath, {
|
|
307
|
+
relativePath,
|
|
308
|
+
role,
|
|
309
|
+
contentBase64: bytes.toString("base64"),
|
|
310
|
+
sha256: sha256Bytes(bytes),
|
|
311
|
+
sizeBytes: bytes.length
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function addExternalArtifactFile(entries, run, file) {
|
|
315
|
+
const sourcePath = node_path_1.default.resolve(file);
|
|
316
|
+
const bytes = node_fs_1.default.readFileSync(sourcePath);
|
|
317
|
+
const relativePath = `external-artifacts/${sha256Bytes(Buffer.from(sourcePath, "utf8")).slice(0, 16)}-${safeArchiveBasename(node_path_1.default.basename(sourcePath))}`;
|
|
318
|
+
entries.set(relativePath, {
|
|
319
|
+
relativePath,
|
|
320
|
+
role: "artifact",
|
|
321
|
+
contentBase64: bytes.toString("base64"),
|
|
322
|
+
sha256: sha256Bytes(bytes),
|
|
323
|
+
sizeBytes: bytes.length,
|
|
324
|
+
sourcePath
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function collectReferencedArtifactPaths(run) {
|
|
328
|
+
const paths = new Set();
|
|
329
|
+
for (const node of run.nodes || []) {
|
|
330
|
+
for (const artifact of node.artifacts || [])
|
|
331
|
+
addArtifactPath(paths, run, artifact.path);
|
|
332
|
+
}
|
|
333
|
+
for (const candidate of run.candidates || []) {
|
|
334
|
+
for (const artifact of candidate.artifacts || [])
|
|
335
|
+
addArtifactPath(paths, run, artifact.path);
|
|
336
|
+
}
|
|
337
|
+
for (const selection of run.candidateSelections || []) {
|
|
338
|
+
for (const artifact of selection.artifacts || [])
|
|
339
|
+
addArtifactPath(paths, run, artifact.path);
|
|
340
|
+
}
|
|
341
|
+
for (const artifact of run.blackboard?.artifacts || [])
|
|
342
|
+
addArtifactPath(paths, run, artifact.path);
|
|
343
|
+
return [...paths].sort();
|
|
344
|
+
}
|
|
345
|
+
function addArtifactPath(paths, run, value) {
|
|
346
|
+
if (!value)
|
|
347
|
+
return;
|
|
348
|
+
paths.add(node_path_1.default.isAbsolute(value) ? value : node_path_1.default.resolve(run.cwd, value));
|
|
349
|
+
}
|
|
350
|
+
function walkFiles(root) {
|
|
351
|
+
if (!node_fs_1.default.existsSync(root))
|
|
352
|
+
return [];
|
|
353
|
+
const found = [];
|
|
354
|
+
for (const name of node_fs_1.default.readdirSync(root)) {
|
|
355
|
+
const file = node_path_1.default.join(root, name);
|
|
356
|
+
const stat = node_fs_1.default.lstatSync(file);
|
|
357
|
+
if (stat.isSymbolicLink())
|
|
358
|
+
continue;
|
|
359
|
+
if (stat.isDirectory())
|
|
360
|
+
found.push(...walkFiles(file));
|
|
361
|
+
else if (stat.isFile())
|
|
362
|
+
found.push(file);
|
|
363
|
+
}
|
|
364
|
+
return found;
|
|
365
|
+
}
|
|
366
|
+
function roleForRelativePath(relativePath) {
|
|
367
|
+
if (relativePath === "telemetry.json")
|
|
368
|
+
return "telemetry";
|
|
369
|
+
if (relativePath === "audit" || relativePath.startsWith("audit/"))
|
|
370
|
+
return "audit";
|
|
371
|
+
if (relativePath === "artifacts" || relativePath.startsWith("artifacts/"))
|
|
372
|
+
return "artifact";
|
|
373
|
+
return "run-file";
|
|
374
|
+
}
|
|
375
|
+
function normalizeArchiveFiles(raw) {
|
|
376
|
+
const modern = raw.files || [];
|
|
377
|
+
if (modern.length) {
|
|
378
|
+
return modern.map((file) => ({
|
|
379
|
+
relativePath: cleanArchiveRelativePath(file.relativePath),
|
|
380
|
+
role: file.role,
|
|
381
|
+
contentBase64: file.contentBase64,
|
|
382
|
+
sha256: file.sha256,
|
|
383
|
+
sizeBytes: file.sizeBytes,
|
|
384
|
+
sourcePath: file.sourcePath
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
return (raw.artifacts || []).map((artifact) => {
|
|
388
|
+
const contentBase64 = artifact.contentBase64 || Buffer.from(artifact.content || "", "utf8").toString("base64");
|
|
389
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
390
|
+
return {
|
|
391
|
+
relativePath: cleanArchiveRelativePath(artifact.path),
|
|
392
|
+
role: "artifact",
|
|
393
|
+
contentBase64,
|
|
394
|
+
sha256: artifact.sha256 || sha256Bytes(bytes),
|
|
395
|
+
sizeBytes: artifact.sizeBytes ?? bytes.length
|
|
396
|
+
};
|
|
397
|
+
});
|
|
398
|
+
}
|
|
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 = [];
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
407
|
+
const actual = sha256Bytes(bytes);
|
|
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) });
|
|
416
|
+
}
|
|
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) });
|
|
422
|
+
const actualManifest = digestManifest(files);
|
|
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)");
|
|
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));
|
|
456
|
+
}
|
|
457
|
+
function digestManifest(files) {
|
|
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.
|
|
463
|
+
.map((file) => ({
|
|
464
|
+
relativePath: file.relativePath,
|
|
465
|
+
role: file.role,
|
|
466
|
+
sha256: file.sha256,
|
|
467
|
+
sizeBytes: file.sizeBytes
|
|
468
|
+
}))
|
|
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));
|
|
473
|
+
return sha256Bytes(Buffer.from(JSON.stringify(manifest), "utf8"));
|
|
474
|
+
}
|
|
475
|
+
function rebaseRun(source, context) {
|
|
476
|
+
const cloned = deepRebase(JSON.parse(JSON.stringify(source)), context);
|
|
477
|
+
cloned.cwd = context.newCwd;
|
|
478
|
+
cloned.paths = context.paths;
|
|
479
|
+
cloned.updatedAt = new Date().toISOString();
|
|
480
|
+
cloned.audit = cloned.audit
|
|
481
|
+
? {
|
|
482
|
+
schemaVersion: 1,
|
|
483
|
+
eventLogPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "events.jsonl"),
|
|
484
|
+
summaryPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "summary.json"),
|
|
485
|
+
indexPath: node_path_1.default.join(context.paths.auditDir || node_path_1.default.join(context.paths.runDir, "audit"), "index.json")
|
|
486
|
+
}
|
|
487
|
+
: cloned.audit;
|
|
488
|
+
return cloned;
|
|
489
|
+
}
|
|
490
|
+
function deepRebase(value, context) {
|
|
491
|
+
if (typeof value === "string")
|
|
492
|
+
return rebaseString(value, context);
|
|
493
|
+
if (Array.isArray(value))
|
|
494
|
+
return value.map((entry) => deepRebase(entry, context));
|
|
495
|
+
if (value && typeof value === "object") {
|
|
496
|
+
const out = {};
|
|
497
|
+
for (const [key, entry] of Object.entries(value))
|
|
498
|
+
out[key] = deepRebase(entry, context);
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
return value;
|
|
502
|
+
}
|
|
503
|
+
function rebaseString(value, context) {
|
|
504
|
+
const archivedExternal = context.externalPathMap?.get(value);
|
|
505
|
+
if (archivedExternal)
|
|
506
|
+
return archivedExternal;
|
|
507
|
+
if (value === context.oldRunDir || value.startsWith(context.oldRunDir + node_path_1.default.sep)) {
|
|
508
|
+
return context.newRunDir + value.slice(context.oldRunDir.length);
|
|
509
|
+
}
|
|
510
|
+
if (value === context.oldCwd || value.startsWith(context.oldCwd + node_path_1.default.sep)) {
|
|
511
|
+
return context.newCwd + value.slice(context.oldCwd.length);
|
|
512
|
+
}
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
function cleanArchiveRelativePath(value) {
|
|
516
|
+
const cleaned = toArchivePath(value).replace(/^\/+/, "");
|
|
517
|
+
if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../")) {
|
|
518
|
+
throw new Error(`Invalid archive relative path: ${value}`);
|
|
519
|
+
}
|
|
520
|
+
return cleaned;
|
|
521
|
+
}
|
|
522
|
+
function toArchivePath(value) {
|
|
523
|
+
return value.split(node_path_1.default.sep).join("/");
|
|
524
|
+
}
|
|
525
|
+
function safeArchiveBasename(value) {
|
|
526
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
|
|
527
|
+
}
|
|
528
|
+
function messageOf(error) {
|
|
529
|
+
return error instanceof Error ? error.message : String(error);
|
|
530
|
+
}
|
|
531
|
+
function sha256Bytes(bytes) {
|
|
532
|
+
return node_crypto_1.default.createHash("sha256").update(bytes).digest("hex");
|
|
64
533
|
}
|