cool-workflow 0.1.83 → 0.1.85
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 +6 -0
- 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/capability-core.js +89 -2
- package/dist/capability-registry.js +269 -2
- package/dist/cli/command-surface.js +1362 -0
- package/dist/cli.js +2 -1318
- package/dist/mcp-server.js +5 -1452
- package/dist/mcp-surface.js +1467 -0
- package/dist/run-export.js +139 -1
- package/dist/telemetry-demo.js +119 -0
- package/dist/types/report-bundle.js +6 -0
- package/dist/types.js +1 -0
- package/dist/version.js +1 -1
- package/dist/worker-accept/acceptance.js +114 -0
- package/dist/worker-accept/blackboard-fanout.js +80 -0
- package/dist/worker-accept/blackboard-linkage.js +19 -0
- package/dist/worker-accept/context.js +2 -0
- package/dist/worker-accept/telemetry-ledger.js +116 -0
- package/dist/worker-accept/validation.js +77 -0
- package/dist/worker-accept/verifier-completion.js +73 -0
- package/dist/worker-isolation.js +19 -444
- package/docs/agent-delegation-drive.7.md +6 -0
- package/docs/cli-mcp-parity.7.md +13 -3
- package/docs/contract-migration-tooling.7.md +6 -0
- package/docs/control-plane-scheduling.7.md +6 -0
- package/docs/durable-state-and-locking.7.md +6 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +6 -0
- package/docs/execution-backends.7.md +6 -0
- package/docs/index.md +1 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +6 -0
- package/docs/multi-agent-eval-replay-harness.7.md +6 -0
- package/docs/multi-agent-operator-ux.7.md +6 -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 +17 -6
- package/docs/real-execution-backends.7.md +6 -0
- package/docs/release-and-migration.7.md +6 -0
- package/docs/release-tooling.7.md +16 -1
- package/docs/report-verifiable-bundle.7.md +123 -0
- package/docs/run-registry-control-plane.7.md +6 -0
- package/docs/run-retention-reclamation.7.md +6 -0
- package/docs/state-explosion-management.7.md +6 -0
- package/docs/team-collaboration.7.md +6 -0
- package/docs/web-desktop-workbench.7.md +6 -0
- package/manifest/plugin.manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/bump-version.js +9 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/parity-check.js +27 -57
- package/scripts/release-flow.js +7 -6
- package/scripts/sync-project-index.js +1 -1
package/dist/run-export.js
CHANGED
|
@@ -16,19 +16,26 @@ exports.importRun = importRun;
|
|
|
16
16
|
exports.verifyImportedRun = verifyImportedRun;
|
|
17
17
|
exports.inspectArchive = inspectArchive;
|
|
18
18
|
exports.importManifestPath = importManifestPath;
|
|
19
|
+
exports.verifyReportBundle = verifyReportBundle;
|
|
19
20
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
20
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
23
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
22
24
|
const state_1 = require("./state");
|
|
23
25
|
const version_1 = require("./version");
|
|
24
26
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
27
|
+
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
25
28
|
const trust_audit_1 = require("./trust-audit");
|
|
26
29
|
const compare_1 = require("./compare");
|
|
27
30
|
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
28
|
-
function exportRun(run, outputPath) {
|
|
31
|
+
function exportRun(run, outputPath, options = {}) {
|
|
29
32
|
const exportedAt = new Date().toISOString();
|
|
30
33
|
const files = collectArchiveFiles(run);
|
|
31
34
|
const manifestSha256 = digestManifest(files);
|
|
35
|
+
// Embed ONLY a public key. resolveTrustPublicKey normalizes an inline PEM or a
|
|
36
|
+
// file path down to inline PEM bytes so the archive is self-contained; a value
|
|
37
|
+
// that resolves to nothing (bad path) yields no trust block rather than a throw.
|
|
38
|
+
const trustPublicKeyPem = (0, telemetry_attestation_1.resolveTrustPublicKey)(options.trustPublicKey);
|
|
32
39
|
const exported = {
|
|
33
40
|
schemaVersion: 1,
|
|
34
41
|
exportedAt,
|
|
@@ -39,6 +46,7 @@ function exportRun(run, outputPath) {
|
|
|
39
46
|
fileCount: files.length,
|
|
40
47
|
manifestSha256
|
|
41
48
|
},
|
|
49
|
+
...(trustPublicKeyPem ? { trust: { publicKeyPem: trustPublicKeyPem, algorithm: "ed25519" } } : {}),
|
|
42
50
|
// Legacy field retained so old readers still find an artifact-ish list.
|
|
43
51
|
artifacts: files
|
|
44
52
|
.filter((file) => file.role === "artifact")
|
|
@@ -62,6 +70,7 @@ function exportRun(run, outputPath) {
|
|
|
62
70
|
artifactCount: files.filter((file) => file.role === "artifact").length,
|
|
63
71
|
auditFileCount: files.filter((file) => file.role === "audit").length,
|
|
64
72
|
telemetryIncluded: files.some((file) => file.role === "telemetry"),
|
|
73
|
+
trustKeyEmbedded: Boolean(trustPublicKeyPem),
|
|
65
74
|
manifestSha256,
|
|
66
75
|
archiveSha256
|
|
67
76
|
};
|
|
@@ -276,6 +285,135 @@ function inspectArchive(archivePath) {
|
|
|
276
285
|
function importManifestPath(run) {
|
|
277
286
|
return node_path_1.default.join(run.paths.runDir, "import-manifest.json");
|
|
278
287
|
}
|
|
288
|
+
/** Verify a portable run bundle OFFLINE and SELF-CONTAINED: prove the archive bytes,
|
|
289
|
+
* the telemetry hash chain, the trust-audit chain, and (with the bundle's embedded
|
|
290
|
+
* public key) the ed25519 signatures — WITHOUT a source repo, a pre-existing .cw
|
|
291
|
+
* tree, or an out-of-band key. Reuses the existing import + restore-verify path: it
|
|
292
|
+
* restores into an auto-cleaned tmpdir so a stranger's machine is left untouched.
|
|
293
|
+
* Never throws — every failure is a structured check and a false `ok` (exit-1 worthy).
|
|
294
|
+
*
|
|
295
|
+
* Key precedence is bundle > argument > environment, so the artifact verifies the
|
|
296
|
+
* same on any machine; only when the bundle omits a key do the override/env apply. */
|
|
297
|
+
function verifyReportBundle(archivePath, options = {}) {
|
|
298
|
+
const inspect = inspectArchive(archivePath);
|
|
299
|
+
const failedChecks = inspect.checks
|
|
300
|
+
.filter((check) => !check.pass)
|
|
301
|
+
.map((check) => ({ name: check.name, code: check.code }));
|
|
302
|
+
const base = {
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
archivePath,
|
|
305
|
+
runId: inspect.runId,
|
|
306
|
+
ok: false,
|
|
307
|
+
archiveOk: inspect.ok,
|
|
308
|
+
telemetryVerified: false,
|
|
309
|
+
trustAuditVerified: false,
|
|
310
|
+
trustKeySource: "none",
|
|
311
|
+
signatureKeyProvided: false,
|
|
312
|
+
signaturesChecked: 0,
|
|
313
|
+
signaturesReverified: 0,
|
|
314
|
+
signaturesFailed: 0,
|
|
315
|
+
failedChecks
|
|
316
|
+
};
|
|
317
|
+
// A bundle that is not even a supported archive cannot be restored — report the
|
|
318
|
+
// inspection failure and stop (fail-closed, no tmpdir, nothing written).
|
|
319
|
+
if (!inspect.schemaSupported)
|
|
320
|
+
return base;
|
|
321
|
+
// Read the embedded trust key (and, if requested, the report bytes) straight from
|
|
322
|
+
// the archive JSON. inspectArchive already proved the bytes parse + digest-match.
|
|
323
|
+
let bundleKey;
|
|
324
|
+
let reportContent;
|
|
325
|
+
try {
|
|
326
|
+
const raw = JSON.parse(node_fs_1.default.readFileSync(archivePath, "utf8"));
|
|
327
|
+
bundleKey = raw.trust?.publicKeyPem;
|
|
328
|
+
if (options.extractReportTo) {
|
|
329
|
+
const reportFile = (raw.files || []).find((file) => file.relativePath === "report.md");
|
|
330
|
+
if (reportFile)
|
|
331
|
+
reportContent = Buffer.from(reportFile.contentBase64, "base64").toString("utf8");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* inspect already recorded the parse failure; treat key as absent */
|
|
336
|
+
}
|
|
337
|
+
const trustKeySource = bundleKey
|
|
338
|
+
? "bundle"
|
|
339
|
+
: options.pubkey
|
|
340
|
+
? "argument"
|
|
341
|
+
: process.env.CW_AGENT_ATTEST_PUBKEY
|
|
342
|
+
? "environment"
|
|
343
|
+
: "none";
|
|
344
|
+
const trustKey = (0, telemetry_attestation_1.resolveTrustPublicKey)(bundleKey || options.pubkey || process.env.CW_AGENT_ATTEST_PUBKEY);
|
|
345
|
+
const tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-verify-bundle-"));
|
|
346
|
+
let telemetryVerified = false;
|
|
347
|
+
let trustAuditVerified = false;
|
|
348
|
+
let signaturesChecked = 0;
|
|
349
|
+
let signaturesReverified = 0;
|
|
350
|
+
let signaturesFailed = 0;
|
|
351
|
+
let reportExtractedTo;
|
|
352
|
+
try {
|
|
353
|
+
// Restore into the throwaway tree. importRun digest-checks every file and throws
|
|
354
|
+
// on the first mismatch (or on a stripped integrity block under
|
|
355
|
+
// CW_REQUIRE_ARCHIVE_INTEGRITY=1) — caught below as a failed "restore" check.
|
|
356
|
+
const imported = importRun(archivePath, tmpDir);
|
|
357
|
+
for (const check of imported.verification.checks) {
|
|
358
|
+
if (check.name === "telemetry-ledger")
|
|
359
|
+
telemetryVerified = check.pass;
|
|
360
|
+
if (check.name === "trust-audit")
|
|
361
|
+
trustAuditVerified = check.pass;
|
|
362
|
+
if (!check.pass)
|
|
363
|
+
failedChecks.push({ name: check.name, code: check.code });
|
|
364
|
+
}
|
|
365
|
+
// Independent ed25519 re-verification over the restored ledger using the bundle's
|
|
366
|
+
// own key (or override/env). With no key, attested records degrade to
|
|
367
|
+
// informational (signaturesFailed stays 0); the chain check above still gates ok.
|
|
368
|
+
const ledger = (0, telemetry_ledger_1.verifyTelemetryLedger)(imported.run);
|
|
369
|
+
const sig = (0, telemetry_attestation_1.verifyTelemetrySignatures)(ledger.records, trustKey);
|
|
370
|
+
signaturesChecked = sig.checked;
|
|
371
|
+
signaturesReverified = sig.reverified;
|
|
372
|
+
signaturesFailed = sig.failed;
|
|
373
|
+
for (const check of sig.checks)
|
|
374
|
+
if (!check.pass)
|
|
375
|
+
failedChecks.push({ name: check.name, code: check.code });
|
|
376
|
+
if (options.extractReportTo && reportContent !== undefined) {
|
|
377
|
+
reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
|
|
378
|
+
node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
failedChecks.push({ name: "restore", code: messageOf(error) });
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
// Strict mode: a bundle that claims attested telemetry but offers no key to check
|
|
388
|
+
// it is unverifiable — refuse rather than green it.
|
|
389
|
+
const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
|
|
390
|
+
if (strictShortfall)
|
|
391
|
+
failedChecks.push({ name: "signatures", code: "signature-key-required" });
|
|
392
|
+
// Extraction was requested but could not be fulfilled (no report.md in the bundle,
|
|
393
|
+
// or the write failed): fail closed rather than silently green a missing artifact —
|
|
394
|
+
// otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
|
|
395
|
+
// nothing (or a stale file) with exit 0. A requested-but-absent output is a failure,
|
|
396
|
+
// not a no-op (distinct from extraction never being requested).
|
|
397
|
+
const extractShortfall = Boolean(options.extractReportTo) && !reportExtractedTo;
|
|
398
|
+
if (extractShortfall)
|
|
399
|
+
failedChecks.push({ name: "extract-report", code: "report-md-unavailable" });
|
|
400
|
+
return {
|
|
401
|
+
schemaVersion: 1,
|
|
402
|
+
archivePath,
|
|
403
|
+
runId: inspect.runId,
|
|
404
|
+
ok: inspect.ok && telemetryVerified && trustAuditVerified && signaturesFailed === 0 && !strictShortfall && !extractShortfall,
|
|
405
|
+
archiveOk: inspect.ok,
|
|
406
|
+
telemetryVerified,
|
|
407
|
+
trustAuditVerified,
|
|
408
|
+
trustKeySource,
|
|
409
|
+
signatureKeyProvided: Boolean(trustKey),
|
|
410
|
+
signaturesChecked,
|
|
411
|
+
signaturesReverified,
|
|
412
|
+
signaturesFailed,
|
|
413
|
+
reportExtractedTo,
|
|
414
|
+
failedChecks
|
|
415
|
+
};
|
|
416
|
+
}
|
|
279
417
|
function collectArchiveFiles(run) {
|
|
280
418
|
const entries = new Map();
|
|
281
419
|
for (const file of walkFiles(run.paths.runDir)) {
|
package/dist/telemetry-demo.js
CHANGED
|
@@ -22,12 +22,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
22
22
|
exports.formatTelemetryVerify = formatTelemetryVerify;
|
|
23
23
|
exports.formatTamperDemo = formatTamperDemo;
|
|
24
24
|
exports.runTamperDemo = runTamperDemo;
|
|
25
|
+
exports.runBundleDemo = runBundleDemo;
|
|
26
|
+
exports.formatBundleDemo = formatBundleDemo;
|
|
25
27
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
26
28
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
27
29
|
const node_os_1 = __importDefault(require("node:os"));
|
|
28
30
|
const node_path_1 = __importDefault(require("node:path"));
|
|
29
31
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
30
32
|
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
33
|
+
const run_export_1 = require("./run-export");
|
|
34
|
+
const state_1 = require("./state");
|
|
31
35
|
const execution_backend_1 = require("./execution-backend");
|
|
32
36
|
/** Human-facing render of `telemetry verify <run>`. */
|
|
33
37
|
function formatTelemetryVerify(r) {
|
|
@@ -164,3 +168,118 @@ function runTamperDemo(options = {}) {
|
|
|
164
168
|
layers.every((l) => l.before.verified && !l.after.verified && l.failures.length > 0);
|
|
165
169
|
return { schemaVersion: 1, runId, workers: HOPS.length, trustKey: "ephemeral-ed25519", baseline, layers, proven };
|
|
166
170
|
}
|
|
171
|
+
function runBundleDemo(options = {}) {
|
|
172
|
+
const workdir = options.dir || node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "cw-bundle-demo-"));
|
|
173
|
+
node_fs_1.default.mkdirSync(workdir, { recursive: true });
|
|
174
|
+
const runId = "demo-bundle-run";
|
|
175
|
+
const runDir = node_path_1.default.join(workdir, ".cw", "runs", runId);
|
|
176
|
+
const paths = (0, state_1.createRunPaths)(runDir);
|
|
177
|
+
(0, state_1.ensureRunDirs)(paths);
|
|
178
|
+
const { publicKey, privateKey } = node_crypto_1.default.generateKeyPairSync("ed25519");
|
|
179
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
180
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
181
|
+
// Build a real signed ledger + a cited report, the way an attested run would.
|
|
182
|
+
const ledgerRun = { id: runId, paths };
|
|
183
|
+
for (const hop of HOPS) {
|
|
184
|
+
const ctx = { runId, taskId: hop.taskId, promptDigest: hop.promptDigest };
|
|
185
|
+
(0, telemetry_ledger_1.appendTelemetryAttestation)(ledgerRun, {
|
|
186
|
+
workerId: hop.workerId,
|
|
187
|
+
taskId: hop.taskId,
|
|
188
|
+
promptDigest: hop.promptDigest,
|
|
189
|
+
reportedUsage: hop.usage,
|
|
190
|
+
usageSignature: hop.attestation === "attested" ? (0, telemetry_attestation_1.signTelemetry)(hop.usage, privateKeyPem, ctx) : undefined,
|
|
191
|
+
attestation: hop.attestation,
|
|
192
|
+
now: DEMO_NOW
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(runDir, "report.md"), "# Architecture review\n\nRisk: src/server.js:18 — unauthenticated route.\n", "utf8");
|
|
196
|
+
const attestedCount = HOPS.filter((h) => h.attestation === "attested").length;
|
|
197
|
+
const fullRun = {
|
|
198
|
+
schemaVersion: 1, id: runId, createdAt: DEMO_NOW, updatedAt: DEMO_NOW, cwd: workdir,
|
|
199
|
+
workflow: { id: "demo", title: "Demo", summary: "", limits: { maxAgents: 1, maxConcurrentAgents: 1 } },
|
|
200
|
+
inputs: { question: "what are the risks?" }, loopStage: "interpret",
|
|
201
|
+
phases: [], tasks: [], dispatches: [], commits: [], paths, nodes: [], contracts: []
|
|
202
|
+
};
|
|
203
|
+
(0, state_1.saveCheckpoint)(fullRun);
|
|
204
|
+
const ledgerFile = (0, telemetry_ledger_1.telemetryLedgerPath)(ledgerRun);
|
|
205
|
+
const cleanLedger = node_fs_1.default.readFileSync(ledgerFile, "utf8");
|
|
206
|
+
const exportSealed = (out) => { (0, run_export_1.exportRun)(fullRun, out, { trustPublicKey: publicKeyPem }); };
|
|
207
|
+
// Baseline: a clean sealed bundle verifies offline; the embedded key reverifies
|
|
208
|
+
// every signed hop.
|
|
209
|
+
const cleanBundle = node_path_1.default.join(workdir, "clean.cwrun.json");
|
|
210
|
+
exportSealed(cleanBundle);
|
|
211
|
+
const clean = (0, run_export_1.verifyReportBundle)(cleanBundle);
|
|
212
|
+
const baseline = { ok: clean.ok, telemetryVerified: clean.telemetryVerified, signaturesReverified: clean.signaturesReverified };
|
|
213
|
+
const layers = [];
|
|
214
|
+
// CHAIN forgery: flip record[1]'s verdict and reseal its recordHash; record[2]'s
|
|
215
|
+
// prevHash still points at the original hash, so the chain breaks — even though
|
|
216
|
+
// every archive file digest (computed at export over the tampered bytes) is valid.
|
|
217
|
+
// This is exactly what inspect-archive alone cannot catch.
|
|
218
|
+
{
|
|
219
|
+
const j = JSON.parse(cleanLedger);
|
|
220
|
+
j.records[1].attestation = "attested";
|
|
221
|
+
const { recordHash: _drop, ...rest } = j.records[1];
|
|
222
|
+
j.records[1].recordHash = (0, telemetry_ledger_1.computeRecordHash)(rest);
|
|
223
|
+
node_fs_1.default.writeFileSync(ledgerFile, JSON.stringify(j, null, 2));
|
|
224
|
+
const forged = node_path_1.default.join(workdir, "forged-chain.cwrun.json");
|
|
225
|
+
exportSealed(forged);
|
|
226
|
+
const after = (0, run_export_1.verifyReportBundle)(forged);
|
|
227
|
+
node_fs_1.default.writeFileSync(ledgerFile, cleanLedger);
|
|
228
|
+
layers.push({
|
|
229
|
+
layer: "chain",
|
|
230
|
+
tamper: `forged record[1] verdict "unattested" -> "attested" and resealed its recordHash; the archive's own file digests stay valid`,
|
|
231
|
+
before: { ok: clean.ok, detail: `${clean.signaturesReverified} signed hop(s) reverify; chain intact` },
|
|
232
|
+
after: { ok: after.ok, detail: after.telemetryVerified ? "telemetry chain still verified (UNDETECTED!)" : "the embedded hash chain broke at the next record" },
|
|
233
|
+
failures: after.ok ? [] : after.failedChecks.map((c) => `${c.name}: ${c.code}`)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
// SIGNATURE forgery: inflate the last attested hop's reported tokens and reseal its
|
|
237
|
+
// usage digest + recordHash so the chain AND archive digests still verify; only the
|
|
238
|
+
// ed25519 signature (over the original usage) no longer matches the inflated number.
|
|
239
|
+
{
|
|
240
|
+
const j = JSON.parse(cleanLedger);
|
|
241
|
+
const idx = j.records.length - 1;
|
|
242
|
+
j.records[idx].reportedUsage = { ...j.records[idx].reportedUsage, output_tokens: j.records[idx].reportedUsage.output_tokens * 10 };
|
|
243
|
+
j.records[idx].reportedUsageDigest = (0, telemetry_ledger_1.reportedUsageDigest)(j.records[idx].reportedUsage);
|
|
244
|
+
const { recordHash: _drop, ...rest } = j.records[idx];
|
|
245
|
+
j.records[idx].recordHash = (0, telemetry_ledger_1.computeRecordHash)(rest);
|
|
246
|
+
node_fs_1.default.writeFileSync(ledgerFile, JSON.stringify(j, null, 2));
|
|
247
|
+
const forged = node_path_1.default.join(workdir, "forged-sig.cwrun.json");
|
|
248
|
+
exportSealed(forged);
|
|
249
|
+
const after = (0, run_export_1.verifyReportBundle)(forged);
|
|
250
|
+
node_fs_1.default.writeFileSync(ledgerFile, cleanLedger);
|
|
251
|
+
layers.push({
|
|
252
|
+
layer: "signature",
|
|
253
|
+
tamper: `inflated the last attested hop's output_tokens 10x and resealed its digest + recordHash; the chain stays valid`,
|
|
254
|
+
before: { ok: clean.ok, detail: `the embedded public key reverifies the original signature` },
|
|
255
|
+
after: { ok: after.ok, detail: after.signaturesFailed > 0 ? `${after.signaturesFailed} signature(s) failed ed25519 reverify` : "signature still verified (UNDETECTED!)" },
|
|
256
|
+
failures: after.ok ? [] : after.failedChecks.map((c) => `${c.name}: ${c.code}`)
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (!options.keepDir && !options.dir)
|
|
260
|
+
node_fs_1.default.rmSync(workdir, { recursive: true, force: true });
|
|
261
|
+
const proven = baseline.ok &&
|
|
262
|
+
baseline.telemetryVerified &&
|
|
263
|
+
baseline.signaturesReverified === attestedCount &&
|
|
264
|
+
layers.every((l) => l.before.ok && !l.after.ok && l.failures.length > 0);
|
|
265
|
+
return { schemaVersion: 1, runId, workers: HOPS.length, trustKey: "ephemeral-ed25519", baseline, layers, proven };
|
|
266
|
+
}
|
|
267
|
+
function formatBundleDemo(r) {
|
|
268
|
+
const lines = [];
|
|
269
|
+
lines.push(`cw demo bundle — portable-bundle verification proof (hermetic, ${r.trustKey} key)`);
|
|
270
|
+
lines.push("");
|
|
271
|
+
lines.push(`▶ Exported a sealed report bundle: ${r.workers} hops, public key embedded`);
|
|
272
|
+
lines.push(` ${r.baseline.ok ? "✓" : "✗"} bundle verifies offline ${r.baseline.signaturesReverified} signed hop(s) reverify with only the embedded public key`);
|
|
273
|
+
for (const l of r.layers) {
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push(`▶ ${l.layer.toUpperCase()} forgery`);
|
|
276
|
+
lines.push(` edit: ${l.tamper}`);
|
|
277
|
+
lines.push(` before: ${l.before.ok ? "✓ verifies" : "✗"} — ${l.before.detail}`);
|
|
278
|
+
lines.push(` after: ${l.after.ok ? "✓ (UNDETECTED!)" : "✗ DETECTED"} — ${l.after.detail}`);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
lines.push(r.proven
|
|
282
|
+
? "VERDICT: bundle verification holds ✓ — every forgery caught offline with only the bundle's embedded public key. No repo, no server, no key handed over."
|
|
283
|
+
: "VERDICT: PROOF FAILED ✗ — a forged bundle verified. This is a regression in the bundle guarantee.");
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Verifiable report bundle — result shapes shared across the run-export verifier,
|
|
3
|
+
// the capability-core producer (reportBundle), and the quickstart auto-bundle field
|
|
4
|
+
// on QuickstartResult. Kept in types/ (a leaf) so types/drive.ts can reference
|
|
5
|
+
// ReportBundleResult without a types->core import cycle.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/types.js
CHANGED
|
@@ -28,6 +28,7 @@ __exportStar(require("./types/sandbox"), exports);
|
|
|
28
28
|
__exportStar(require("./types/execution-backend"), exports);
|
|
29
29
|
__exportStar(require("./types/boundary"), exports);
|
|
30
30
|
__exportStar(require("./types/drive"), exports);
|
|
31
|
+
__exportStar(require("./types/report-bundle"), exports);
|
|
31
32
|
__exportStar(require("./types/multi-agent"), exports);
|
|
32
33
|
__exportStar(require("./types/topology"), exports);
|
|
33
34
|
__exportStar(require("./types/blackboard"), exports);
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MIN_SUPPORTED_RUN_STATE_SCHEMA_VERSION = exports.LEGACY_RUN_STATE_SCHEMA_VERSION = exports.CURRENT_RUN_STATE_SCHEMA_VERSION = exports.WORKFLOW_APP_SCHEMA_VERSION = exports.CURRENT_COOL_WORKFLOW_VERSION = void 0;
|
|
4
|
-
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.
|
|
4
|
+
exports.CURRENT_COOL_WORKFLOW_VERSION = "0.1.85";
|
|
5
5
|
exports.WORKFLOW_APP_SCHEMA_VERSION = 1;
|
|
6
6
|
exports.CURRENT_RUN_STATE_SCHEMA_VERSION = 1;
|
|
7
7
|
exports.LEGACY_RUN_STATE_SCHEMA_VERSION = 0;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.acceptWorkerResult = acceptWorkerResult;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const state_1 = require("../state");
|
|
10
|
+
const pipeline_contract_1 = require("../pipeline-contract");
|
|
11
|
+
const state_node_1 = require("../state-node");
|
|
12
|
+
const result_normalize_1 = require("../result-normalize");
|
|
13
|
+
const trust_audit_1 = require("../trust-audit");
|
|
14
|
+
/** Step 3 — recordStateNode/audit: the irreversible accept. Records the allowed
|
|
15
|
+
* path decision, copies the result into the run results dir, completes the task,
|
|
16
|
+
* builds + appends the result node, emits the accepted audit event, re-normalizes
|
|
17
|
+
* node evidence against both audit ids, and surfaces the empty-capture warning.
|
|
18
|
+
* Writes destination/pathAuditId/acceptedAuditId/resultNode back into `accept`. */
|
|
19
|
+
function acceptWorkerResult(accept, delegation) {
|
|
20
|
+
const { run, workerId, scope, task, absoluteResultPath, parsedResult } = accept;
|
|
21
|
+
const { agentDelegation } = delegation;
|
|
22
|
+
const pathAudit = (0, trust_audit_1.recordSandboxPathDecision)(run, {
|
|
23
|
+
workerId,
|
|
24
|
+
taskId: task.id,
|
|
25
|
+
sandboxProfileId: scope.sandboxProfileId,
|
|
26
|
+
policySnapshot: scope.sandboxPolicy,
|
|
27
|
+
target: absoluteResultPath,
|
|
28
|
+
decision: "allowed",
|
|
29
|
+
metadata: { operation: "worker-output-acceptance" }
|
|
30
|
+
});
|
|
31
|
+
const destination = node_path_1.default.join(run.paths.resultsDir, `${(0, state_1.safeFileName)(task.id)}.md`);
|
|
32
|
+
node_fs_1.default.mkdirSync(run.paths.resultsDir, { recursive: true });
|
|
33
|
+
node_fs_1.default.copyFileSync(absoluteResultPath, destination);
|
|
34
|
+
task.status = "completed";
|
|
35
|
+
task.completedAt = new Date().toISOString();
|
|
36
|
+
task.resultPath = destination;
|
|
37
|
+
task.loopStage = "observe";
|
|
38
|
+
task.result = parsedResult;
|
|
39
|
+
const evidence = (0, trust_audit_1.normalizeEvidence)(run, parsedResult.evidence.map((entry, index) => ({
|
|
40
|
+
id: `result:${index + 1}`,
|
|
41
|
+
source: "cw:result",
|
|
42
|
+
locator: entry,
|
|
43
|
+
summary: entry
|
|
44
|
+
})), { source: "cw-validated", workerId, taskId: task.id, auditEventIds: [pathAudit.id] });
|
|
45
|
+
const resultNode = (0, state_node_1.appendRunNode)(run, (0, state_node_1.createStateNode)({
|
|
46
|
+
id: `${run.id}:result:${task.id}`,
|
|
47
|
+
kind: "result",
|
|
48
|
+
status: "completed",
|
|
49
|
+
loopStage: "observe",
|
|
50
|
+
inputs: { taskId: task.id, dispatchId: task.dispatchId, workerId },
|
|
51
|
+
outputs: parsedResult,
|
|
52
|
+
artifacts: [
|
|
53
|
+
{ id: "result", kind: "markdown", path: destination },
|
|
54
|
+
{ id: "worker-result", kind: "markdown", path: absoluteResultPath }
|
|
55
|
+
],
|
|
56
|
+
evidence,
|
|
57
|
+
parents: task.dispatchId ? [`${run.id}:dispatch:${task.dispatchId}`] : [task.stateNodeId || `${run.id}:task:${task.id}`],
|
|
58
|
+
contractId: pipeline_contract_1.DEFAULT_PIPELINE_CONTRACT_ID,
|
|
59
|
+
metadata: {
|
|
60
|
+
taskId: task.id,
|
|
61
|
+
workerId,
|
|
62
|
+
workerDir: scope.workerDir,
|
|
63
|
+
sandboxProfileId: scope.sandboxProfileId,
|
|
64
|
+
auditEventIds: [pathAudit.id],
|
|
65
|
+
// Empty-capture warning (v0.1.42): even after robust normalization the result
|
|
66
|
+
// yielded NO findings and NO evidence — surfaced, never silently passed.
|
|
67
|
+
...((0, result_normalize_1.isEmptyCapture)(parsedResult) ? { captureWarning: "no findings or evidence captured from result.md" } : {}),
|
|
68
|
+
// Folded into the snapshotted node body so v0.1.35 replay re-verifies the
|
|
69
|
+
// prompt/result/model digests WITHOUT re-spawning the agent. NOT evidence.
|
|
70
|
+
...(agentDelegation ? { agentDelegation } : {})
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
const acceptedAudit = (0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
74
|
+
kind: "worker.output",
|
|
75
|
+
decision: "accepted",
|
|
76
|
+
source: "cw-validated",
|
|
77
|
+
workerId,
|
|
78
|
+
taskId: task.id,
|
|
79
|
+
nodeId: resultNode.id,
|
|
80
|
+
sandboxProfileId: scope.sandboxProfileId,
|
|
81
|
+
policySnapshot: scope.sandboxPolicy,
|
|
82
|
+
normalizedPath: absoluteResultPath,
|
|
83
|
+
evidence,
|
|
84
|
+
parentEventIds: [pathAudit.id],
|
|
85
|
+
metadata: { destination }
|
|
86
|
+
});
|
|
87
|
+
resultNode.evidence = (0, trust_audit_1.normalizeEvidence)(run, resultNode.evidence, {
|
|
88
|
+
source: "cw-validated",
|
|
89
|
+
workerId,
|
|
90
|
+
taskId: task.id,
|
|
91
|
+
resultNodeId: resultNode.id,
|
|
92
|
+
auditEventIds: [pathAudit.id, acceptedAudit.id]
|
|
93
|
+
});
|
|
94
|
+
(0, state_node_1.appendRunNode)(run, resultNode);
|
|
95
|
+
task.resultNodeId = resultNode.id;
|
|
96
|
+
// Warn (don't silently pass) when a worker's result captured no structured signal
|
|
97
|
+
// at all — the v0.1.41 self-audit's "accepted with evidenceCount:0" failure mode.
|
|
98
|
+
if ((0, result_normalize_1.isEmptyCapture)(parsedResult)) {
|
|
99
|
+
(0, trust_audit_1.recordTrustAuditEvent)(run, {
|
|
100
|
+
kind: "worker.capture-warning",
|
|
101
|
+
decision: "recorded",
|
|
102
|
+
source: "cw-validated",
|
|
103
|
+
workerId,
|
|
104
|
+
taskId: task.id,
|
|
105
|
+
nodeId: resultNode.id,
|
|
106
|
+
parentEventIds: [acceptedAudit.id],
|
|
107
|
+
metadata: { reason: "no findings or evidence captured from result.md", resultPath: destination }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
accept.destination = destination;
|
|
111
|
+
accept.pathAuditId = pathAudit.id;
|
|
112
|
+
accept.acceptedAuditId = acceptedAudit.id;
|
|
113
|
+
accept.resultNode = resultNode;
|
|
114
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fanOutWorkerOutput = fanOutWorkerOutput;
|
|
4
|
+
const coordinator_1 = require("../coordinator");
|
|
5
|
+
const multi_agent_1 = require("../multi-agent");
|
|
6
|
+
const blackboard_linkage_1 = require("./blackboard-linkage");
|
|
7
|
+
/** Step 7 — fanOut: publish the accepted output to the blackboard and record the
|
|
8
|
+
* multi-agent worker output (linking the blackboard message/artifact refs). */
|
|
9
|
+
function fanOutWorkerOutput(accept) {
|
|
10
|
+
const { run, workerId, scope, task, absoluteResultPath, parsedResult, destination, resultNode, verifierNodeId, acceptedAuditId } = accept;
|
|
11
|
+
const blackboardLinks = publishWorkerOutputToBlackboard(run, scope, task, parsedResult.summary, destination, absoluteResultPath, resultNode.evidence, acceptedAuditId);
|
|
12
|
+
(0, multi_agent_1.recordMultiAgentWorkerOutput)(run, {
|
|
13
|
+
workerId,
|
|
14
|
+
taskId: task.id,
|
|
15
|
+
resultNodeId: resultNode.id,
|
|
16
|
+
verifierNodeId,
|
|
17
|
+
evidence: resultNode.evidence,
|
|
18
|
+
artifactPaths: [destination, absoluteResultPath],
|
|
19
|
+
blackboardMessageIds: blackboardLinks.messageIds,
|
|
20
|
+
blackboardArtifactRefIds: blackboardLinks.artifactRefIds
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function publishWorkerOutputToBlackboard(run, scope, task, summary, destination, workerResultPath, evidence, acceptedAuditId) {
|
|
24
|
+
const linkage = (0, blackboard_linkage_1.blackboardLinkage)(run, scope);
|
|
25
|
+
if (!linkage.blackboardId || !linkage.topicIds.length)
|
|
26
|
+
return { messageIds: [], artifactRefIds: [] };
|
|
27
|
+
const topicId = linkage.topicIds[0];
|
|
28
|
+
const artifactRefs = [
|
|
29
|
+
(0, coordinator_1.addBlackboardArtifact)(run, {
|
|
30
|
+
topicId,
|
|
31
|
+
blackboardId: linkage.blackboardId,
|
|
32
|
+
kind: "worker-result",
|
|
33
|
+
path: destination,
|
|
34
|
+
owner: { kind: "worker", id: scope.id },
|
|
35
|
+
author: { kind: "runtime", id: "cw" },
|
|
36
|
+
source: "cw-validated-worker-output",
|
|
37
|
+
provenance: {
|
|
38
|
+
workerId: scope.id,
|
|
39
|
+
taskId: task.id,
|
|
40
|
+
multiAgentRunId: scope.multiAgent?.runId,
|
|
41
|
+
agentGroupId: scope.multiAgent?.groupId,
|
|
42
|
+
agentRoleId: scope.multiAgent?.roleId,
|
|
43
|
+
agentMembershipId: scope.multiAgent?.membershipId,
|
|
44
|
+
auditEventIds: [acceptedAuditId]
|
|
45
|
+
},
|
|
46
|
+
evidenceRefs: evidence.map((entry) => entry.locator || entry.path || entry.summary || entry.id).filter(Boolean),
|
|
47
|
+
auditEventIds: [acceptedAuditId],
|
|
48
|
+
metadata: { workerResultPath }
|
|
49
|
+
})
|
|
50
|
+
];
|
|
51
|
+
const message = (0, coordinator_1.postBlackboardMessage)(run, {
|
|
52
|
+
topicId,
|
|
53
|
+
blackboardId: linkage.blackboardId,
|
|
54
|
+
body: summary,
|
|
55
|
+
author: { kind: "worker", id: scope.id },
|
|
56
|
+
scope: { kind: "worker", id: scope.id },
|
|
57
|
+
artifactRefIds: artifactRefs.map((artifact) => artifact.id),
|
|
58
|
+
evidenceRefs: evidence.map((entry) => entry.locator || entry.path || entry.summary || entry.id).filter(Boolean),
|
|
59
|
+
auditEventIds: [acceptedAuditId],
|
|
60
|
+
links: {
|
|
61
|
+
multiAgentRunId: scope.multiAgent?.runId,
|
|
62
|
+
agentGroupId: scope.multiAgent?.groupId,
|
|
63
|
+
agentRoleId: scope.multiAgent?.roleId,
|
|
64
|
+
agentMembershipId: scope.multiAgent?.membershipId,
|
|
65
|
+
agentFanoutId: scope.multiAgent?.fanoutId,
|
|
66
|
+
taskId: task.id,
|
|
67
|
+
workerId: scope.id,
|
|
68
|
+
auditEventIds: [acceptedAuditId]
|
|
69
|
+
},
|
|
70
|
+
metadata: {
|
|
71
|
+
taskId: task.id,
|
|
72
|
+
resultPath: destination,
|
|
73
|
+
multiAgent: scope.multiAgent
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
messageIds: [message.id],
|
|
78
|
+
artifactRefIds: artifactRefs.map((artifact) => artifact.id)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.blackboardLinkage = blackboardLinkage;
|
|
4
|
+
const helpers_1 = require("../worker-isolation/helpers");
|
|
5
|
+
function blackboardLinkage(run, scope) {
|
|
6
|
+
const membershipId = scope.multiAgent?.membershipId;
|
|
7
|
+
const membership = membershipId ? run.multiAgent?.memberships.find((entry) => entry.id === membershipId) : undefined;
|
|
8
|
+
const group = scope.multiAgent?.groupId ? run.multiAgent?.groups.find((entry) => entry.id === scope.multiAgent?.groupId) : undefined;
|
|
9
|
+
const role = scope.multiAgent?.roleId ? run.multiAgent?.roles.find((entry) => entry.id === scope.multiAgent?.roleId) : undefined;
|
|
10
|
+
const multiAgentRun = scope.multiAgent?.runId ? run.multiAgent?.runs.find((entry) => entry.id === scope.multiAgent?.runId) : undefined;
|
|
11
|
+
const blackboardId = membership?.blackboardId || group?.blackboardId || role?.blackboardId || multiAgentRun?.blackboardId;
|
|
12
|
+
const topicIds = (0, helpers_1.unique)([
|
|
13
|
+
...(membership?.topicIds || []),
|
|
14
|
+
...(group?.topicIds || []),
|
|
15
|
+
...(role?.topicIds || []),
|
|
16
|
+
...(multiAgentRun?.topicIds || [])
|
|
17
|
+
]);
|
|
18
|
+
return { blackboardId, topicIds };
|
|
19
|
+
}
|