cool-workflow 0.1.87 → 0.1.88
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 +107 -71
- 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/agent-config.js +42 -1
- package/dist/capability-core.js +9 -4
- package/dist/capability-registry.js +24 -0
- package/dist/cli/command-surface.js +102 -1
- package/dist/doctor.js +14 -1
- package/dist/drive.js +222 -16
- package/dist/execution-backend.js +4 -4
- package/dist/loop-expansion.js +60 -0
- package/dist/onramp.js +25 -0
- package/dist/orchestrator/lifecycle-operations.js +134 -3
- package/dist/orchestrator.js +24 -76
- package/dist/run-export.js +106 -2
- package/dist/state-node.js +13 -3
- package/dist/state.js +21 -0
- package/dist/telemetry-attestation.js +30 -6
- package/dist/telemetry-demo.js +29 -1
- package/dist/telemetry-ledger.js +6 -0
- package/dist/version.js +1 -1
- package/dist/worker-accept/telemetry-ledger.js +12 -2
- package/dist/workflow-api.js +33 -0
- package/dist/workflow-app-framework.js +20 -0
- package/docs/agent-delegation-drive.7.md +4 -0
- package/docs/capability-topology-registry.7.md +69 -46
- package/docs/cli-mcp-parity.7.md +12 -2
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +4 -0
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/launch/launch-kit.md +9 -9
- 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 +20 -5
- package/docs/readme-v0.1.87-full.md +301 -0
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +4 -0
- package/docs/report-verifiable-bundle.7.md +34 -2
- package/docs/run-registry-control-plane.7.md +14 -0
- package/docs/run-retention-reclamation.7.md +4 -0
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-model.md +6 -4
- package/docs/web-desktop-workbench.7.md +4 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +1 -1
- package/package.json +8 -5
- package/scripts/agents/agent-adapter-core.js +1 -1
- package/scripts/agents/builtin-templates.json +2 -1
- package/scripts/agents/claude-p-agent.js +7 -33
- package/scripts/agents/codex-agent.js +1 -1
- package/scripts/agents/cw-attest-wrap.js +9 -1
- package/scripts/agents/gemini-agent.js +1 -1
- package/scripts/agents/opencode-agent.js +1 -1
- package/scripts/canonical-apps.js +4 -4
- package/scripts/coverage-gate.js +15 -1
- package/scripts/cw.js +0 -0
- package/scripts/dogfood-release.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/release-flow.js +49 -2
- package/tsconfig.json +3 -1
package/dist/orchestrator.js
CHANGED
|
@@ -800,10 +800,20 @@ function parseArgv(argv) {
|
|
|
800
800
|
positionals.push(rest[restIndex]);
|
|
801
801
|
break;
|
|
802
802
|
}
|
|
803
|
-
if (!token.startsWith("
|
|
803
|
+
if (!token.startsWith("-")) {
|
|
804
804
|
positionals.push(token);
|
|
805
805
|
continue;
|
|
806
806
|
}
|
|
807
|
+
if (!token.startsWith("--")) {
|
|
808
|
+
// Single-dash short flag aliases: -q → question, -r → repo, -a → agent-command, -h → help, -v → version
|
|
809
|
+
const shortMap = { q: "question", r: "repo", a: "agent-command", h: "help", v: "version" };
|
|
810
|
+
const flag = token.slice(1);
|
|
811
|
+
// Handle combined short flags like -qr (not common but safe to ignore)
|
|
812
|
+
const key = shortMap[flag] || flag;
|
|
813
|
+
const val = rest[index + 1] && !rest[index + 1].startsWith("-") ? rest[++index] : true;
|
|
814
|
+
appendOption(options, key, val);
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
807
817
|
const withoutPrefix = token.slice(2);
|
|
808
818
|
const equalsIndex = withoutPrefix.indexOf("=");
|
|
809
819
|
let key;
|
|
@@ -829,7 +839,7 @@ exports.KNOWN_COMMANDS = new Set([
|
|
|
829
839
|
"history", "audit-run", "multi-agent", "topology", "summary", "blackboard",
|
|
830
840
|
"coordinator", "metrics", "operator", "sched", "gc", "telemetry",
|
|
831
841
|
"migration", "demo", "workbench", "approve", "reject", "comment", "handoff",
|
|
832
|
-
"graph", "eval"
|
|
842
|
+
"graph", "eval", "version", "update", "fix"
|
|
833
843
|
]);
|
|
834
844
|
/** Levenshtein distance between two short strings. */
|
|
835
845
|
function levenshtein(a, b) {
|
|
@@ -916,82 +926,20 @@ function formatHelp() {
|
|
|
916
926
|
return [
|
|
917
927
|
(0, term_1.bold)("Cool Workflow"),
|
|
918
928
|
"",
|
|
919
|
-
"
|
|
920
|
-
"
|
|
921
|
-
"
|
|
922
|
-
"",
|
|
923
|
-
|
|
924
|
-
" list List available workflow apps",
|
|
925
|
-
" search <keyword> Search workflows by title or description",
|
|
926
|
-
" info <app-id> [--json] Show what a workflow app does and how to run it",
|
|
927
|
-
" doctor [--json] [--onramp] [--fix] Check setup (--fix for consolidated fix commands)",
|
|
928
|
-
" init <id> [--title T] Create a new workflow app",
|
|
929
|
-
" quickstart [app] [...] Plan → drive → report in one command",
|
|
930
|
-
" demo tamper|bundle Prove trust checks work (30s, no agent needed)",
|
|
931
|
-
" man <topic> Show a man page (e.g. cw man release-tooling)",
|
|
932
|
-
"",
|
|
933
|
-
(0, term_1.bold)("Run Management"),
|
|
934
|
-
" plan <id> [--repo P] [--question Q] Create a new run plan",
|
|
935
|
-
" quickstart|audit-run [app] [...] Plan → drive → report in one command",
|
|
936
|
-
" status <run-id> [--json] [--brief] Show run status (--brief for compact summary)",
|
|
937
|
-
" next <run-id> [--limit N] Show pending dispatch tasks",
|
|
938
|
-
" dispatch <run-id> [--limit N] Dispatch tasks to workers",
|
|
939
|
-
" result <run-id> <task-id> <file> Record a task result",
|
|
940
|
-
" state check <run-id> [--write] Validate run state",
|
|
941
|
-
" commit <run-id> <mode> [...] Record a gated commit",
|
|
942
|
-
" report <run-id> [--show|--summary] Show the report (or bundle/verify-bundle)",
|
|
943
|
-
" graph <run-id> [--json] Show operator graph",
|
|
944
|
-
"",
|
|
945
|
-
(0, term_1.bold)("Inspection & Diagnostics"),
|
|
946
|
-
" operator status|report <run-id> [--json] Human-friendly operator panel",
|
|
947
|
-
" metrics show|summary <run-id> [--json] Cost and usage metrics",
|
|
948
|
-
" telemetry verify <run-id> [--pubkey P] Verify tamper-evident telemetry",
|
|
949
|
-
" migration list|check|prove [target] Schema migration tools",
|
|
950
|
-
" gc plan|run|verify [run-id] Garbage collection",
|
|
951
|
-
"",
|
|
952
|
-
(0, term_1.bold)("Audit & Trust"),
|
|
953
|
-
" audit summary|worker|provenance|multi-agent|... <run-id> Trust audit operations",
|
|
954
|
-
" candidate list|register|score|rank|select|reject <run-id> Candidate management",
|
|
955
|
-
" node list|show|graph|snapshot|diff|replay|verify <run-id> State-node inspection",
|
|
956
|
-
" eval snapshot|replay|compare|score|gate|report Eval/replay harness",
|
|
957
|
-
"",
|
|
958
|
-
(0, term_1.bold)("Multi‑Agent & Collaboration"),
|
|
959
|
-
" multi-agent run|status|step|blackboard|score|... <run-id> Multi-agent coordination",
|
|
960
|
-
" topology list|show|validate|apply|summary|graph Topology management",
|
|
961
|
-
" blackboard summary|graph|resolve|topic|message|... <run-id> Blackboard workspace",
|
|
962
|
-
" coordinator summary|decision <run-id> Coordinator interface",
|
|
963
|
-
" summary refresh|show <run-id> State explosion summaries",
|
|
964
|
-
" approve|reject|comment <kind> <run-id> <id> [--reason T] Team approval actions",
|
|
965
|
-
" handoff <kind> <run-id> <id> [--to ROLE] Team handoff",
|
|
966
|
-
" review status|policy <run-id> [--json] Review status",
|
|
967
|
-
"",
|
|
968
|
-
(0, term_1.bold)("Run Registry & Scheduling"),
|
|
969
|
-
" run search|list|show|resume|archive|export|import <id> Cross-repo run management",
|
|
970
|
-
" registry refresh|show [--scope repo|home] [--json] Run registry index",
|
|
971
|
-
" queue add|list|drain|show [queue-id] Work queue operations",
|
|
972
|
-
" history [--scope repo|home] [--json] Run history",
|
|
973
|
-
" schedule create|list|due|complete|... Scheduled tasks",
|
|
974
|
-
" routine create|fire|list|events|delete Event-driven triggers",
|
|
975
|
-
" sched plan|lease|release|complete|... Lease-based scheduling",
|
|
976
|
-
" loop --prompt T [--interval-minutes N] Continuous loop runner",
|
|
929
|
+
" -q \"question\" [-claude|-codex|-deepseek] Ask a question, get a report",
|
|
930
|
+
" version Show version",
|
|
931
|
+
" update Update to latest release",
|
|
932
|
+
" doctor Check setup",
|
|
933
|
+
" fix Show fix commands for setup issues",
|
|
977
934
|
"",
|
|
978
|
-
(0, term_1.bold)("
|
|
979
|
-
"
|
|
980
|
-
"
|
|
981
|
-
"
|
|
982
|
-
"
|
|
983
|
-
"
|
|
984
|
-
" feedback list|show|summary|collect|task|resolve <id> Feedback loop",
|
|
985
|
-
" workbench serve [--port N] | view <run-id> Optional localhost workbench",
|
|
935
|
+
(0, term_1.bold)("Flags"),
|
|
936
|
+
" -q, --question TEXT The task or question to answer",
|
|
937
|
+
" -r, --repo PATH Target repository path (default: .)",
|
|
938
|
+
" -claude Use Claude agent",
|
|
939
|
+
" -codex Use Codex agent",
|
|
940
|
+
" -deepseek Use DeepSeek (via opencode)",
|
|
986
941
|
"",
|
|
987
|
-
|
|
988
|
-
" --json, --format json Machine-readable JSON output",
|
|
989
|
-
" --repo PATH Target repository path",
|
|
990
|
-
" --question TEXT The task or question to answer",
|
|
991
|
-
" --agent-command CMD Agent backend (e.g. builtin:claude, builtin:codex)",
|
|
992
|
-
" --scope repo|home Scope for cross-repo operations",
|
|
993
|
-
" --cwd PATH Working directory override",
|
|
994
|
-
""
|
|
942
|
+
" list|search|info|init|plan|status|next|dispatch|result|state|commit|report|app|sandbox|backend|contract|node|feedback|worker|audit|candidate|review|loop|schedule|routine|registry|run|queue|history|quickstart|audit-run|multi-agent|topology|summary|blackboard|coordinator|metrics|operator|sched|gc|telemetry|migration|demo|workbench|approve|reject|comment|handoff|graph|eval|man|version|update|fix More commands"
|
|
995
943
|
].join("\n");
|
|
996
944
|
}
|
|
997
945
|
function appendOption(options, key, value) {
|
package/dist/run-export.js
CHANGED
|
@@ -26,6 +26,7 @@ const version_1 = require("./version");
|
|
|
26
26
|
const telemetry_ledger_1 = require("./telemetry-ledger");
|
|
27
27
|
const telemetry_attestation_1 = require("./telemetry-attestation");
|
|
28
28
|
const trust_audit_1 = require("./trust-audit");
|
|
29
|
+
const execution_backend_1 = require("./execution-backend");
|
|
29
30
|
const compare_1 = require("./compare");
|
|
30
31
|
/** Export a run to a portable JSON archive with run-local bytes and digests. */
|
|
31
32
|
function exportRun(run, outputPath, options = {}) {
|
|
@@ -84,9 +85,21 @@ function importRun(exportPath, targetDir) {
|
|
|
84
85
|
const archiveSha256 = sha256Bytes(node_fs_1.default.readFileSync(exportPath));
|
|
85
86
|
const files = normalizeArchiveFiles(raw);
|
|
86
87
|
verifyArchiveFileDigests(files, raw.integrity);
|
|
88
|
+
if (!raw.run || typeof raw.run !== "object") {
|
|
89
|
+
throw new Error("Invalid run export: missing run object");
|
|
90
|
+
}
|
|
91
|
+
// The run id from the archive becomes a directory name under the target's
|
|
92
|
+
// runs root; a crafted id like "../../etc" would otherwise escape it. Refuse
|
|
93
|
+
// any id that is not a single safe path segment, then assert containment as
|
|
94
|
+
// defense-in-depth (catches a symlinked runs root too) before any write.
|
|
95
|
+
const runId = (0, state_1.assertSafeRunId)(raw.run.id);
|
|
96
|
+
const runsRoot = node_path_1.default.join(targetDir, ".cw", "runs");
|
|
97
|
+
const runDir = node_path_1.default.join(runsRoot, runId);
|
|
98
|
+
if (!(0, state_1.isContainedPath)(runDir, runsRoot)) {
|
|
99
|
+
throw new Error(`Run id escapes the runs directory: ${JSON.stringify(raw.run.id)}`);
|
|
100
|
+
}
|
|
87
101
|
const oldRunDir = raw.run.paths.runDir;
|
|
88
102
|
const oldCwd = raw.run.cwd;
|
|
89
|
-
const runDir = node_path_1.default.join(targetDir, ".cw", "runs", raw.run.id);
|
|
90
103
|
const paths = (0, state_1.createRunPaths)(runDir);
|
|
91
104
|
(0, state_1.ensureRunDirs)(paths);
|
|
92
105
|
for (const file of files) {
|
|
@@ -294,6 +307,28 @@ function importManifestPath(run) {
|
|
|
294
307
|
*
|
|
295
308
|
* Key precedence is bundle > argument > environment, so the artifact verifies the
|
|
296
309
|
* same on any machine; only when the bundle omits a key do the override/env apply. */
|
|
310
|
+
/** True when report.md embeds `expected` (the trimmed result) at the task's OWN
|
|
311
|
+
* section, exactly as orchestrator/report.ts renderResults emits it: a
|
|
312
|
+
* `### <taskId>` heading, a `Result: <path>` line, then the result body — and the
|
|
313
|
+
* body STARTS WITH `expected`. Anchoring to the section (not a whole-file substring)
|
|
314
|
+
* means a decoy copy buried elsewhere does not satisfy it; matching from the heading
|
|
315
|
+
* forward (rather than to the next heading) means a result body that itself contains
|
|
316
|
+
* `###` cannot break the bound. The `Result:` path is matched loosely since it is
|
|
317
|
+
* host-specific (rebased on import). */
|
|
318
|
+
function reportSectionEmbedsResult(reportMd, taskId, expected) {
|
|
319
|
+
const needle = `### ${taskId}\n`;
|
|
320
|
+
// Walk EVERY `### <taskId>` occurrence — not just the first — so a stray heading
|
|
321
|
+
// inside an earlier task's result body (which is not followed by the `Result:`
|
|
322
|
+
// structure) is skipped rather than mis-anchoring the check (a false positive on a
|
|
323
|
+
// legitimate, fully-signed bundle whose findings contain markdown headings).
|
|
324
|
+
for (let from = reportMd.indexOf(needle); from >= 0; from = reportMd.indexOf(needle, from + 1)) {
|
|
325
|
+
const after = reportMd.slice(from);
|
|
326
|
+
const prefix = after.match(/^### [^\n]*\n\nResult: [^\n]*\n\n/);
|
|
327
|
+
if (prefix && after.slice(prefix[0].length).startsWith(expected))
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
297
332
|
function verifyReportBundle(archivePath, options = {}) {
|
|
298
333
|
const inspect = inspectArchive(archivePath);
|
|
299
334
|
const failedChecks = inspect.checks
|
|
@@ -312,6 +347,8 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
312
347
|
signaturesChecked: 0,
|
|
313
348
|
signaturesReverified: 0,
|
|
314
349
|
signaturesFailed: 0,
|
|
350
|
+
trustLevel: "unsigned",
|
|
351
|
+
reportFindingsVerified: false,
|
|
315
352
|
failedChecks
|
|
316
353
|
};
|
|
317
354
|
// A bundle that is not even a supported archive cannot be restored — report the
|
|
@@ -348,6 +385,8 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
348
385
|
let signaturesChecked = 0;
|
|
349
386
|
let signaturesReverified = 0;
|
|
350
387
|
let signaturesFailed = 0;
|
|
388
|
+
let signaturesResultBound = 0;
|
|
389
|
+
let reportFindingsOk = true;
|
|
351
390
|
let reportExtractedTo;
|
|
352
391
|
try {
|
|
353
392
|
// Restore into the throwaway tree. importRun digest-checks every file and throws
|
|
@@ -370,9 +409,48 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
370
409
|
signaturesChecked = sig.checked;
|
|
371
410
|
signaturesReverified = sig.reverified;
|
|
372
411
|
signaturesFailed = sig.failed;
|
|
412
|
+
signaturesResultBound = sig.resultBound.length;
|
|
373
413
|
for (const check of sig.checks)
|
|
374
414
|
if (!check.pass)
|
|
375
415
|
failedChecks.push({ name: check.name, code: check.code });
|
|
416
|
+
// Report ⇄ result ⇄ signature cross-check — three links so the agent's findings
|
|
417
|
+
// cannot be altered undetected on a signed bundle. CRUCIALLY this is driven by
|
|
418
|
+
// sig.resultBound (the records whose signature actually COVERED the result
|
|
419
|
+
// digest), NOT the run.tasks list — run.tasks is in the archive but bound by
|
|
420
|
+
// nothing (not the manifest digest, the chain, or the signature), so iterating it
|
|
421
|
+
// would let an attacker silence the check by dropping/un-completing a task. The
|
|
422
|
+
// obligation comes from the chained+signed ledger record instead:
|
|
423
|
+
// 1. each bound record's resultDigest is anchored by the executor signature
|
|
424
|
+
// (coversResult re-verified above) — a 4-field signature is excluded, so an
|
|
425
|
+
// injected resultDigest is never trusted;
|
|
426
|
+
// 2. the matching completed task's RESTORED result file must hash to that signed
|
|
427
|
+
// digest (a missing/un-completed task or edited/empty result is a forgery);
|
|
428
|
+
// 3. report.md must embed that result at the task's own `### <taskId>` section.
|
|
429
|
+
// Editing the report breaks link 3; editing the result breaks link 2; editing both
|
|
430
|
+
// to one consistent lie still breaks link 2 (the signed digest does not move);
|
|
431
|
+
// dropping the task fails link 2 (the signed obligation remains).
|
|
432
|
+
const reportPath = imported.run.paths.report;
|
|
433
|
+
const reportMd = reportPath && node_fs_1.default.existsSync(reportPath) ? node_fs_1.default.readFileSync(reportPath, "utf8") : "";
|
|
434
|
+
const completedById = new Map(imported.run.tasks.filter((task) => task.status === "completed").map((task) => [task.id, task]));
|
|
435
|
+
for (const bound of sig.resultBound) {
|
|
436
|
+
const failBound = (code) => {
|
|
437
|
+
reportFindingsOk = false;
|
|
438
|
+
failedChecks.push({ name: "report-findings", code: `${code}:${bound.taskId}` });
|
|
439
|
+
};
|
|
440
|
+
const task = completedById.get(bound.taskId);
|
|
441
|
+
if (!task || !task.resultPath || !node_fs_1.default.existsSync(task.resultPath)) {
|
|
442
|
+
failBound("result-missing");
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const resultRaw = node_fs_1.default.readFileSync(task.resultPath, "utf8");
|
|
446
|
+
if ((0, execution_backend_1.sha256)(resultRaw) !== bound.resultDigest) {
|
|
447
|
+
failBound("result-digest-mismatch");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!reportSectionEmbedsResult(reportMd, bound.taskId, resultRaw.trim())) {
|
|
451
|
+
failBound("report-result-mismatch");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
376
454
|
if (options.extractReportTo && reportContent !== undefined) {
|
|
377
455
|
reportExtractedTo = node_path_1.default.resolve(options.extractReportTo);
|
|
378
456
|
node_fs_1.default.writeFileSync(reportExtractedTo, reportContent);
|
|
@@ -389,6 +467,23 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
389
467
|
const strictShortfall = Boolean(options.strictSignatures) && signaturesChecked > 0 && !trustKey;
|
|
390
468
|
if (strictShortfall)
|
|
391
469
|
failedChecks.push({ name: "signatures", code: "signature-key-required" });
|
|
470
|
+
// Trust level + requireSignatures — closes the prior fail-open. "signed" means the
|
|
471
|
+
// agent's SIGNED findings are present and unaltered: at least one result-COVERING
|
|
472
|
+
// signature re-verified against a key, none failed, and each signed result is
|
|
473
|
+
// faithfully embedded in report.md (the forward cross-check held). A usage-only
|
|
474
|
+
// (4-field) signature, an unverifiable one (no key), or a tampered signed finding
|
|
475
|
+
// all yield "unsigned". requireSignatures refuses "unsigned".
|
|
476
|
+
//
|
|
477
|
+
// SCOPE (honest): "signed" attests the SIGNED findings, NOT that the report is
|
|
478
|
+
// exhaustively signed. CW holds no key to sign the rendered report and the ledger
|
|
479
|
+
// chain is self-recomputable, so the report MAY carry additional unsigned content
|
|
480
|
+
// (prose, or extra sections), and a determined re-chainer can OMIT a signed
|
|
481
|
+
// finding. Verify findings against the signed results; full report-completeness
|
|
482
|
+
// needs an external anchor. See report-verifiable-bundle.7.md / trust-model.md.
|
|
483
|
+
const trustLevel = signaturesResultBound > 0 && signaturesFailed === 0 && reportFindingsOk ? "signed" : "unsigned";
|
|
484
|
+
const unsignedShortfall = Boolean(options.requireSignatures) && trustLevel === "unsigned";
|
|
485
|
+
if (unsignedShortfall)
|
|
486
|
+
failedChecks.push({ name: "signatures", code: "signatures-required" });
|
|
392
487
|
// Extraction was requested but could not be fulfilled (no report.md in the bundle,
|
|
393
488
|
// or the write failed): fail closed rather than silently green a missing artifact —
|
|
394
489
|
// otherwise `report bundle <run> --extract-report r.md && send r.md` would ship
|
|
@@ -401,7 +496,14 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
401
496
|
schemaVersion: 1,
|
|
402
497
|
archivePath,
|
|
403
498
|
runId: inspect.runId,
|
|
404
|
-
ok: inspect.ok &&
|
|
499
|
+
ok: inspect.ok &&
|
|
500
|
+
telemetryVerified &&
|
|
501
|
+
trustAuditVerified &&
|
|
502
|
+
signaturesFailed === 0 &&
|
|
503
|
+
reportFindingsOk &&
|
|
504
|
+
!strictShortfall &&
|
|
505
|
+
!extractShortfall &&
|
|
506
|
+
!unsignedShortfall,
|
|
405
507
|
archiveOk: inspect.ok,
|
|
406
508
|
telemetryVerified,
|
|
407
509
|
trustAuditVerified,
|
|
@@ -410,6 +512,8 @@ function verifyReportBundle(archivePath, options = {}) {
|
|
|
410
512
|
signaturesChecked,
|
|
411
513
|
signaturesReverified,
|
|
412
514
|
signaturesFailed,
|
|
515
|
+
trustLevel,
|
|
516
|
+
reportFindingsVerified: reportFindingsOk,
|
|
413
517
|
reportExtractedTo,
|
|
414
518
|
failedChecks
|
|
415
519
|
};
|
package/dist/state-node.js
CHANGED
|
@@ -164,10 +164,20 @@ function linkStateNodes(parent, child) {
|
|
|
164
164
|
];
|
|
165
165
|
}
|
|
166
166
|
function appendRunNode(run, node) {
|
|
167
|
-
|
|
167
|
+
// Mutate run.nodes in place rather than rebuilding the whole array on every
|
|
168
|
+
// append. The old code did `nodes.map(...)` / `[...nodes, node]` per call, so a
|
|
169
|
+
// run that appends N nodes allocated arrays of size 1..N — O(N^2) churn + GC on
|
|
170
|
+
// a hot path (every dispatch/result/blackboard node). In place: O(1) per append
|
|
171
|
+
// for the common new-node case, and the resulting array — content AND order — is
|
|
172
|
+
// byte-identical to before (push appends at the end, replace keeps the slot), so
|
|
173
|
+
// the persisted state.json is unchanged. appendRunNode is the only writer of
|
|
174
|
+
// run.nodes, so the array reference stays stable across a run.
|
|
175
|
+
const nodes = run.nodes || (run.nodes = []);
|
|
168
176
|
const index = nodes.findIndex((candidate) => candidate.id === node.id);
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
if (index >= 0)
|
|
178
|
+
nodes[index] = node;
|
|
179
|
+
else
|
|
180
|
+
nodes.push(node);
|
|
171
181
|
writeRunNode(run, node);
|
|
172
182
|
return node;
|
|
173
183
|
}
|
package/dist/state.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.realResolve = realResolve;
|
|
|
19
19
|
exports.isContainedPath = isContainedPath;
|
|
20
20
|
exports.withFileLock = withFileLock;
|
|
21
21
|
exports.safeFileName = safeFileName;
|
|
22
|
+
exports.assertSafeRunId = assertSafeRunId;
|
|
22
23
|
exports.hashArtifactFile = hashArtifactFile;
|
|
23
24
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
24
25
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -293,6 +294,26 @@ function withFileLock(targetPath, fn) {
|
|
|
293
294
|
function safeFileName(value) {
|
|
294
295
|
return String(value).replace(/[^a-zA-Z0-9_.:-]+/g, "_");
|
|
295
296
|
}
|
|
297
|
+
/** Refuse a run id that is not a single safe path segment, so an id taken from
|
|
298
|
+
* an untrusted source (an imported run archive / bundle) can never escape the
|
|
299
|
+
* runs directory via a separator or a `..`/`.` component (path traversal). A
|
|
300
|
+
* real run id is `${workflowId}-${stamp}-${suffix}` (createRunId), and a
|
|
301
|
+
* workflow id is alnum-bounded [a-z0-9.-] (validateWorkflowId) — all within
|
|
302
|
+
* [A-Za-z0-9._-]. Because the charset already forbids any separator, the whole
|
|
303
|
+
* id is ONE path component, so the only values that could traverse are the
|
|
304
|
+
* components `.` and `..` themselves; an embedded `..` (e.g. a workflow id like
|
|
305
|
+
* `v1..2`) is a safe directory name and is allowed. A separator, an absolute
|
|
306
|
+
* path, or an empty value is refused, fail-closed. Returns the id on success so
|
|
307
|
+
* callers can use it inline. */
|
|
308
|
+
function assertSafeRunId(value, context = "run id") {
|
|
309
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
310
|
+
throw new Error(`Invalid ${context}: expected a non-empty string`);
|
|
311
|
+
}
|
|
312
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value) || value === "." || value === "..") {
|
|
313
|
+
throw new Error(`Unsafe ${context}: ${JSON.stringify(value)} must be a single path segment ([A-Za-z0-9._-], not '.' or '..')`);
|
|
314
|
+
}
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
296
317
|
/** Compute and set SHA256 + sizeBytes on a StateArtifact from its file path
|
|
297
318
|
* (v0.1.73). Fails silently when the file doesn't exist — does not throw. */
|
|
298
319
|
function hashArtifactFile(artifact) {
|
|
@@ -53,7 +53,11 @@ function canonicalTelemetryPayload(usage, ctx) {
|
|
|
53
53
|
usage: usage ?? null,
|
|
54
54
|
runId: ctx.runId,
|
|
55
55
|
taskId: ctx.taskId,
|
|
56
|
-
promptDigest: ctx.promptDigest
|
|
56
|
+
promptDigest: ctx.promptDigest,
|
|
57
|
+
// Present ONLY when the signer covered the result. Omitting the key keeps the
|
|
58
|
+
// canonical bytes byte-identical to the original 4-field payload, so every
|
|
59
|
+
// pre-result-coverage signature still verifies (POLA / back-compat).
|
|
60
|
+
...(ctx.resultDigest !== undefined ? { resultDigest: ctx.resultDigest } : {})
|
|
57
61
|
});
|
|
58
62
|
}
|
|
59
63
|
function messageOf(error) {
|
|
@@ -110,16 +114,26 @@ function verifyTelemetryAttestation(usage, signatureB64, trustPublicKeyPem, ctx)
|
|
|
110
114
|
catch {
|
|
111
115
|
return { status: "unattested", reason: "signature is not valid base64" };
|
|
112
116
|
}
|
|
113
|
-
const
|
|
117
|
+
const matches = (c) => node_crypto_1.default.verify(null, Buffer.from(canonicalTelemetryPayload(usage, c), "utf8"), publicKey, signature);
|
|
114
118
|
let ok = false;
|
|
119
|
+
let coversResult = false;
|
|
115
120
|
try {
|
|
116
|
-
ok =
|
|
121
|
+
ok = matches(ctx);
|
|
122
|
+
// A match on the first arm (which carries resultDigest when CW has one) means
|
|
123
|
+
// the signature covered the result — the findings — not just the usage.
|
|
124
|
+
coversResult = ok && ctx.resultDigest !== undefined;
|
|
125
|
+
// Back-compat: a signer that predates result coverage signed only the 4-field
|
|
126
|
+
// payload, so on a miss retry WITHOUT resultDigest. A NEW signer who covered
|
|
127
|
+
// the result fails BOTH arms when the result is edited (its resultDigest no
|
|
128
|
+
// longer matches), so result tampering is still caught.
|
|
129
|
+
if (!ok && ctx.resultDigest !== undefined)
|
|
130
|
+
ok = matches({ ...ctx, resultDigest: undefined });
|
|
117
131
|
}
|
|
118
132
|
catch (error) {
|
|
119
133
|
return { status: "unattested", reason: `verification error: ${messageOf(error)}` };
|
|
120
134
|
}
|
|
121
135
|
return ok
|
|
122
|
-
? { status: "attested", algorithm: "ed25519" }
|
|
136
|
+
? { status: "attested", algorithm: "ed25519", ...(coversResult ? { coversResult: true } : {}) }
|
|
123
137
|
: { status: "unattested", reason: "signature does not match reported usage (tampered, replayed, or wrong key)" };
|
|
124
138
|
}
|
|
125
139
|
/** Resolve a trust key from a config value that is EITHER an inline PEM or a path
|
|
@@ -160,6 +174,7 @@ function stableDigest(value) {
|
|
|
160
174
|
}
|
|
161
175
|
function verifyTelemetrySignatures(records, trustPublicKeyPem) {
|
|
162
176
|
const checks = [];
|
|
177
|
+
const resultBound = [];
|
|
163
178
|
let checked = 0;
|
|
164
179
|
let reverified = 0;
|
|
165
180
|
let failed = 0;
|
|
@@ -190,11 +205,20 @@ function verifyTelemetrySignatures(records, trustPublicKeyPem) {
|
|
|
190
205
|
const result = verifyTelemetryAttestation(record.reportedUsage, record.usageSignature, trustPublicKeyPem, {
|
|
191
206
|
runId: record.runId,
|
|
192
207
|
taskId: record.taskId,
|
|
193
|
-
promptDigest: record.promptDigest
|
|
208
|
+
promptDigest: record.promptDigest,
|
|
209
|
+
// Result-bound records carry the signed digest; verifyTelemetryAttestation
|
|
210
|
+
// reconstructs the 5-field payload (and falls back to 4-field for old records).
|
|
211
|
+
resultDigest: record.resultDigest
|
|
194
212
|
});
|
|
195
213
|
if (result.status === "attested") {
|
|
196
214
|
reverified += 1;
|
|
197
215
|
checks.push({ name: `signature[${i}]`, pass: true });
|
|
216
|
+
// Only a signature that actually COVERED the result digest anchors it — a
|
|
217
|
+
// 4-field fallback (coversResult false) must not let an injected resultDigest
|
|
218
|
+
// be trusted downstream.
|
|
219
|
+
if (result.coversResult && record.resultDigest) {
|
|
220
|
+
resultBound.push({ taskId: record.taskId, resultDigest: record.resultDigest });
|
|
221
|
+
}
|
|
198
222
|
}
|
|
199
223
|
else {
|
|
200
224
|
failed += 1;
|
|
@@ -207,5 +231,5 @@ function verifyTelemetrySignatures(records, trustPublicKeyPem) {
|
|
|
207
231
|
});
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
|
-
return { keyProvided: Boolean(trustPublicKeyPem), checked, reverified, failed, checks };
|
|
234
|
+
return { keyProvided: Boolean(trustPublicKeyPem), checked, reverified, failed, resultBound, checks };
|
|
211
235
|
}
|
package/dist/telemetry-demo.js
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
// Fully hermetic + deterministic: generates an EPHEMERAL ed25519 keypair, builds
|
|
7
7
|
// a REAL telemetry ledger through the production append API (appendTelemetryAttestation
|
|
8
8
|
// + signTelemetry — byte-identical to what a live attested run writes), then
|
|
9
|
-
// demonstrates
|
|
9
|
+
// demonstrates all THREE tamper-evidence layers catching a forgery:
|
|
10
10
|
// A) LEDGER layer — flip a recorded verdict on disk (unattested -> attested, the
|
|
11
11
|
// canonical "forge a green record" attack) -> verifyTelemetryLedger recomputes
|
|
12
12
|
// every hash independently, so the edited record's hash mismatches AND every
|
|
13
13
|
// record after it breaks the chain (cascade).
|
|
14
14
|
// B) SIGNATURE layer — inflate the reported tokens but keep the original ed25519
|
|
15
15
|
// signature -> verifyTelemetryAttestation rejects it ("signature does not match").
|
|
16
|
+
// C) RESULT layer — edit the agent's SIGNED FINDING after signing. The executor
|
|
17
|
+
// binds sha256(result.md) into the ed25519 payload; CW re-derives the digest
|
|
18
|
+
// from the result file at verify time, so an edited finding no longer joins the
|
|
19
|
+
// signature. This is the threat a usage-only signature never covered.
|
|
16
20
|
//
|
|
17
21
|
// No model, no network, no API key, no second repo — runs in a private tmpdir.
|
|
18
22
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
@@ -161,6 +165,30 @@ function runTamperDemo(options = {}) {
|
|
|
161
165
|
after: { verified: sigCheck.status === "attested", detail: sigCheck.reason || sigCheck.status },
|
|
162
166
|
failures: sigCheck.status === "attested" ? [] : [`signature: ${sigCheck.reason}`]
|
|
163
167
|
});
|
|
168
|
+
// 3c. RESULT layer — the REAL threat the result-bound signature closes: the agent's
|
|
169
|
+
// signed FINDING is edited after signing. The executor binds sha256(result.md)
|
|
170
|
+
// into the ed25519 payload (a 5-field, result-COVERING signature); CW re-derives
|
|
171
|
+
// the digest from the result file at verify time. An edited finding hashes to a
|
|
172
|
+
// different digest, so the signature joins neither the 5-field nor the 4-field
|
|
173
|
+
// payload and the verify rejects it. A usage-only signature never covered this.
|
|
174
|
+
const findingSigned = "Finding: auth bypass in login() — severity HIGH";
|
|
175
|
+
const findingEdited = findingSigned.replace("HIGH", "LOW");
|
|
176
|
+
const resultCtx = { runId, taskId: target.hop.taskId, promptDigest: target.hop.promptDigest };
|
|
177
|
+
const resultSignature = (0, telemetry_attestation_1.signTelemetry)(target.hop.usage, privateKeyPem, { ...resultCtx, resultDigest: (0, execution_backend_1.sha256)(findingSigned) });
|
|
178
|
+
// Baseline: the signed finding verifies, and the signature actually COVERED the result.
|
|
179
|
+
const resultClean = (0, telemetry_attestation_1.verifyTelemetryAttestation)(target.hop.usage, resultSignature, publicKeyPem, { ...resultCtx, resultDigest: (0, execution_backend_1.sha256)(findingSigned) });
|
|
180
|
+
// Tamper: CW re-derives the digest from the EDITED finding -> no longer the signed digest.
|
|
181
|
+
const resultCheck = (0, telemetry_attestation_1.verifyTelemetryAttestation)(target.hop.usage, resultSignature, publicKeyPem, { ...resultCtx, resultDigest: (0, execution_backend_1.sha256)(findingEdited) });
|
|
182
|
+
layers.push({
|
|
183
|
+
layer: "result",
|
|
184
|
+
tamper: `edited the agent's signed finding "severity HIGH" -> "severity LOW" after it was signed`,
|
|
185
|
+
before: {
|
|
186
|
+
verified: resultClean.status === "attested" && resultClean.coversResult === true,
|
|
187
|
+
detail: `the signed finding verifies — ed25519 binds usage + sha256(result), result-covering`
|
|
188
|
+
},
|
|
189
|
+
after: { verified: resultCheck.status === "attested", detail: resultCheck.reason || resultCheck.status },
|
|
190
|
+
failures: resultCheck.status === "attested" ? [] : [`result: ${resultCheck.reason}`]
|
|
191
|
+
});
|
|
164
192
|
if (!options.keepDir && !options.dir)
|
|
165
193
|
node_fs_1.default.rmSync(runDir, { recursive: true, force: true });
|
|
166
194
|
const proven = baseline.ledgerVerified &&
|
package/dist/telemetry-ledger.js
CHANGED
|
@@ -94,6 +94,9 @@ function recordHashInput(record) {
|
|
|
94
94
|
reportedUsageDigest: record.reportedUsageDigest,
|
|
95
95
|
...(record.reportedUsage !== undefined ? { reportedUsage: record.reportedUsage } : {}),
|
|
96
96
|
usageSignature: record.usageSignature || null,
|
|
97
|
+
// Chain-bind resultDigest only when present, so a usage-only record's hash is
|
|
98
|
+
// byte-identical to a pre-result-coverage one (back-compat with old ledgers).
|
|
99
|
+
...(record.resultDigest !== undefined ? { resultDigest: record.resultDigest } : {}),
|
|
97
100
|
attestation: record.attestation,
|
|
98
101
|
attestationReason: record.attestationReason || null,
|
|
99
102
|
prevHash: record.prevHash
|
|
@@ -131,6 +134,9 @@ function appendTelemetryAttestation(run, input) {
|
|
|
131
134
|
// signature can be independently re-verified offline at `telemetry verify`.
|
|
132
135
|
...(input.reportedUsage ? { reportedUsage: input.reportedUsage } : {}),
|
|
133
136
|
usageSignature: input.usageSignature,
|
|
137
|
+
// Present only for a result-bound signature, so usage-only records are
|
|
138
|
+
// byte-identical (and their recordHash unchanged) — back-compat.
|
|
139
|
+
...(input.resultDigest ? { resultDigest: input.resultDigest } : {}),
|
|
134
140
|
attestation: input.attestation,
|
|
135
141
|
attestationReason: input.attestationReason,
|
|
136
142
|
prevHash
|
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.88";
|
|
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;
|
|
@@ -24,7 +24,12 @@ function attestWorkerDelegation(accept, deps) {
|
|
|
24
24
|
// usage. Absent/invalid signature => `unattested`/`absent`, surfaced loudly,
|
|
25
25
|
// NEVER silently recorded as trusted.
|
|
26
26
|
const telemetry = options.agentDelegation
|
|
27
|
-
? (0, telemetry_attestation_1.verifyTelemetryAttestation)(options.agentDelegation.reportedUsage, options.agentDelegation.usageSignature, (0, telemetry_attestation_1.resolveTrustPublicKey)(options.agentDelegation.usageTrustPublicKey),
|
|
27
|
+
? (0, telemetry_attestation_1.verifyTelemetryAttestation)(options.agentDelegation.reportedUsage, options.agentDelegation.usageSignature, (0, telemetry_attestation_1.resolveTrustPublicKey)(options.agentDelegation.usageTrustPublicKey),
|
|
28
|
+
// resultDigest binds the agent's findings into the signature: CW recomputes
|
|
29
|
+
// the digest from the accepted result (the SAME raw bytes the executor
|
|
30
|
+
// signed) so a result edited after signing fails verification. A signer
|
|
31
|
+
// that did not cover the result still verifies (verifier back-compat).
|
|
32
|
+
{ runId: run.id, taskId: task.id, promptDigest: options.agentDelegation.promptDigest, resultDigest: (0, execution_backend_1.sha256)(rawResult) })
|
|
28
33
|
: undefined;
|
|
29
34
|
// Track 1 fail-closed (Decision 2 — OPT-IN, off by default). When the operator
|
|
30
35
|
// requires attested telemetry, a delegated hop whose verdict is not `attested`
|
|
@@ -59,7 +64,7 @@ function attestWorkerDelegation(accept, deps) {
|
|
|
59
64
|
* event can cross-link the record hash), then emits the worker.agent-delegation
|
|
60
65
|
* audit event. No-op for non-agent hops. */
|
|
61
66
|
function recordWorkerDelegationLedger(accept, delegation) {
|
|
62
|
-
const { agentDelegation } = delegation;
|
|
67
|
+
const { agentDelegation, telemetry } = delegation;
|
|
63
68
|
// The agent-hop attestation event — hung off worker.output, alongside
|
|
64
69
|
// worker.backend. Recorded in trust-audit/provenance, NEVER in node evidence.
|
|
65
70
|
if (!agentDelegation)
|
|
@@ -76,6 +81,11 @@ function recordWorkerDelegationLedger(accept, delegation) {
|
|
|
76
81
|
promptDigest: agentDelegation.promptDigest,
|
|
77
82
|
reportedUsage: agentDelegation.reportedUsage,
|
|
78
83
|
usageSignature: agentDelegation.usageSignature,
|
|
84
|
+
// Store the signed result digest ONLY when the signature actually covered
|
|
85
|
+
// it, so the offline re-verifier (telemetry verify --pubkey / report verify)
|
|
86
|
+
// can reconstruct the 5-field payload. A usage-only signature stores none
|
|
87
|
+
// (its record stays byte-identical to a pre-result-coverage one).
|
|
88
|
+
resultDigest: telemetry?.coversResult ? agentDelegation.resultDigest : undefined,
|
|
79
89
|
attestation: agentDelegation.usageAttestation,
|
|
80
90
|
attestationReason: agentDelegation.usageAttestationReason
|
|
81
91
|
})
|
package/dist/workflow-api.js
CHANGED
|
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.workflow = workflow;
|
|
4
4
|
exports.phase = phase;
|
|
5
5
|
exports.parallel = parallel;
|
|
6
|
+
exports.loop = loop;
|
|
6
7
|
exports.createWorkflowApi = createWorkflowApi;
|
|
7
8
|
exports.agent = agent;
|
|
9
|
+
exports.subWorkflow = subWorkflow;
|
|
8
10
|
exports.artifact = artifact;
|
|
9
11
|
exports.input = input;
|
|
10
12
|
exports.slugify = slugify;
|
|
@@ -46,19 +48,50 @@ function phase(name, tasks, options = {}) {
|
|
|
46
48
|
function parallel(name, tasks, options = {}) {
|
|
47
49
|
return phase(name, tasks, { mode: "parallel", ...options });
|
|
48
50
|
}
|
|
51
|
+
/** A BOUNDED DYNAMIC LOOP phase: `tasks` are a per-round template. After each round
|
|
52
|
+
* completes, the registered `until` predicate decides whether to run another round
|
|
53
|
+
* (a fresh appended phase with the same tasks, round-suffixed ids) or stop; capped
|
|
54
|
+
* at `maxRounds`. Sugar over phase() that sets `loop`; plain phases are unaffected. */
|
|
55
|
+
function loop(name, tasks, spec, options = {}) {
|
|
56
|
+
if (!spec || typeof spec.maxRounds !== "number" || spec.maxRounds < 1) {
|
|
57
|
+
throw new Error(`loop ${name} requires a positive integer maxRounds`);
|
|
58
|
+
}
|
|
59
|
+
const until = spec.until;
|
|
60
|
+
const valid = until
|
|
61
|
+
&& ((until.kind === "predicate" && Boolean(until.ref))
|
|
62
|
+
|| (until.kind === "budget-target" && typeof until.target === "number" && until.target > 0));
|
|
63
|
+
if (!valid) {
|
|
64
|
+
throw new Error(`loop ${name} requires until: { kind: "predicate", ref } or { kind: "budget-target", target }`);
|
|
65
|
+
}
|
|
66
|
+
return phase(name, tasks, { loop: { maxRounds: Math.floor(spec.maxRounds), until }, ...options });
|
|
67
|
+
}
|
|
49
68
|
function createWorkflowApi() {
|
|
50
69
|
return {
|
|
51
70
|
workflow,
|
|
52
71
|
phase,
|
|
53
72
|
parallel,
|
|
73
|
+
loop,
|
|
54
74
|
agent,
|
|
55
75
|
artifact,
|
|
76
|
+
subWorkflow,
|
|
56
77
|
input
|
|
57
78
|
};
|
|
58
79
|
}
|
|
59
80
|
function agent(id, prompt, options = {}) {
|
|
60
81
|
return task("agent", id, prompt, options);
|
|
61
82
|
}
|
|
83
|
+
/** A task fulfilled by an inline SUB-WORKFLOW: instead of spawning an agent, the
|
|
84
|
+
* drive plans + drives the child `appId` and binds its report back as this task's
|
|
85
|
+
* result. The prompt is recorded for provenance but is not sent to an agent. */
|
|
86
|
+
function subWorkflow(id, appId, options = {}) {
|
|
87
|
+
if (!appId)
|
|
88
|
+
throw new Error(`subWorkflow task ${id} requires an appId`);
|
|
89
|
+
const { inputs, bindResult, prompt, ...rest } = options;
|
|
90
|
+
return task("agent", id, prompt || `Delegate to sub-workflow app: ${appId}`, {
|
|
91
|
+
...rest,
|
|
92
|
+
subWorkflow: { appId, ...(inputs ? { inputs } : {}), ...(bindResult ? { bindResult } : {}) }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
62
95
|
function artifact(id, prompt, options = {}) {
|
|
63
96
|
return task("artifact", id, prompt, options);
|
|
64
97
|
}
|