cool-workflow 0.2.5 → 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/plugin.json +1 -1
- package/.opencode-plugin/plugin.json +1 -1
- package/README.md +5 -3
- 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/core/state/run-paths.js +3 -30
- package/dist/core/version.js +1 -1
- package/dist/mcp/server.js +95 -27
- package/dist/mcp/tool-process.js +181 -0
- package/dist/mcp-server.js +8 -1
- package/dist/shell/agent-config.js +11 -2
- package/dist/shell/audit-cli.js +1 -1
- package/dist/shell/drive.js +99 -89
- package/dist/shell/execution-backend/agent.js +18 -15
- package/dist/shell/execution-backend/local.js +1 -0
- package/dist/shell/fs-atomic.js +3 -0
- package/dist/shell/metrics-cli.js +1 -1
- package/dist/shell/multi-agent-cli.js +4 -1
- package/dist/shell/multi-agent-operator-ux.js +1 -1
- package/dist/shell/observability.js +7 -4
- package/dist/shell/operator-ux.js +1 -1
- package/dist/shell/perf-trace.js +136 -0
- package/dist/shell/pipeline.js +1 -1
- package/dist/shell/report-view-cli.js +2 -0
- package/dist/shell/run-export-cli.js +5 -2
- package/dist/shell/run-export.js +241 -27
- package/dist/shell/run-registry-io.js +8 -0
- package/dist/shell/run-store.js +27 -3
- package/dist/shell/state-explosion-cli.js +4 -1
- package/dist/shell/telemetry-demo.js +1 -1
- package/dist/shell/trust-audit.js +91 -43
- package/dist/shell/workbench.js +4 -1
- package/dist/wiring/capability-table/parity.js +10 -5
- package/docs/agent-delegation-drive.7.md +2 -0
- package/docs/cli-mcp-parity.7.md +21 -5
- package/docs/contract-migration-tooling.7.md +2 -0
- package/docs/control-plane-scheduling.7.md +2 -0
- package/docs/durable-state-and-locking.7.md +2 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +2 -0
- package/docs/execution-backends.7.md +2 -0
- package/docs/mcp-app-surface.7.md +21 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +2 -0
- package/docs/multi-agent-eval-replay-harness.7.md +2 -0
- package/docs/multi-agent-operator-ux.7.md +2 -0
- package/docs/node-snapshot-diff-replay.7.md +2 -0
- package/docs/observability-cost-accounting.7.md +2 -0
- package/docs/project-index.md +18 -4
- package/docs/real-execution-backends.7.md +2 -0
- package/docs/release-and-migration.7.md +2 -0
- package/docs/release-tooling.7.md +10 -0
- package/docs/run-registry-control-plane.7.md +35 -9
- package/docs/run-retention-reclamation.7.md +2 -0
- package/docs/state-explosion-management.7.md +2 -0
- package/docs/team-collaboration.7.md +2 -0
- package/docs/trust-audit-anchor.7.md +2 -0
- package/docs/web-desktop-workbench.7.md +48 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/gen-manifests.js +32 -61
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +42 -23
- package/scripts/purity-baseline.json +0 -3
- package/scripts/release-flow.js +43 -13
- package/scripts/release-gate.js +30 -6
- package/scripts/schema-version-inventory.json +31 -0
- package/scripts/validate-run-state-schema.js +95 -4
- package/scripts/verify-release-verdict.js +139 -0
- package/ui/workbench/app.css +15 -7
- package/ui/workbench/app.js +118 -16
- package/ui/workbench/index.html +2 -0
- package/ui/workbench/inspection.js +51 -0
- package/ui/workbench/navigation.js +44 -0
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
|
}
|
|
@@ -127,7 +131,7 @@ function importRun(exportPath, targetDir) {
|
|
|
127
131
|
const oldRunDir = raw.run.paths.runDir;
|
|
128
132
|
const oldCwd = raw.run.cwd;
|
|
129
133
|
const paths = (0, run_paths_1.createRunPaths)(runDir);
|
|
130
|
-
(0,
|
|
134
|
+
(0, run_store_1.ensureRunDirs)(paths);
|
|
131
135
|
for (const file of files) {
|
|
132
136
|
const destination = path.join(runDir, file.relativePath);
|
|
133
137
|
if (!(0, fs_atomic_1.isContainedPath)(destination, runDir)) {
|
|
@@ -164,6 +168,66 @@ function importRun(exportPath, targetDir) {
|
|
|
164
168
|
verification,
|
|
165
169
|
};
|
|
166
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
|
+
}
|
|
167
231
|
/** Verify an imported run against its restore manifest and telemetry
|
|
168
232
|
* chain. */
|
|
169
233
|
function verifyImportedRun(run) {
|
|
@@ -239,12 +303,21 @@ function inspectArchive(archivePath) {
|
|
|
239
303
|
archiveSha256: null,
|
|
240
304
|
checks: [],
|
|
241
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
|
+
}
|
|
242
313
|
let bytes;
|
|
243
314
|
try {
|
|
244
|
-
bytes =
|
|
315
|
+
bytes = readArchiveBytes(archivePath, limits);
|
|
245
316
|
}
|
|
246
317
|
catch (error) {
|
|
247
|
-
|
|
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 }] };
|
|
248
321
|
}
|
|
249
322
|
base.archiveSha256 = (0, hash_1.sha256Bytes)(bytes);
|
|
250
323
|
let raw;
|
|
@@ -263,7 +336,23 @@ function inspectArchive(archivePath) {
|
|
|
263
336
|
}
|
|
264
337
|
try {
|
|
265
338
|
const files = normalizeArchiveFiles(raw);
|
|
266
|
-
|
|
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;
|
|
267
356
|
if (!raw.integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
268
357
|
checks.push({ name: "archive-integrity", pass: false, code: "archive-integrity-required" });
|
|
269
358
|
}
|
|
@@ -325,12 +414,12 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
325
414
|
reportFindingsVerified: false,
|
|
326
415
|
failedChecks,
|
|
327
416
|
};
|
|
328
|
-
if (!inspect.schemaSupported)
|
|
417
|
+
if (!inspect.schemaSupported || inspect.checks.some((check) => check.code?.startsWith("archive-limit-")))
|
|
329
418
|
return base;
|
|
330
419
|
let bundleKey;
|
|
331
420
|
let reportContent;
|
|
332
421
|
try {
|
|
333
|
-
const raw = JSON.parse(
|
|
422
|
+
const raw = JSON.parse(readArchiveBytes(archivePath, archiveIntakeLimits()).toString("utf8"));
|
|
334
423
|
bundleKey = raw.trust?.publicKeyPem;
|
|
335
424
|
if (options.extractReportTo) {
|
|
336
425
|
const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
|
|
@@ -565,28 +654,133 @@ function roleForRelativePath(relativePath) {
|
|
|
565
654
|
return "run-file";
|
|
566
655
|
}
|
|
567
656
|
function normalizeArchiveFiles(raw) {
|
|
568
|
-
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 || []);
|
|
569
662
|
if (modern.length) {
|
|
570
|
-
return modern.map(
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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");
|
|
580
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}`);
|
|
581
679
|
const bytes = Buffer.from(contentBase64, "base64");
|
|
582
680
|
return {
|
|
583
681
|
relativePath: cleanArchiveRelativePath(artifact.path),
|
|
584
682
|
role: "artifact",
|
|
585
683
|
contentBase64,
|
|
586
|
-
sha256: artifact.sha256
|
|
587
|
-
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,
|
|
588
686
|
};
|
|
589
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;
|
|
590
784
|
}
|
|
591
785
|
function decodeBase64StrictResult(value, relativePath) {
|
|
592
786
|
if (typeof value !== "string") {
|
|
@@ -610,8 +804,9 @@ function decodeBase64Strict(value, relativePath) {
|
|
|
610
804
|
throw new Error(archiveCheckMessage(decoded.check));
|
|
611
805
|
return decoded.bytes;
|
|
612
806
|
}
|
|
613
|
-
function collectArchiveDigestChecks(files, integrity) {
|
|
807
|
+
function collectArchiveDigestChecks(files, integrity, limits = {}) {
|
|
614
808
|
const checks = [];
|
|
809
|
+
let decodedTotal = 0;
|
|
615
810
|
for (const file of files) {
|
|
616
811
|
const decoded = decodeBase64StrictResult(file.contentBase64, file.relativePath);
|
|
617
812
|
if (!decoded.ok) {
|
|
@@ -619,6 +814,12 @@ function collectArchiveDigestChecks(files, integrity) {
|
|
|
619
814
|
continue;
|
|
620
815
|
}
|
|
621
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
|
+
}
|
|
622
823
|
const actual = (0, hash_1.sha256Bytes)(bytes);
|
|
623
824
|
const digestOk = actual === file.sha256;
|
|
624
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 });
|
|
@@ -650,11 +851,11 @@ function archiveCheckMessage(check) {
|
|
|
650
851
|
return `Archive verification failed: ${check.name}`;
|
|
651
852
|
}
|
|
652
853
|
}
|
|
653
|
-
function verifyArchiveFileDigests(files, integrity) {
|
|
854
|
+
function verifyArchiveFileDigests(files, integrity, limits = {}) {
|
|
654
855
|
if (!integrity && /^(1|true|yes|on)$/i.test(process.env.CW_REQUIRE_ARCHIVE_INTEGRITY || "")) {
|
|
655
856
|
throw new Error("Archive integrity block required but absent (CW_REQUIRE_ARCHIVE_INTEGRITY=1)");
|
|
656
857
|
}
|
|
657
|
-
const failed = collectArchiveDigestChecks(files, integrity).checks.find((c) => !c.pass);
|
|
858
|
+
const failed = collectArchiveDigestChecks(files, integrity, limits).checks.find((c) => !c.pass);
|
|
658
859
|
if (failed)
|
|
659
860
|
throw new Error(archiveCheckMessage(failed));
|
|
660
861
|
}
|
|
@@ -679,6 +880,16 @@ function rebaseRun(source, context) {
|
|
|
679
880
|
: cloned.audit;
|
|
680
881
|
return cloned;
|
|
681
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
|
+
}
|
|
682
893
|
function deepRebase(value, context) {
|
|
683
894
|
if (typeof value === "string")
|
|
684
895
|
return rebaseString(value, context);
|
|
@@ -705,10 +916,13 @@ function rebaseString(value, context) {
|
|
|
705
916
|
return value;
|
|
706
917
|
}
|
|
707
918
|
function cleanArchiveRelativePath(value) {
|
|
708
|
-
const
|
|
709
|
-
if (
|
|
919
|
+
const portable = value.replace(/\\/g, "/");
|
|
920
|
+
if (portable.startsWith("/") || /^[A-Za-z]:\//.test(portable) || /(^|\/)\.\.(\/|$)/.test(portable) || /[\u0000-\u001f\u007f]/.test(portable)) {
|
|
710
921
|
throw new Error(`Invalid archive relative path: ${value}`);
|
|
711
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}`);
|
|
712
926
|
return cleaned;
|
|
713
927
|
}
|
|
714
928
|
function toArchivePath(value) {
|
|
@@ -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");
|
package/dist/shell/run-store.js
CHANGED
|
@@ -44,7 +44,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
};
|
|
45
45
|
})();
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.
|
|
47
|
+
exports.createRunPaths = void 0;
|
|
48
|
+
exports.ensureRunDirs = ensureRunDirs;
|
|
48
49
|
exports.hashArtifactFile = hashArtifactFile;
|
|
49
50
|
exports.loadRunStateFile = loadRunStateFile;
|
|
50
51
|
exports.checkRunStateFile = checkRunStateFile;
|
|
@@ -63,9 +64,32 @@ const path = __importStar(require("node:path"));
|
|
|
63
64
|
const fs_atomic_1 = require("./fs-atomic");
|
|
64
65
|
const run_paths_1 = require("../core/state/run-paths");
|
|
65
66
|
Object.defineProperty(exports, "createRunPaths", { enumerable: true, get: function () { return run_paths_1.createRunPaths; } });
|
|
66
|
-
Object.defineProperty(exports, "ensureRunDirs", { enumerable: true, get: function () { return run_paths_1.ensureRunDirs; } });
|
|
67
67
|
const migrations_1 = require("../core/state/migrations");
|
|
68
68
|
const hash_1 = require("../core/hash");
|
|
69
|
+
/** `mkdirSync` (recursive) every dir this run needs. Missing optional dir
|
|
70
|
+
* fields fall back to `path.join(runDir, "<name>")`, matching the old
|
|
71
|
+
* build's defensive default (a RunPaths loaded from an old/partial
|
|
72
|
+
* state.json may be missing an optional key). */
|
|
73
|
+
function ensureRunDirs(paths) {
|
|
74
|
+
const dirs = [
|
|
75
|
+
paths.runDir,
|
|
76
|
+
paths.tasksDir,
|
|
77
|
+
paths.resultsDir,
|
|
78
|
+
paths.dispatchesDir,
|
|
79
|
+
paths.artifactsDir,
|
|
80
|
+
paths.commitsDir,
|
|
81
|
+
paths.stateNodesDir,
|
|
82
|
+
paths.feedbackDir,
|
|
83
|
+
paths.auditDir || path.join(paths.runDir, "audit"),
|
|
84
|
+
paths.workersDir || path.join(paths.runDir, "workers"),
|
|
85
|
+
paths.candidatesDir || path.join(paths.runDir, "candidates"),
|
|
86
|
+
paths.multiAgentDir || path.join(paths.runDir, "multi-agent"),
|
|
87
|
+
paths.blackboardDir || path.join(paths.runDir, "blackboard"),
|
|
88
|
+
paths.topologiesDir || path.join(paths.runDir, "topologies"),
|
|
89
|
+
];
|
|
90
|
+
for (const dir of dirs)
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
92
|
+
}
|
|
69
93
|
/** Read the file at artifact.path and stamp sha256 (the core `sha256:`+hex
|
|
70
94
|
* form) + sizeBytes onto the StateArtifact. A missing/unreadable file is
|
|
71
95
|
* silently skipped so hashing an absent artifact never throws. Byte-exact
|
|
@@ -393,7 +417,7 @@ function compactCheckpoint(run) {
|
|
|
393
417
|
* same primitive. */
|
|
394
418
|
function createRun(runDir, runId, workflowId, cwd) {
|
|
395
419
|
const paths = (0, run_paths_1.createRunPaths)(runDir);
|
|
396
|
-
|
|
420
|
+
ensureRunDirs(paths);
|
|
397
421
|
const seed = {
|
|
398
422
|
id: runId,
|
|
399
423
|
cwd,
|
|
@@ -176,7 +176,10 @@ function showStateExplosionSummary(run, options = {}) {
|
|
|
176
176
|
// ---------------------------------------------------------------------
|
|
177
177
|
function loadRun(runId, options = {}) {
|
|
178
178
|
const cwd = options.cwd ? path.resolve(String(options.cwd)) : process.cwd();
|
|
179
|
-
|
|
179
|
+
const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
|
|
180
|
+
if (options.__cwWorkbenchReadOnlyProjection === true)
|
|
181
|
+
run.__cwWorkbenchReadOnlyProjection = true;
|
|
182
|
+
return run;
|
|
180
183
|
}
|
|
181
184
|
/** `cw summary refresh <run-id> [--json]` — refresh also runs `writeReport`
|
|
182
185
|
* + `saveCheckpoint` in the old build; `writeReport` (report.md) is a
|
|
@@ -219,7 +219,7 @@ function runBundleDemo(options = {}) {
|
|
|
219
219
|
const runId = "demo-bundle-run";
|
|
220
220
|
const runDir = path.join(workdir, ".cw", "runs", runId);
|
|
221
221
|
const paths = (0, run_paths_1.createRunPaths)(runDir);
|
|
222
|
-
(0,
|
|
222
|
+
(0, run_store_1.ensureRunDirs)(paths);
|
|
223
223
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
224
224
|
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
225
225
|
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
@@ -62,6 +62,7 @@ exports.trustAuditHead = trustAuditHead;
|
|
|
62
62
|
exports.verifyTrustAudit = verifyTrustAudit;
|
|
63
63
|
exports.repairTrustAuditTornTail = repairTrustAuditTornTail;
|
|
64
64
|
exports.recordTrustAuditEvent = recordTrustAuditEvent;
|
|
65
|
+
exports.withTrustAuditBatch = withTrustAuditBatch;
|
|
65
66
|
exports.recordSandboxPathDecision = recordSandboxPathDecision;
|
|
66
67
|
exports.normalizeEvidence = normalizeEvidence;
|
|
67
68
|
exports.writeTrustAuditIndexPlaceholder = writeTrustAuditIndexPlaceholder;
|
|
@@ -114,6 +115,7 @@ function computeEventHash(event) {
|
|
|
114
115
|
void eventHash;
|
|
115
116
|
return (0, hash_1.sha256)((0, hash_1.eventHashInput)(rest));
|
|
116
117
|
}
|
|
118
|
+
const ACTIVE_AUDIT_BATCHES = new Map();
|
|
117
119
|
function tailCachePathFor(eventLogPath) {
|
|
118
120
|
return path.join(path.dirname(eventLogPath), "tail-cache.json");
|
|
119
121
|
}
|
|
@@ -474,49 +476,90 @@ function recordTrustAuditEvent(run, input) {
|
|
|
474
476
|
parentEventIds: unique(input.parentEventIds || []).sort(),
|
|
475
477
|
metadata: scrubMetadata(input.metadata || {}),
|
|
476
478
|
});
|
|
479
|
+
const batch = ACTIVE_AUDIT_BATCHES.get(path.resolve(audit.eventLogPath));
|
|
480
|
+
if (batch)
|
|
481
|
+
return appendTrustAuditEvent(run, input, event, batch);
|
|
477
482
|
return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
|
|
478
|
-
// The prior event count and last-event hash are the ONLY two things
|
|
479
|
-
// this append needs from the existing log. A tail cache (keyed on the
|
|
480
|
-
// log's own byte size) serves both without a full parse when nothing
|
|
481
|
-
// else has touched the log since it was written; any size mismatch
|
|
482
|
-
// (a repair, a torn write, this being the very first append) falls
|
|
483
|
-
// back to the full parse, same as before this cache existed.
|
|
484
|
-
const tailCachePath = tailCachePathFor(audit.eventLogPath);
|
|
485
483
|
const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
|
|
486
|
-
const cached = readAuditTailCache(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
484
|
+
const cached = readAuditTailCache(tailCachePathFor(audit.eventLogPath));
|
|
485
|
+
const prior = cached && cached.logBytes === currentBytes ? undefined : readEventsRaw(audit.eventLogPath);
|
|
486
|
+
const state = {
|
|
487
|
+
audit,
|
|
488
|
+
currentBytes,
|
|
489
|
+
count: cached && cached.logBytes === currentBytes ? cached.count : prior.length,
|
|
490
|
+
lastHash: cached && cached.logBytes === currentBytes ? cached.lastHash : (prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id)),
|
|
491
|
+
lines: [],
|
|
492
|
+
};
|
|
493
|
+
const result = appendTrustAuditEvent(run, input, event, state);
|
|
494
|
+
flushTrustAuditBatch(state);
|
|
495
|
+
return result;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function appendTrustAuditEvent(run, input, event, batch) {
|
|
499
|
+
// The prior event count and last-event hash are the ONLY two things
|
|
500
|
+
// this append needs from the existing log. A tail cache (keyed on the
|
|
501
|
+
// log's own byte size) serves both without a full parse when nothing
|
|
502
|
+
// else has touched the log since it was written; any size mismatch
|
|
503
|
+
// (a repair, a torn write, this being the very first append) falls
|
|
504
|
+
// back to the full parse, same as before this cache existed.
|
|
505
|
+
event.id = createEventId(input.kind, batch.count);
|
|
506
|
+
event.prevEventHash = batch.lastHash;
|
|
507
|
+
event.eventHash = computeEventHash(event);
|
|
508
|
+
// Newline-boundary safety (fail-closed). `durableAppendFileSync` only ever
|
|
509
|
+
// ADDS bytes at the end of file and never writes a separator of its own. A
|
|
510
|
+
// completed append always leaves the log ending in "\n"; if the last byte
|
|
511
|
+
// is NOT "\n", the previous append was torn by a crash (its bytes were
|
|
512
|
+
// never a confirmed event — the append never returned). Writing this new,
|
|
513
|
+
// already-cross-linked event straight onto that partial byte-run would
|
|
514
|
+
// MERGE the two into one line that no longer parses — losing THIS event and
|
|
515
|
+
// poisoning the forward chain (the next append's prevEventHash would point
|
|
516
|
+
// into an unparseable blob), with no repair for that shape. So put the new
|
|
517
|
+
// event on its own clean line: prepend a "\n" when the log does not already
|
|
518
|
+
// end in one, confining any crash artifact to its own now-orphaned line.
|
|
519
|
+
// (Reads only the last byte, so the O(1) tail-cache path is preserved.)
|
|
520
|
+
// An empty log (currentBytes === 0, e.g. the first append) has no tail to
|
|
521
|
+
// merge with, so it never needs a leading newline.
|
|
522
|
+
const leadingNewline = batch.currentBytes > 0 && batch.lines.length === 0 && !(0, fs_atomic_1.logEndsWithNewline)(batch.audit.eventLogPath, batch.currentBytes) ? "\n" : "";
|
|
523
|
+
const line = `${leadingNewline}${JSON.stringify(event)}\n`;
|
|
524
|
+
batch.lines.push(line);
|
|
525
|
+
batch.currentBytes += Buffer.byteLength(line, "utf8");
|
|
526
|
+
batch.count += 1;
|
|
527
|
+
batch.lastHash = event.eventHash;
|
|
528
|
+
return event;
|
|
529
|
+
}
|
|
530
|
+
function flushTrustAuditBatch(batch) {
|
|
531
|
+
if (!batch.lines.length)
|
|
532
|
+
return;
|
|
533
|
+
(0, fs_atomic_1.durableAppendFileSync)(batch.audit.eventLogPath, batch.lines.join(""));
|
|
534
|
+
writeAuditTailCache(tailCachePathFor(batch.audit.eventLogPath), { schemaVersion: 1, logBytes: batch.currentBytes, count: batch.count, lastHash: batch.lastHash });
|
|
535
|
+
}
|
|
536
|
+
/** Run a short, synchronous mutation group under one audit lock and append its
|
|
537
|
+
* exact NDJSON lines with one durable write before the caller checkpoints. */
|
|
538
|
+
function withTrustAuditBatch(run, fn) {
|
|
539
|
+
const audit = ensureTrustAudit(run);
|
|
540
|
+
const key = path.resolve(audit.eventLogPath);
|
|
541
|
+
if (ACTIVE_AUDIT_BATCHES.has(key))
|
|
542
|
+
return fn();
|
|
543
|
+
return (0, fs_atomic_1.withFileLock)(audit.eventLogPath, () => {
|
|
544
|
+
const currentBytes = fs.existsSync(audit.eventLogPath) ? fs.statSync(audit.eventLogPath).size : 0;
|
|
545
|
+
const cached = readAuditTailCache(tailCachePathFor(audit.eventLogPath));
|
|
546
|
+
const prior = cached && cached.logBytes === currentBytes ? undefined : readEventsRaw(audit.eventLogPath);
|
|
547
|
+
const batch = {
|
|
548
|
+
audit,
|
|
549
|
+
currentBytes,
|
|
550
|
+
count: cached && cached.logBytes === currentBytes ? cached.count : prior.length,
|
|
551
|
+
lastHash: cached && cached.logBytes === currentBytes ? cached.lastHash : (prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id)),
|
|
552
|
+
lines: [],
|
|
553
|
+
};
|
|
554
|
+
ACTIVE_AUDIT_BATCHES.set(key, batch);
|
|
555
|
+
try {
|
|
556
|
+
const result = fn();
|
|
557
|
+
flushTrustAuditBatch(batch);
|
|
558
|
+
return result;
|
|
492
559
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
count = prior.length;
|
|
496
|
-
prevHash = prior.length ? prior[prior.length - 1].eventHash || computeEventHash(prior[prior.length - 1]) : trustAuditGenesis(run.id);
|
|
560
|
+
finally {
|
|
561
|
+
ACTIVE_AUDIT_BATCHES.delete(key);
|
|
497
562
|
}
|
|
498
|
-
event.id = createEventId(input.kind, count);
|
|
499
|
-
event.prevEventHash = prevHash;
|
|
500
|
-
event.eventHash = computeEventHash(event);
|
|
501
|
-
// Newline-boundary safety (fail-closed). `durableAppendFileSync` only ever
|
|
502
|
-
// ADDS bytes at the end of file and never writes a separator of its own. A
|
|
503
|
-
// completed append always leaves the log ending in "\n"; if the last byte
|
|
504
|
-
// is NOT "\n", the previous append was torn by a crash (its bytes were
|
|
505
|
-
// never a confirmed event — the append never returned). Writing this new,
|
|
506
|
-
// already-cross-linked event straight onto that partial byte-run would
|
|
507
|
-
// MERGE the two into one line that no longer parses — losing THIS event and
|
|
508
|
-
// poisoning the forward chain (the next append's prevEventHash would point
|
|
509
|
-
// into an unparseable blob), with no repair for that shape. So put the new
|
|
510
|
-
// event on its own clean line: prepend a "\n" when the log does not already
|
|
511
|
-
// end in one, confining any crash artifact to its own now-orphaned line.
|
|
512
|
-
// (Reads only the last byte, so the O(1) tail-cache path is preserved.)
|
|
513
|
-
// An empty log (currentBytes === 0, e.g. the first append) has no tail to
|
|
514
|
-
// merge with, so it never needs a leading newline.
|
|
515
|
-
const leadingNewline = currentBytes > 0 && !(0, fs_atomic_1.logEndsWithNewline)(audit.eventLogPath, currentBytes) ? "\n" : "";
|
|
516
|
-
const line = `${leadingNewline}${JSON.stringify(event)}\n`;
|
|
517
|
-
(0, fs_atomic_1.durableAppendFileSync)(audit.eventLogPath, line);
|
|
518
|
-
writeAuditTailCache(tailCachePath, { schemaVersion: 1, logBytes: currentBytes + Buffer.byteLength(line, "utf8"), count: count + 1, lastHash: event.eventHash });
|
|
519
|
-
return event;
|
|
520
563
|
});
|
|
521
564
|
}
|
|
522
565
|
function recordSandboxPathDecision(run, input) {
|
|
@@ -711,8 +754,12 @@ function writeSummaryFingerprint(fingerprintPath, fingerprint) {
|
|
|
711
754
|
// -- it just means the next call falls back to a real rewrite.
|
|
712
755
|
}
|
|
713
756
|
}
|
|
714
|
-
function summarizeTrustAudit(run) {
|
|
715
|
-
const
|
|
757
|
+
function summarizeTrustAudit(run, options = {}) {
|
|
758
|
+
const persist = options.persist !== false;
|
|
759
|
+
// A read-only projection must not call ensureTrustAudit(): that helper is
|
|
760
|
+
// deliberately for mutation paths and creates the audit directory and an
|
|
761
|
+
// empty event log when they are absent.
|
|
762
|
+
const audit = persist ? ensureTrustAudit(run) : { schemaVersion: 1, ...trustAuditPaths(run) };
|
|
716
763
|
const { events, corruptLines } = readEventsRawCounted(audit.eventLogPath);
|
|
717
764
|
const ma = run.multiAgent;
|
|
718
765
|
const bb = run.blackboard;
|
|
@@ -797,11 +844,12 @@ function summarizeTrustAudit(run) {
|
|
|
797
844
|
const fingerprintPath = summaryFingerprintPathFor(audit.summaryPath);
|
|
798
845
|
const freshFingerprint = (0, hash_1.stableHash)({ summary: summaryForFingerprint, index });
|
|
799
846
|
const priorFingerprint = readSummaryFingerprint(fingerprintPath);
|
|
800
|
-
if (priorFingerprint !== freshFingerprint) {
|
|
847
|
+
if (persist && priorFingerprint !== freshFingerprint) {
|
|
801
848
|
(0, fs_atomic_1.writeJson)(audit.summaryPath, summary, { durable: true });
|
|
802
849
|
(0, fs_atomic_1.writeJson)(audit.indexPath, index, { durable: true });
|
|
803
850
|
writeSummaryFingerprint(fingerprintPath, { schemaVersion: 1, hash: freshFingerprint });
|
|
804
851
|
}
|
|
805
|
-
|
|
852
|
+
if (persist)
|
|
853
|
+
run.audit = { schemaVersion: exports.TRUST_AUDIT_SCHEMA_VERSION, ...audit };
|
|
806
854
|
return summary;
|
|
807
855
|
}
|
package/dist/shell/workbench.js
CHANGED
|
@@ -147,7 +147,10 @@ function buildWorkbenchRunView(runId, args = {}) {
|
|
|
147
147
|
lifecycle = undefined;
|
|
148
148
|
}
|
|
149
149
|
const panels = {};
|
|
150
|
-
|
|
150
|
+
// This is an internal mechanism flag, not a CLI/MCP option. The Workbench
|
|
151
|
+
// must use the same capability bodies as the other front doors, but its
|
|
152
|
+
// GET-only projection must not make derived audit or metrics files.
|
|
153
|
+
const panelArgs = { ...args, runId, cwd, __cwWorkbenchReadOnlyProjection: true };
|
|
151
154
|
for (const [group, members] of Object.entries(PANEL_MAP)) {
|
|
152
155
|
panels[group] = {};
|
|
153
156
|
for (const [member, capability] of Object.entries(members)) {
|