cool-workflow 0.2.4 → 0.2.6
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/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +204 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +32 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +337 -8
- package/dist/core/format/help.js +73 -3
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/state/run-paths.js +3 -30
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +22 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +215 -36
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +28 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +195 -128
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +201 -26
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +33 -0
- package/dist/shell/fs-atomic.js +96 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +9 -2
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +3 -2
- package/dist/shell/reclamation-io.js +10 -9
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +293 -31
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +183 -3
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +178 -36
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +83 -6
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/wiring/capability-table/basics.js +5 -0
- package/dist/wiring/capability-table/exec-backend.js +40 -22
- package/dist/wiring/capability-table/parity.js +10 -5
- package/dist/wiring/capability-table/pipeline.js +32 -0
- package/dist/wiring/capability-table/registry-core.js +26 -3
- package/dist/wiring/capability-table/reporting.js +7 -1
- package/dist/wiring/capability-table/scheduling-registry.js +8 -2
- package/dist/wiring/capability-table/trust-ledger.js +54 -12
- package/docs/agent-delegation-drive.7.md +17 -0
- package/docs/cli-mcp-parity.7.md +46 -5
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- 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/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +27 -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 +4 -0
- package/docs/project-index.md +53 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +12 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +37 -9
- package/docs/run-retention-reclamation.7.md +12 -4
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +72 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +6 -3
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +169 -41
- package/scripts/release-gate.js +208 -0
- package/scripts/release-oneclick.js +38 -7
- package/scripts/release-tags.js +47 -0
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-release-verdict.js +139 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/ui/workbench/app.css +50 -10
- package/ui/workbench/app.js +269 -34
- package/ui/workbench/index.html +4 -2
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -75
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/dist/shell/run-export.js
CHANGED
|
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
45
|
exports.exportRun = exportRun;
|
|
46
46
|
exports.importRun = importRun;
|
|
47
|
+
exports.restoreRunAtomically = restoreRunAtomically;
|
|
47
48
|
exports.verifyImportedRun = verifyImportedRun;
|
|
48
49
|
exports.inspectArchive = inspectArchive;
|
|
49
50
|
exports.importManifestPath = importManifestPath;
|
|
@@ -98,12 +99,15 @@ function exportRun(run, outputPath, options = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
/** Import a run from a portable JSON file into a target directory. */
|
|
100
101
|
function importRun(exportPath, targetDir) {
|
|
101
|
-
const
|
|
102
|
+
const limits = archiveIntakeLimits();
|
|
103
|
+
const archiveBytes = readArchiveBytes(exportPath, limits);
|
|
104
|
+
const raw = JSON.parse(archiveBytes.toString("utf8"));
|
|
102
105
|
if (raw.schemaVersion !== 1)
|
|
103
106
|
throw new Error(`Unsupported export schema version: ${raw.schemaVersion}`);
|
|
104
|
-
const archiveSha256 = (0, hash_1.sha256Bytes)(
|
|
107
|
+
const archiveSha256 = (0, hash_1.sha256Bytes)(archiveBytes);
|
|
105
108
|
const files = normalizeArchiveFiles(raw);
|
|
106
|
-
|
|
109
|
+
validateArchiveIntake(files, limits);
|
|
110
|
+
verifyArchiveFileDigests(files, raw.integrity, limits);
|
|
107
111
|
if (!raw.run || typeof raw.run !== "object") {
|
|
108
112
|
throw new Error("Invalid run export: missing run object");
|
|
109
113
|
}
|
|
@@ -113,10 +117,21 @@ function importRun(exportPath, targetDir) {
|
|
|
113
117
|
if (!(0, fs_atomic_1.isContainedPath)(runDir, runsRoot)) {
|
|
114
118
|
throw new Error(`Run id escapes the runs directory: ${JSON.stringify(raw.run.id)}`);
|
|
115
119
|
}
|
|
120
|
+
// Validate the run shape BEFORE the deref and before any dir/file is written.
|
|
121
|
+
// A truncated archive can be missing run.paths (or carry non-string
|
|
122
|
+
// runDir/cwd); without this guard the deref throws a raw TypeError, or a
|
|
123
|
+
// present-but-shapeless paths half-restores the run dir with a broken rebase.
|
|
124
|
+
// Fail closed as an invalid archive instead.
|
|
125
|
+
if (!raw.run.paths ||
|
|
126
|
+
typeof raw.run.paths !== "object" ||
|
|
127
|
+
typeof raw.run.paths.runDir !== "string" ||
|
|
128
|
+
typeof raw.run.cwd !== "string") {
|
|
129
|
+
throw new Error("Invalid run export: run.paths.runDir and run.cwd must be strings");
|
|
130
|
+
}
|
|
116
131
|
const oldRunDir = raw.run.paths.runDir;
|
|
117
132
|
const oldCwd = raw.run.cwd;
|
|
118
133
|
const paths = (0, run_paths_1.createRunPaths)(runDir);
|
|
119
|
-
(0,
|
|
134
|
+
(0, run_store_1.ensureRunDirs)(paths);
|
|
120
135
|
for (const file of files) {
|
|
121
136
|
const destination = path.join(runDir, file.relativePath);
|
|
122
137
|
if (!(0, fs_atomic_1.isContainedPath)(destination, runDir)) {
|
|
@@ -153,6 +168,66 @@ function importRun(exportPath, targetDir) {
|
|
|
153
168
|
verification,
|
|
154
169
|
};
|
|
155
170
|
}
|
|
171
|
+
/** Restore through a same-disk staging tree, then publish the checked run with
|
|
172
|
+
* one rename. Low-level import keeps its old report-only chain behavior. */
|
|
173
|
+
function restoreRunAtomically(exportPath, targetDir) {
|
|
174
|
+
const physicalTarget = path.resolve(targetDir);
|
|
175
|
+
fs.mkdirSync(physicalTarget, { recursive: true });
|
|
176
|
+
const stageRoot = fs.mkdtempSync(path.join(physicalTarget, ".cw-restore-"));
|
|
177
|
+
let publishedRunDir;
|
|
178
|
+
try {
|
|
179
|
+
const staged = importRun(exportPath, stageRoot);
|
|
180
|
+
const finalRunDir = path.join(targetDir, ".cw", "runs", staged.run.id);
|
|
181
|
+
const physicalFinalRunDir = path.resolve(finalRunDir);
|
|
182
|
+
const stagedVerification = rebaseVerificationPaths(staged.verification, staged.run.paths.runDir, finalRunDir);
|
|
183
|
+
if (!staged.verification.ok)
|
|
184
|
+
return { imported: null, verification: stagedVerification };
|
|
185
|
+
if (fs.existsSync(physicalFinalRunDir)) {
|
|
186
|
+
throw new Error(`Refusing to overwrite existing restored run: ${staged.run.id}`);
|
|
187
|
+
}
|
|
188
|
+
const finalPaths = (0, run_paths_1.createRunPaths)(finalRunDir);
|
|
189
|
+
const finalRun = rebaseRun(staged.run, {
|
|
190
|
+
oldRunDir: staged.run.paths.runDir,
|
|
191
|
+
newRunDir: finalRunDir,
|
|
192
|
+
oldCwd: stageRoot,
|
|
193
|
+
newCwd: targetDir,
|
|
194
|
+
paths: finalPaths,
|
|
195
|
+
});
|
|
196
|
+
(0, fs_atomic_1.writeJson)(staged.run.paths.state, finalRun, { durable: true });
|
|
197
|
+
const stateCheck = (0, run_store_1.checkRunStateFile)(staged.run.paths.state);
|
|
198
|
+
if (stateCheck.report.status === "unsupported") {
|
|
199
|
+
throw new Error(`Restore state validation failed: ${stateCheck.report.errors.join("; ") || "unsupported run state"}`);
|
|
200
|
+
}
|
|
201
|
+
fs.mkdirSync(path.dirname(physicalFinalRunDir), { recursive: true });
|
|
202
|
+
fs.renameSync(path.resolve(staged.run.paths.runDir), physicalFinalRunDir);
|
|
203
|
+
publishedRunDir = physicalFinalRunDir;
|
|
204
|
+
const verification = verifyImportedRun(finalRun);
|
|
205
|
+
if (!verification.ok) {
|
|
206
|
+
fs.rmSync(physicalFinalRunDir, { recursive: true, force: true });
|
|
207
|
+
publishedRunDir = undefined;
|
|
208
|
+
return { imported: null, verification };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
imported: {
|
|
212
|
+
run: finalRun,
|
|
213
|
+
runDir: finalRunDir,
|
|
214
|
+
statePath: finalPaths.state,
|
|
215
|
+
manifestPath: importManifestPath(finalRun),
|
|
216
|
+
verifyCommand: `cw run verify-import ${finalRun.id} --cwd ${targetDir} --json`,
|
|
217
|
+
verification,
|
|
218
|
+
},
|
|
219
|
+
verification,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (publishedRunDir)
|
|
224
|
+
fs.rmSync(publishedRunDir, { recursive: true, force: true });
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
fs.rmSync(stageRoot, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
156
231
|
/** Verify an imported run against its restore manifest and telemetry
|
|
157
232
|
* chain. */
|
|
158
233
|
function verifyImportedRun(run) {
|
|
@@ -228,12 +303,21 @@ function inspectArchive(archivePath) {
|
|
|
228
303
|
archiveSha256: null,
|
|
229
304
|
checks: [],
|
|
230
305
|
};
|
|
306
|
+
let limits;
|
|
307
|
+
try {
|
|
308
|
+
limits = archiveIntakeLimits();
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
return { ...base, checks: [{ name: "archive-limit", pass: false, code: "archive-limit-invalid", actual: messageOf(error) }] };
|
|
312
|
+
}
|
|
231
313
|
let bytes;
|
|
232
314
|
try {
|
|
233
|
-
bytes =
|
|
315
|
+
bytes = readArchiveBytes(archivePath, limits);
|
|
234
316
|
}
|
|
235
317
|
catch (error) {
|
|
236
|
-
|
|
318
|
+
const message = messageOf(error);
|
|
319
|
+
const limited = message.startsWith("Archive raw byte limit exceeded:");
|
|
320
|
+
return { ...base, checks: [{ name: limited ? "archive-limit" : "archive", pass: false, code: limited ? "archive-limit-raw-bytes" : "archive-unreadable", path: archivePath, actual: message }] };
|
|
237
321
|
}
|
|
238
322
|
base.archiveSha256 = (0, hash_1.sha256Bytes)(bytes);
|
|
239
323
|
let raw;
|
|
@@ -252,7 +336,23 @@ function inspectArchive(archivePath) {
|
|
|
252
336
|
}
|
|
253
337
|
try {
|
|
254
338
|
const files = normalizeArchiveFiles(raw);
|
|
255
|
-
|
|
339
|
+
try {
|
|
340
|
+
validateArchiveIntake(files, limits);
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
return {
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
archivePath,
|
|
346
|
+
ok: false,
|
|
347
|
+
schemaSupported: true,
|
|
348
|
+
runId: raw.run && raw.run.id ? raw.run.id : null,
|
|
349
|
+
fileCount: files.length,
|
|
350
|
+
manifestSha256: raw.integrity ? digestManifest(files) : null,
|
|
351
|
+
archiveSha256: base.archiveSha256,
|
|
352
|
+
checks: [archiveLimitCheck(error)],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const checks = collectArchiveDigestChecks(files, raw.integrity, limits).checks;
|
|
256
356
|
if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
257
357
|
checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
|
|
258
358
|
}
|
|
@@ -293,7 +393,7 @@ function reportSectionEmbedsResult(reportMd, taskId, expected) {
|
|
|
293
393
|
* (with the bundle's embedded public key) the ed25519 signatures.
|
|
294
394
|
* Never throws — every failure is a structured check and a false `ok`.
|
|
295
395
|
*
|
|
296
|
-
* Key precedence is
|
|
396
|
+
* Key precedence is argument > bundle > environment. */
|
|
297
397
|
function verifyReportBundle(archivePath, options = {}) {
|
|
298
398
|
const inspect = inspectArchive(archivePath);
|
|
299
399
|
const failedChecks = inspect.checks.filter((check) => !check.pass).map((check) => ({ name: check.name, code: check.code }));
|
|
@@ -314,12 +414,12 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
314
414
|
reportFindingsVerified: false,
|
|
315
415
|
failedChecks,
|
|
316
416
|
};
|
|
317
|
-
if (!inspect.schemaSupported)
|
|
417
|
+
if (!inspect.schemaSupported || inspect.checks.some((check) => check.code?.startsWith("archive-limit-")))
|
|
318
418
|
return base;
|
|
319
419
|
let bundleKey;
|
|
320
420
|
let reportContent;
|
|
321
421
|
try {
|
|
322
|
-
const raw = JSON.parse(
|
|
422
|
+
const raw = JSON.parse(readArchiveBytes(archivePath, archiveIntakeLimits()).toString("utf8"));
|
|
323
423
|
bundleKey = raw.trust?.publicKeyPem;
|
|
324
424
|
if (options.extractReportTo) {
|
|
325
425
|
const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
|
|
@@ -333,8 +433,39 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
333
433
|
catch {
|
|
334
434
|
/* inspect already recorded the parse failure; treat key as absent */
|
|
335
435
|
}
|
|
336
|
-
|
|
337
|
-
|
|
436
|
+
// Key precedence: an explicit operator --pubkey/options.pubkey WINS over a
|
|
437
|
+
// bundle-embedded key. A bundle carries its own key so it verifies OFFLINE,
|
|
438
|
+
// but that key must never OVERRIDE a key the operator pinned by hand — else a
|
|
439
|
+
// bundle re-signed with an attacker's OWN key (and embedding that key) would
|
|
440
|
+
// verify green against itself. When the operator pins a key AND the bundle
|
|
441
|
+
// embeds a DIFFERENT one, fail closed with a clear trust-key-mismatch rather
|
|
442
|
+
// than silently trusting the bundle's own key.
|
|
443
|
+
const resolvedArg = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.pubkey);
|
|
444
|
+
const resolvedBundle = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey);
|
|
445
|
+
const resolvedEnv = (0, telemetry_attestation_1.resolveTrustPublicKey)(process.env.CW_AGENT_ATTEST_PUBKEY);
|
|
446
|
+
let trustKey;
|
|
447
|
+
let trustKeySource;
|
|
448
|
+
let trustKeyConflict = false;
|
|
449
|
+
if (options.pubkey) {
|
|
450
|
+
trustKey = resolvedArg;
|
|
451
|
+
trustKeySource = "argument";
|
|
452
|
+
if (resolvedArg && resolvedBundle && normalizePem(resolvedArg) !== normalizePem(resolvedBundle)) {
|
|
453
|
+
trustKeyConflict = true;
|
|
454
|
+
failedChecks.push({ name: "trust-key", code: "trust-key-mismatch" });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (bundleKey) {
|
|
458
|
+
trustKey = resolvedBundle;
|
|
459
|
+
trustKeySource = "bundle";
|
|
460
|
+
}
|
|
461
|
+
else if (process.env.CW_AGENT_ATTEST_PUBKEY) {
|
|
462
|
+
trustKey = resolvedEnv;
|
|
463
|
+
trustKeySource = "environment";
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
trustKey = undefined;
|
|
467
|
+
trustKeySource = "none";
|
|
468
|
+
}
|
|
338
469
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-verify-bundle-"));
|
|
339
470
|
let telemetryVerified = false;
|
|
340
471
|
let trustAuditVerified = false;
|
|
@@ -421,7 +552,7 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
421
552
|
schemaVersion: 1,
|
|
422
553
|
archivePath,
|
|
423
554
|
runId: inspect.runId,
|
|
424
|
-
ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && reportFindingsOk && !strictShortfall && !extractShortfall && !unsignedShortfall,
|
|
555
|
+
ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && reportFindingsOk && !strictShortfall && !extractShortfall && !unsignedShortfall && !trustKeyConflict,
|
|
425
556
|
archiveOk: inspect.ok,
|
|
426
557
|
telemetryVerified,
|
|
427
558
|
trustAuditVerified,
|
|
@@ -523,28 +654,133 @@ function roleForRelativePath(relativePath) {
|
|
|
523
654
|
return "run-file";
|
|
524
655
|
}
|
|
525
656
|
function normalizeArchiveFiles(raw) {
|
|
526
|
-
const
|
|
657
|
+
const modernValue = raw.files;
|
|
658
|
+
if (modernValue !== undefined && !Array.isArray(modernValue)) {
|
|
659
|
+
throw new Error("Invalid run export: files must be an array");
|
|
660
|
+
}
|
|
661
|
+
const modern = (modernValue || []);
|
|
527
662
|
if (modern.length) {
|
|
528
|
-
return modern.map(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
663
|
+
return validateArchiveFileTable(modern.map(normalizeModernArchiveFile));
|
|
664
|
+
}
|
|
665
|
+
const legacyValue = raw.artifacts;
|
|
666
|
+
if (legacyValue !== undefined && !Array.isArray(legacyValue)) {
|
|
667
|
+
throw new Error("Invalid run export: artifacts must be an array");
|
|
668
|
+
}
|
|
669
|
+
const legacy = (legacyValue || []).map((value) => {
|
|
670
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
671
|
+
throw new Error("Invalid run export: every artifact must be an object");
|
|
672
|
+
}
|
|
673
|
+
const artifact = value;
|
|
674
|
+
if (typeof artifact.path !== "string")
|
|
675
|
+
throw new Error("Invalid run export: artifact path must be a string");
|
|
538
676
|
const contentBase64 = artifact.contentBase64 || Buffer.from("", "utf8").toString("base64");
|
|
677
|
+
if (typeof contentBase64 !== "string")
|
|
678
|
+
throw new Error(`Invalid run export: contentBase64 must be a string for ${artifact.path}`);
|
|
539
679
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
540
680
|
return {
|
|
541
681
|
relativePath: cleanArchiveRelativePath(artifact.path),
|
|
542
682
|
role: "artifact",
|
|
543
683
|
contentBase64,
|
|
544
|
-
sha256: artifact.sha256
|
|
545
|
-
sizeBytes: artifact.sizeBytes
|
|
684
|
+
sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : (0, hash_1.sha256Bytes)(bytes),
|
|
685
|
+
sizeBytes: artifact.sizeBytes === undefined ? bytes.length : artifact.sizeBytes,
|
|
546
686
|
};
|
|
547
687
|
});
|
|
688
|
+
return validateArchiveFileTable(legacy);
|
|
689
|
+
}
|
|
690
|
+
function archiveIntakeLimits() {
|
|
691
|
+
return {
|
|
692
|
+
rawBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_BYTES"),
|
|
693
|
+
fileCount: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_FILES"),
|
|
694
|
+
contentBytes: archiveLimitFromEnvironment("CW_MAX_RUN_ARCHIVE_CONTENT_BYTES"),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function archiveLimitFromEnvironment(name) {
|
|
698
|
+
const value = process.env[name];
|
|
699
|
+
if (value === undefined)
|
|
700
|
+
return undefined;
|
|
701
|
+
if (!/^[1-9][0-9]*$/.test(value))
|
|
702
|
+
throw new Error(`Invalid ${name}: expected a positive safe integer`);
|
|
703
|
+
const parsed = Number(value);
|
|
704
|
+
if (!Number.isSafeInteger(parsed))
|
|
705
|
+
throw new Error(`Invalid ${name}: expected a positive safe integer`);
|
|
706
|
+
return parsed;
|
|
707
|
+
}
|
|
708
|
+
/** Read the archive only after its operator-set raw-byte limit has passed. */
|
|
709
|
+
function readArchiveBytes(archivePath, limits) {
|
|
710
|
+
if (limits.rawBytes !== undefined) {
|
|
711
|
+
const size = fs.statSync(archivePath).size;
|
|
712
|
+
if (size > limits.rawBytes)
|
|
713
|
+
throw new Error(`Archive raw byte limit exceeded: max ${limits.rawBytes}, got ${size}`);
|
|
714
|
+
}
|
|
715
|
+
return fs.readFileSync(archivePath);
|
|
716
|
+
}
|
|
717
|
+
function validateArchiveIntake(files, limits) {
|
|
718
|
+
if (limits.fileCount !== undefined && files.length > limits.fileCount) {
|
|
719
|
+
throw new Error(`Archive file count limit exceeded: max ${limits.fileCount}, got ${files.length}`);
|
|
720
|
+
}
|
|
721
|
+
if (limits.contentBytes === undefined)
|
|
722
|
+
return;
|
|
723
|
+
let total = 0;
|
|
724
|
+
for (const file of files) {
|
|
725
|
+
if (file.sizeBytes > limits.contentBytes - total) {
|
|
726
|
+
throw new Error(`Archive content byte limit exceeded: max ${limits.contentBytes}, got more than ${limits.contentBytes}`);
|
|
727
|
+
}
|
|
728
|
+
total += file.sizeBytes;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function archiveLimitCheck(error) {
|
|
732
|
+
const actual = messageOf(error);
|
|
733
|
+
const code = actual.startsWith("Archive file count limit exceeded:")
|
|
734
|
+
? "archive-limit-file-count"
|
|
735
|
+
: actual.startsWith("Archive content byte limit exceeded:")
|
|
736
|
+
? "archive-limit-content-bytes"
|
|
737
|
+
: "archive-limit-invalid";
|
|
738
|
+
return { name: "archive-limit", pass: false, code, actual };
|
|
739
|
+
}
|
|
740
|
+
function normalizeModernArchiveFile(value) {
|
|
741
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
742
|
+
throw new Error("Invalid run export: every file entry must be an object");
|
|
743
|
+
}
|
|
744
|
+
const file = value;
|
|
745
|
+
if (typeof file.relativePath !== "string")
|
|
746
|
+
throw new Error("Invalid run export: file relativePath must be a string");
|
|
747
|
+
if (typeof file.contentBase64 !== "string")
|
|
748
|
+
throw new Error(`Invalid run export: contentBase64 must be a string for ${file.relativePath}`);
|
|
749
|
+
if (typeof file.sha256 !== "string")
|
|
750
|
+
throw new Error(`Invalid run export: sha256 must be a string for ${file.relativePath}`);
|
|
751
|
+
if (file.sourcePath !== undefined && typeof file.sourcePath !== "string") {
|
|
752
|
+
throw new Error(`Invalid run export: sourcePath must be a string for ${file.relativePath}`);
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
relativePath: cleanArchiveRelativePath(file.relativePath),
|
|
756
|
+
role: file.role,
|
|
757
|
+
contentBase64: file.contentBase64,
|
|
758
|
+
sha256: file.sha256,
|
|
759
|
+
sizeBytes: file.sizeBytes,
|
|
760
|
+
sourcePath: file.sourcePath,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/** Make one canonical, unique archive file table before any caller writes.
|
|
764
|
+
* Digest and base64 checks run next, but shape and path faults stop here. */
|
|
765
|
+
function validateArchiveFileTable(files) {
|
|
766
|
+
const roles = new Set(["artifact", "audit", "telemetry", "run-file"]);
|
|
767
|
+
const seen = new Set();
|
|
768
|
+
for (const file of files) {
|
|
769
|
+
if (!roles.has(file.role))
|
|
770
|
+
throw new Error(`Invalid archive file role for ${file.relativePath}: ${String(file.role)}`);
|
|
771
|
+
if (!/^[a-f0-9]{64}$/.test(file.sha256))
|
|
772
|
+
throw new Error(`Invalid archive sha256 for ${file.relativePath}`);
|
|
773
|
+
if (!Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0) {
|
|
774
|
+
throw new Error(`Invalid archive size for ${file.relativePath}: ${String(file.sizeBytes)}`);
|
|
775
|
+
}
|
|
776
|
+
if (file.relativePath === "state.json" || file.relativePath === "import-manifest.json" || file.relativePath.endsWith(".lock")) {
|
|
777
|
+
throw new Error(`Reserved archive relative path: ${file.relativePath}`);
|
|
778
|
+
}
|
|
779
|
+
if (seen.has(file.relativePath))
|
|
780
|
+
throw new Error(`Duplicate archive relative path: ${file.relativePath}`);
|
|
781
|
+
seen.add(file.relativePath);
|
|
782
|
+
}
|
|
783
|
+
return files;
|
|
548
784
|
}
|
|
549
785
|
function decodeBase64StrictResult(value, relativePath) {
|
|
550
786
|
if (typeof value !== "string") {
|
|
@@ -568,8 +804,9 @@ function decodeBase64Strict(value, relativePath) {
|
|
|
568
804
|
throw new Error(archiveCheckMessage(decoded.check));
|
|
569
805
|
return decoded.bytes;
|
|
570
806
|
}
|
|
571
|
-
function collectArchiveDigestChecks(files, integrity) {
|
|
807
|
+
function collectArchiveDigestChecks(files, integrity, limits = {}) {
|
|
572
808
|
const checks = [];
|
|
809
|
+
let decodedTotal = 0;
|
|
573
810
|
for (const file of files) {
|
|
574
811
|
const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
|
|
575
812
|
if (!decoded.ok) {
|
|
@@ -577,6 +814,12 @@ function collectArchiveDigestChecks(files, integrity) {
|
|
|
577
814
|
continue;
|
|
578
815
|
}
|
|
579
816
|
const bytes = decoded.bytes;
|
|
817
|
+
if (limits.contentBytes !== undefined && bytes.length > limits.contentBytes - decodedTotal) {
|
|
818
|
+
checks.push({ name: "archive-limit", pass: false, code: "archive-limit-content-bytes", path: file.relativePath, expected: String(limits.contentBytes), actual: `more than ${limits.contentBytes}` });
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
decodedTotal += bytes.length;
|
|
822
|
+
}
|
|
580
823
|
const actual = (0, hash_1.sha256Bytes)(bytes);
|
|
581
824
|
const digestOk = actual === file.sha256;
|
|
582
825
|
checks.push(digestOk ? { name: "archive-file", pass: true, path: file.relativePath } : { name: "archive-file", pass: false, code: "digest-mismatch", path: file.relativePath, expected: file.sha256, actual });
|
|
@@ -608,11 +851,11 @@ function archiveCheckMessage(check) {
|
|
|
608
851
|
return `Archive verification failed: ${check.name}`;
|
|
609
852
|
}
|
|
610
853
|
}
|
|
611
|
-
function verifyArchiveFileDigests(files, integrity) {
|
|
854
|
+
function verifyArchiveFileDigests(files, integrity, limits = {}) {
|
|
612
855
|
if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
613
856
|
throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
|
|
614
857
|
}
|
|
615
|
-
const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
|
|
858
|
+
const failed = collectArchiveDigestChecks(files, integrity, limits).checks.find((c) => !c.pass);
|
|
616
859
|
if (failed)
|
|
617
860
|
throw new Error(archiveCheckMessage(failed));
|
|
618
861
|
}
|
|
@@ -637,6 +880,16 @@ function rebaseRun(source, context) {
|
|
|
637
880
|
: cloned.audit;
|
|
638
881
|
return cloned;
|
|
639
882
|
}
|
|
883
|
+
function rebaseVerificationPaths(result, oldRunDir, newRunDir) {
|
|
884
|
+
const replace = (value) => value === oldRunDir || value.startsWith(oldRunDir + path.sep)
|
|
885
|
+
? newRunDir + value.slice(oldRunDir.length)
|
|
886
|
+
: value;
|
|
887
|
+
return {
|
|
888
|
+
...result,
|
|
889
|
+
manifestPath: replace(result.manifestPath),
|
|
890
|
+
checks: result.checks.map((check) => ({ ...check, ...(check.path ? { path: replace(check.path) } : {}) })),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
640
893
|
function deepRebase(value, context) {
|
|
641
894
|
if (typeof value === "string")
|
|
642
895
|
return rebaseString(value, context);
|
|
@@ -663,10 +916,13 @@ function rebaseString(value, context) {
|
|
|
663
916
|
return value;
|
|
664
917
|
}
|
|
665
918
|
function cleanArchiveRelativePath(value) {
|
|
666
|
-
const
|
|
667
|
-
if (
|
|
919
|
+
const portable = value.replace(/\\/g, "/");
|
|
920
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || /(^|\/)\.\.(\/|$)/.test(portable) || /[\u0000-\u001f\u007f]/.test(portable)) {
|
|
668
921
|
throw new Error(`Invalid archive relative path: ${value}`);
|
|
669
922
|
}
|
|
923
|
+
const cleaned = path.posix.normalize(portable);
|
|
924
|
+
if (!cleaned || cleaned === "." || cleaned.startsWith("../") || cleaned.includes("/../"))
|
|
925
|
+
throw new Error(`Invalid archive relative path: ${value}`);
|
|
670
926
|
return cleaned;
|
|
671
927
|
}
|
|
672
928
|
function toArchivePath(value) {
|
|
@@ -675,6 +931,12 @@ function toArchivePath(value) {
|
|
|
675
931
|
function safeArchiveBasename(value) {
|
|
676
932
|
return value.replace(/[^A-Za-z0-9._-]/g, "_") || "artifact";
|
|
677
933
|
}
|
|
934
|
+
/** Compare two PEM public keys byte-for-byte after dropping whitespace, so a
|
|
935
|
+
* benign line-wrap or trailing-newline difference does not read as a key
|
|
936
|
+
* change, but a truly different key (different base64 body) does. */
|
|
937
|
+
function normalizePem(pem) {
|
|
938
|
+
return (pem || "").replace(/\s+/g, "");
|
|
939
|
+
}
|
|
678
940
|
function messageOf(error) {
|
|
679
941
|
return error instanceof Error ? error.message : String(error);
|
|
680
942
|
}
|
|
@@ -1048,6 +1048,14 @@ function formatResume(result) {
|
|
|
1048
1048
|
`Resume ${result.runId} [${result.lifecycle}] loopStage=${result.loopStage} (resolved from ${result.resolvedFrom}, ${result.freshness})`,
|
|
1049
1049
|
` resumable=${result.resumable} nextTasks=${result.nextTasks.length}`,
|
|
1050
1050
|
];
|
|
1051
|
+
if (result.drive) {
|
|
1052
|
+
lines.push(` drive=${result.drive.status} workers=${result.drive.completedWorkers}/${result.drive.plannedWorkers} parked=${result.drive.parkedWorkers}`);
|
|
1053
|
+
for (const step of result.drive.steps) {
|
|
1054
|
+
lines.push(` step ${step.action} [${step.status}]${step.taskId ? ` task=${step.taskId}` : ""}${step.reason ? ` reason=${step.reason}` : ""}`);
|
|
1055
|
+
}
|
|
1056
|
+
if (result.drive.commitId)
|
|
1057
|
+
lines.push(` commit=${result.drive.commitId}`);
|
|
1058
|
+
}
|
|
1051
1059
|
for (const action of result.nextActions)
|
|
1052
1060
|
lines.push(` -> ${action.command}\n ${action.reason}`);
|
|
1053
1061
|
return lines.join("\n");
|