cool-workflow 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini-plugin/mcp.json +10 -0
- package/.gemini-plugin/plugin.json +40 -0
- package/.opencode-plugin/mcp.json +10 -0
- package/.opencode-plugin/plugin.json +40 -0
- package/README.md +202 -48
- package/apps/architecture-review/app.json +1 -1
- package/apps/architecture-review-fast/app.json +1 -1
- package/apps/end-to-end-golden-path/app.json +1 -1
- package/apps/pr-review-fix-ci/app.json +1 -1
- package/apps/release-cut/app.json +1 -1
- package/apps/research-synthesis/app.json +1 -1
- package/dist/cli/dispatch.js +33 -4
- package/dist/cli/entry.js +11 -19
- package/dist/cli/global-flags.js +113 -0
- package/dist/cli/io.js +6 -20
- package/dist/cli/parseargv.js +7 -1
- package/dist/core/capability-data.js +339 -10
- package/dist/core/format/help.js +80 -4
- package/dist/core/format/recovery-hint.js +32 -0
- package/dist/core/multi-agent/collaboration.js +35 -6
- package/dist/core/multi-agent/runtime.js +7 -0
- package/dist/core/multi-agent/trust-policy.js +7 -1
- package/dist/core/pipeline/contract.js +7 -0
- package/dist/core/pipeline/error-feedback.js +2 -2
- package/dist/core/trust/evidence-grounding.js +13 -1
- package/dist/core/util/cli-args.js +55 -0
- package/dist/core/version.js +1 -1
- package/dist/mcp/dispatch.js +22 -2
- package/dist/mcp/server.js +124 -13
- package/dist/mcp-server.js +20 -0
- package/dist/shell/commit.js +8 -2
- package/dist/shell/coordinator-io.js +73 -1
- package/dist/shell/drive.js +129 -64
- package/dist/shell/error-feedback-io.js +6 -0
- package/dist/shell/execution-backend/agent.js +205 -24
- package/dist/shell/execution-backend/container.js +44 -10
- package/dist/shell/execution-backend/local.js +47 -10
- package/dist/shell/fs-atomic.js +93 -12
- package/dist/shell/ledger-cli.js +9 -2
- package/dist/shell/multi-agent-cli.js +5 -1
- package/dist/shell/onramp.js +48 -5
- package/dist/shell/pipeline-cli.js +20 -2
- package/dist/shell/pipeline.js +2 -1
- package/dist/shell/reclamation-io.js +76 -7
- package/dist/shell/registry-cli.js +4 -0
- package/dist/shell/run-export.js +52 -4
- package/dist/shell/run-registry-io.js +1 -0
- package/dist/shell/run-store.js +156 -0
- package/dist/shell/scheduler-io.js +101 -10
- package/dist/shell/telemetry-ledger-io.js +36 -24
- package/dist/shell/trust-audit.js +104 -10
- package/dist/shell/workbench-host.js +121 -10
- package/dist/shell/workbench.js +79 -5
- package/dist/shell/worker-isolation.js +1 -1
- package/dist/shell/workflow-app-loader.js +67 -1
- package/dist/wiring/capability-table/basics.js +7 -2
- package/dist/wiring/capability-table/exec-backend.js +42 -24
- package/dist/wiring/capability-table/multi-agent.js +69 -69
- package/dist/wiring/capability-table/pipeline.js +60 -28
- package/dist/wiring/capability-table/registry-core.js +29 -6
- package/dist/wiring/capability-table/reporting.js +45 -39
- package/dist/wiring/capability-table/scheduling-registry.js +66 -60
- package/dist/wiring/capability-table/state.js +34 -34
- package/dist/wiring/capability-table/trust-ledger.js +66 -24
- package/dist/wiring/capability-table/workflow-apps.js +15 -15
- package/docs/agent-delegation-drive.7.md +17 -0
- package/docs/cli-mcp-parity.7.md +43 -13
- package/docs/contract-migration-tooling.7.md +4 -0
- package/docs/control-plane-scheduling.7.md +38 -0
- package/docs/cross-agent-ledger.7.md +20 -8
- package/docs/durable-state-and-locking.7.md +4 -0
- package/docs/evidence-adoption-reasoning-chain.7.md +4 -0
- package/docs/execution-backends.7.md +4 -0
- package/docs/fix.7.md +4 -4
- package/docs/getting-started.md +40 -32
- package/docs/index.md +17 -0
- package/docs/launch/demo.tape +4 -3
- package/docs/mcp-app-surface.7.md +6 -0
- package/docs/multi-agent-cli-mcp-surface.7.md +4 -0
- package/docs/multi-agent-eval-replay-harness.7.md +4 -0
- package/docs/multi-agent-operator-ux.7.md +4 -0
- package/docs/node-snapshot-diff-replay.7.md +4 -0
- package/docs/observability-cost-accounting.7.md +4 -0
- package/docs/project-index.md +43 -9
- package/docs/real-execution-backends.7.md +4 -0
- package/docs/release-and-migration.7.md +4 -0
- package/docs/release-tooling.7.md +22 -0
- package/docs/routine.7.md +22 -0
- package/docs/run-registry-control-plane.7.md +4 -0
- package/docs/run-retention-reclamation.7.md +28 -3
- package/docs/state-explosion-management.7.md +4 -0
- package/docs/team-collaboration.7.md +4 -0
- package/docs/trust-audit-anchor.7.md +4 -0
- package/docs/web-desktop-workbench.7.md +26 -2
- package/docs/workflow-app-framework.7.md +31 -0
- package/manifest/plugin.manifest.json +1 -1
- package/manifest/source-context-profiles.json +4 -3
- package/package.json +5 -2
- package/scripts/agents/agent-adapter-core.js +26 -0
- package/scripts/agents/claude-p-agent.js +4 -1
- package/scripts/agents/codex-agent.js +4 -0
- package/scripts/agents/cw-attest-wrap.js +1 -1
- package/scripts/agents/gemini-agent.js +4 -0
- package/scripts/agents/opencode-agent.js +5 -0
- package/scripts/block-unapproved-tag.js +160 -0
- package/scripts/bump-version.js +27 -10
- package/scripts/canonical-apps.js +4 -4
- package/scripts/children/batch-delegate-child.js +72 -4
- package/scripts/children/http-batch-delegate-child.js +132 -0
- package/scripts/children/http-delegate-child.js +8 -0
- package/scripts/dogfood-release.js +2 -2
- package/scripts/fake-date-for-reproduction.js +1 -1
- package/scripts/golden-path.js +4 -4
- package/scripts/lang-policy-check.js +82 -0
- package/scripts/purity-baseline.json +0 -30
- package/scripts/purity-gate.js +16 -3
- package/scripts/release-check.js +2 -1
- package/scripts/release-flow.js +264 -41
- package/scripts/release-gate.js +184 -0
- package/scripts/release-oneclick.js +438 -0
- package/scripts/release-tags.js +47 -0
- package/scripts/verdict-keygen.js +2 -2
- package/scripts/verify-bump-reproduction.js +193 -0
- package/scripts/verify-verdict-signature.js +1 -1
- package/scripts/version-sync-check.js +39 -22
- package/ui/workbench/app.css +41 -9
- package/ui/workbench/app.js +160 -27
- package/ui/workbench/index.html +2 -2
- package/docs/agent-framework.md +0 -177
- package/docs/designs/handoff-ledger.md +0 -145
- package/docs/dogfood/resume-drive-real-agent-2026-06-14.md +0 -40
- package/docs/launch/launch-kit.md +0 -195
- package/docs/launch/pre-launch-checklist.md +0 -53
- package/docs/readme-v0.1.87-full.md +0 -301
- package/docs/routines.md +0 -101
- package/docs/scheduled-tasks.md +0 -80
- package/scripts/agents/claude-p-agent.sh +0 -9
- package/scripts/block-unapproved-tag.sh +0 -60
- package/scripts/release-gate.sh +0 -94
- package/scripts/verify-bump-reproduction.sh +0 -148
- package/scripts/verify-container-selfref.js +0 -64
package/dist/shell/drive.js
CHANGED
|
@@ -630,7 +630,7 @@ function maybeExpandLoop(run) {
|
|
|
630
630
|
requiresEvidence: t.requiresEvidence,
|
|
631
631
|
sandboxProfileId: t.sandboxProfileId,
|
|
632
632
|
},
|
|
633
|
-
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node) });
|
|
633
|
+
}, { persist: false, persistNode: (r, node) => void (0, node_store_1.appendRunNode)(r, node), pathExists: fs.existsSync });
|
|
634
634
|
t.stateNodeId = result.outputNodeId;
|
|
635
635
|
}
|
|
636
636
|
(0, dispatch_1.updatePhaseStatuses)(run);
|
|
@@ -656,17 +656,32 @@ function driveStep(ctx) {
|
|
|
656
656
|
return processSelectedTask(ctx, selected.id);
|
|
657
657
|
}
|
|
658
658
|
/** Dispatch every batch task (sequential — dispatch mutates state), then
|
|
659
|
-
* collect ALL
|
|
660
|
-
* (
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
659
|
+
* collect ALL agent child outcomes in one concurrent window: spawn-style
|
|
660
|
+
* (CLI-binary) agents through the batch delegate child, and endpoint-mode
|
|
661
|
+
* agents through the HTTP batch delegate child — so `--concurrency N` runs N
|
|
662
|
+
* endpoint delegations in parallel instead of one-at-a-time on the serial
|
|
663
|
+
* path. Returns outcomes keyed by task id; a cache-hit, endpoint-configured
|
|
664
|
+
* agent, or sub-workflow task gets no prepared outcome and settles through
|
|
665
|
+
* the serial accept path inside processSelectedTask (a sub-workflow task is
|
|
666
|
+
* never spawn-eligible in the first place — processSelectedTask always
|
|
667
|
+
* takes its own runSubWorkflow branch for it, so building a spawn job here
|
|
668
|
+
* would only be spawned and thrown away unused). Dispatch failures become
|
|
669
|
+
* recorded fail steps up front, exactly what the serial path would emit. */
|
|
666
670
|
function prepareConcurrentOutcomes(ctx, batch) {
|
|
667
671
|
const failSteps = new Map();
|
|
668
672
|
const jobs = [];
|
|
669
673
|
const jobTaskIds = [];
|
|
674
|
+
const endpointJobs = [];
|
|
675
|
+
const endpointTaskIds = [];
|
|
676
|
+
// Set whenever a task's status flips pending -> running (workerId
|
|
677
|
+
// assigned) this round, regardless of whether it later becomes a real
|
|
678
|
+
// spawn job, an endpoint job, a cache hit, or a sub-workflow task. That
|
|
679
|
+
// dispatch is a real state mutation on the round-cached run object that
|
|
680
|
+
// nothing has written to disk yet — the pre-spawn checkpoint below must
|
|
681
|
+
// flush it even when the batch produces zero spawn/endpoint jobs (e.g. an
|
|
682
|
+
// all-sub-workflow batch), or a crash before the round-end flush loses
|
|
683
|
+
// the dispatch.
|
|
684
|
+
let dispatchedThisRound = false;
|
|
670
685
|
for (const taskId of batch) {
|
|
671
686
|
const run = loadRun(ctx);
|
|
672
687
|
const task = run.tasks.find((candidate) => candidate.id === taskId);
|
|
@@ -681,6 +696,7 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
681
696
|
continue;
|
|
682
697
|
}
|
|
683
698
|
workerId = dispatchedTask.workerId;
|
|
699
|
+
dispatchedThisRound = true;
|
|
684
700
|
}
|
|
685
701
|
if (!workerId) {
|
|
686
702
|
failSteps.set(taskId, (0, drive_decide_1.makeStep)("dispatch", "failed", { runId: ctx.runId, taskId, phase: task.phase, reason: "no worker scope for task" }));
|
|
@@ -694,7 +710,15 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
694
710
|
const cachePath = resultCachePath(freshRun, task, (0, hash_1.sha256)(task.prompt || ""), ctx.incremental, delegationDigest);
|
|
695
711
|
if (cachePath && fs.existsSync(cachePath))
|
|
696
712
|
continue;
|
|
697
|
-
|
|
713
|
+
// A sub-workflow task never goes through the spawn path — processSelectedTask
|
|
714
|
+
// checks task.subWorkflow before it ever looks at a prepared spawn outcome
|
|
715
|
+
// and always takes its own runSubWorkflow branch instead (mirrors that
|
|
716
|
+
// check byte-for-byte). Building a spawn OR endpoint job for it here would
|
|
717
|
+
// just be a real agent child spawned/delegated and thrown away unused.
|
|
718
|
+
const subWorkflow = task.subWorkflow;
|
|
719
|
+
if (subWorkflow)
|
|
720
|
+
continue;
|
|
721
|
+
const request = {
|
|
698
722
|
schemaVersion: 1,
|
|
699
723
|
runId: ctx.runId,
|
|
700
724
|
taskId: task.id,
|
|
@@ -705,7 +729,8 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
705
729
|
label: task.id,
|
|
706
730
|
timeoutMs: ctx.config.timeoutMs,
|
|
707
731
|
delegation: { command: ctx.config.command, args: ctx.config.args, endpoint: ctx.config.endpoint, model: task.model || ctx.config.model },
|
|
708
|
-
}
|
|
732
|
+
};
|
|
733
|
+
const job = (0, agent_1.prepareAgentSpawn)(request);
|
|
709
734
|
if (job) {
|
|
710
735
|
const sandboxPolicy = manifest.sandboxPolicy;
|
|
711
736
|
if (sandboxPolicy) {
|
|
@@ -713,24 +738,53 @@ function prepareConcurrentOutcomes(ctx, batch) {
|
|
|
713
738
|
}
|
|
714
739
|
jobs.push(job);
|
|
715
740
|
jobTaskIds.push(taskId);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
// Endpoint-configured agent: no CLI binary to batch-spawn, but its HTTP
|
|
744
|
+
// delegation joins the concurrent window through the endpoint batch child
|
|
745
|
+
// (runEndpointBatchOutcomes) rather than settling one-at-a-time on the
|
|
746
|
+
// serial path. An unconfigured agent (neither command nor endpoint) returns
|
|
747
|
+
// undefined from both prepares here and falls to the serial refusal.
|
|
748
|
+
const endpointJob = (0, agent_1.prepareEndpointJob)(request);
|
|
749
|
+
if (endpointJob) {
|
|
750
|
+
endpointJobs.push(endpointJob);
|
|
751
|
+
endpointTaskIds.push(taskId);
|
|
716
752
|
}
|
|
717
753
|
}
|
|
718
|
-
|
|
719
|
-
|
|
754
|
+
const totalJobs = jobs.length + endpointJobs.length;
|
|
755
|
+
if (totalJobs) {
|
|
756
|
+
emitProgress(`⇉ concurrent round: ${totalJobs} agent${totalJobs > 1 ? "s" : ""} spawning in parallel, may take minutes…`);
|
|
757
|
+
}
|
|
758
|
+
if (totalJobs || dispatchedThisRound) {
|
|
759
|
+
// Every task above that reached "pending" got dispatched (workerId
|
|
760
|
+
// assigned) in the round-cached run object, but nothing durable was
|
|
761
|
+
// written for it — unlike the serial path's own dispatch branch, which
|
|
762
|
+
// always checkpoints immediately. Flush now, BEFORE the batch's long
|
|
763
|
+
// settle window opens: a crash mid-spawn OR mid-sub-workflow-recursion
|
|
764
|
+
// (both can run for minutes) then leaves state.json correctly showing
|
|
765
|
+
// these tasks as dispatched, instead of losing the dispatch entirely.
|
|
766
|
+
// Gated on dispatchedThisRound (not just totalJobs) so a batch made
|
|
767
|
+
// entirely of sub-workflow and/or cache-hit tasks — which builds zero
|
|
768
|
+
// spawn/endpoint jobs — still gets this flush before its long-running
|
|
769
|
+
// settle loop starts.
|
|
770
|
+
(0, run_store_1.saveCheckpoint)(loadRun(ctx));
|
|
720
771
|
}
|
|
721
|
-
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
722
772
|
const outcomes = new Map();
|
|
773
|
+
const settled = (0, agent_1.runAgentBatchOutcomes)(jobs);
|
|
723
774
|
jobTaskIds.forEach((taskId, index) => outcomes.set(taskId, settled[index]));
|
|
775
|
+
const endpointSettled = (0, agent_1.runEndpointBatchOutcomes)(endpointJobs);
|
|
776
|
+
endpointTaskIds.forEach((taskId, index) => outcomes.set(taskId, endpointSettled[index]));
|
|
724
777
|
return { outcomes, failSteps };
|
|
725
778
|
}
|
|
726
779
|
/** One concurrent round inside one cached in-memory run: dispatches every
|
|
727
780
|
* batch task, spawns all spawn-style agent children in one concurrent
|
|
728
|
-
* window
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
781
|
+
* window (CLI-binary agents through the batch child, endpoint agents
|
|
782
|
+
* through the HTTP batch child), then settles + accepts in DETERMINISTIC
|
|
783
|
+
* batch (task-id) order regardless of wall-clock finish order. At round end
|
|
784
|
+
* it flushes once: commitState(run, "concurrent-round:<n>-tasks") +
|
|
785
|
+
* writeReport + saveCheckpoint. Cache-hit tasks get no prepared outcome and
|
|
786
|
+
* settle through the serial path (still inside this one deferred-persist
|
|
787
|
+
* round). If no step was produced (nothing
|
|
734
788
|
* runnable at round entry — terminal/blocked/token-budget gate) the
|
|
735
789
|
* round degrades to one plain driveStep. Byte-exact to the old build's
|
|
736
790
|
* src/drive.ts's driveConcurrentRound. */
|
|
@@ -896,14 +950,14 @@ function driveOneRound(ctx, options, steps, emitPhaseProgress) {
|
|
|
896
950
|
* finishes its own bookkeeping first. A SECOND signal means the caller
|
|
897
951
|
* wants out right now, not a graceful stop, and force-exits immediately
|
|
898
952
|
* with the conventional 128+signum code. */
|
|
899
|
-
function createStopSignalController() {
|
|
953
|
+
function createStopSignalController(runId) {
|
|
900
954
|
let interruptedBy;
|
|
901
955
|
let stopSignalHits = 0;
|
|
902
956
|
const onStopSignal = (signal) => {
|
|
903
957
|
stopSignalHits += 1;
|
|
904
958
|
if (stopSignalHits === 1) {
|
|
905
959
|
interruptedBy = signal;
|
|
906
|
-
emitProgress(`received ${signal} — stopping after the current step (
|
|
960
|
+
emitProgress(`received ${signal} — stopping after the current step (resume: cw run resume ${runId} --drive)`);
|
|
907
961
|
return;
|
|
908
962
|
}
|
|
909
963
|
// A second signal means the caller wants out right now, not a graceful stop.
|
|
@@ -952,11 +1006,11 @@ function finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhau
|
|
|
952
1006
|
if (interruptedBy) {
|
|
953
1007
|
exhaustedMaxIterations = false;
|
|
954
1008
|
if (!alreadyComplete) {
|
|
955
|
-
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive interrupted by ${interruptedBy} —
|
|
1009
|
+
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive interrupted by ${interruptedBy} — resume: cw run resume ${ctx.runId} --drive` }));
|
|
956
1010
|
}
|
|
957
1011
|
}
|
|
958
1012
|
else if (exhaustedMaxIterations) {
|
|
959
|
-
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state` }));
|
|
1013
|
+
steps.push((0, drive_decide_1.makeStep)("blocked", "blocked", { runId: ctx.runId, reason: `drive reached max iteration limit (${maxIter}) before a terminal state — resume: cw run resume ${ctx.runId} --drive` }));
|
|
960
1014
|
}
|
|
961
1015
|
const statusInputs = {
|
|
962
1016
|
once: Boolean(options.once),
|
|
@@ -996,28 +1050,35 @@ function finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhau
|
|
|
996
1050
|
* exact synchronous shape) and fixed only in driveAsync(). */
|
|
997
1051
|
function drive(runId, cwd, options = {}) {
|
|
998
1052
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1053
|
+
// Resolve the run dir before the mutex: a missing run throws "Run not found"
|
|
1054
|
+
// here (never a drive.lock side effect on a nonexistent run), via a stat only
|
|
1055
|
+
// so the per-round read accounting is unchanged. Mirrors withRunStateLock's
|
|
1056
|
+
// own probe-before-lock.
|
|
1057
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1058
|
+
return (0, run_store_1.withDriveLock)(runDir, runId, () => {
|
|
1059
|
+
const steps = [];
|
|
1060
|
+
const run0 = loadRun(ctx);
|
|
1061
|
+
const plannedWorkers = run0.tasks.length;
|
|
1062
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1063
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1064
|
+
let exhaustedMaxIterations = !options.once;
|
|
1065
|
+
const stopSignal = createStopSignalController(runId);
|
|
1066
|
+
stopSignal.install();
|
|
1067
|
+
try {
|
|
1068
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1069
|
+
if (stopSignal.getInterruptedBy())
|
|
1070
|
+
break;
|
|
1071
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1072
|
+
exhaustedMaxIterations = false;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1014
1075
|
}
|
|
1015
1076
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1077
|
+
finally {
|
|
1078
|
+
stopSignal.remove();
|
|
1079
|
+
}
|
|
1080
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1081
|
+
});
|
|
1021
1082
|
}
|
|
1022
1083
|
/** Same contract and same DriveResult as drive() -- but actually
|
|
1023
1084
|
* interruptible. After every round it awaits yieldToEventLoop(), a real
|
|
@@ -1045,29 +1106,33 @@ function drive(runId, cwd, options = {}) {
|
|
|
1045
1106
|
* future cycle if it proves needed in practice. */
|
|
1046
1107
|
async function driveAsync(runId, cwd, options = {}) {
|
|
1047
1108
|
const ctx = buildDriveContext(runId, cwd, options);
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1109
|
+
// Resolve the run dir before the mutex — see drive() above.
|
|
1110
|
+
const runDir = (0, run_store_1.resolveRunDir)(runId, cwd);
|
|
1111
|
+
return (0, run_store_1.withDriveLockAsync)(runDir, runId, async () => {
|
|
1112
|
+
const steps = [];
|
|
1113
|
+
const run0 = loadRun(ctx);
|
|
1114
|
+
const plannedWorkers = run0.tasks.length;
|
|
1115
|
+
const maxIter = (0, drive_decide_1.maxIterations)(plannedWorkers, (0, loop_expansion_1.maxLoopExpansion)(run0), ctx.policy);
|
|
1116
|
+
const emitPhaseProgress = createPhaseProgressEmitter();
|
|
1117
|
+
let exhaustedMaxIterations = !options.once;
|
|
1118
|
+
const stopSignal = createStopSignalController(runId);
|
|
1119
|
+
stopSignal.install();
|
|
1120
|
+
try {
|
|
1121
|
+
for (let i = 0; i < maxIter; i++) {
|
|
1122
|
+
if (stopSignal.getInterruptedBy())
|
|
1123
|
+
break;
|
|
1124
|
+
if (!driveOneRound(ctx, options, steps, emitPhaseProgress)) {
|
|
1125
|
+
exhaustedMaxIterations = false;
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
await yieldToEventLoop();
|
|
1063
1129
|
}
|
|
1064
|
-
await yieldToEventLoop();
|
|
1065
1130
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1131
|
+
finally {
|
|
1132
|
+
stopSignal.remove();
|
|
1133
|
+
}
|
|
1134
|
+
return finalizeDriveResult(ctx, options, steps, plannedWorkers, maxIter, exhaustedMaxIterations, stopSignal.getInterruptedBy());
|
|
1135
|
+
});
|
|
1071
1136
|
}
|
|
1072
1137
|
function drivePreview(runId, cwd, args = {}) {
|
|
1073
1138
|
const run = (0, run_store_1.loadRunFromCwd)(runId, cwd);
|
|
@@ -290,6 +290,12 @@ function resolveFeedback(run, feedbackId, result) {
|
|
|
290
290
|
function runPipelineStage(run, stageId, inputNodeId, options = {}, runnerOptions = {}) {
|
|
291
291
|
return (0, runner_1.runPipelineStage)(run, stageId, inputNodeId, options, {
|
|
292
292
|
...runnerOptions,
|
|
293
|
+
// The core gate's `pathExists` defaults to `() => true` (a pure core/
|
|
294
|
+
// module never reads the filesystem). This is the shell seam, so default
|
|
295
|
+
// it to the real `fs.existsSync` — that is what makes the contract's
|
|
296
|
+
// `requireReadablePaths` / `missing-artifact-path` gate live. A caller may
|
|
297
|
+
// still override it (e.g. a replay against a captured path set).
|
|
298
|
+
pathExists: runnerOptions.pathExists || fs.existsSync,
|
|
293
299
|
// Keep the caller-named failure node id (the old build honored outputNodeId
|
|
294
300
|
// for the preserved failure node); the raw core auto-mints when unset.
|
|
295
301
|
failureNodeId: runnerOptions.failureNodeId || options.outputNodeId,
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
// MILESTONE 5 (docs/rebuild/PLAN.md build order, step 5). Byte-exact port of
|
|
6
6
|
// plugins/cool-workflow/src/execution-backend/agent.ts. This module holds
|
|
7
7
|
// the PURE data-transform helpers (invocation resolution, arg substitution,
|
|
8
|
-
// secret redaction, report parsing) plus the batch delegate-child spawn
|
|
8
|
+
// secret redaction, report parsing) plus the batch delegate-child spawn — both
|
|
9
|
+
// the CLI-binary batch (runAgentBatchOutcomes) and, added later, its
|
|
10
|
+
// HTTP-endpoint sibling (runEndpointBatchOutcomes) so `--concurrency N` gives
|
|
11
|
+
// endpoint-mode agents real concurrency instead of a serial per-task spawn.
|
|
9
12
|
//
|
|
10
13
|
// THE RED LINE: CW spawns the agent and records its attested output. It
|
|
11
14
|
// NEVER imports a model SDK, holds an API key, or constructs a model API
|
|
@@ -49,6 +52,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
52
|
};
|
|
50
53
|
})();
|
|
51
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
55
|
+
exports.reapRecordedVendor = reapRecordedVendor;
|
|
52
56
|
exports.buildAgentChildEnv = buildAgentChildEnv;
|
|
53
57
|
exports.resolveAgentInvocation = resolveAgentInvocation;
|
|
54
58
|
exports.stripSecretArgs = stripSecretArgs;
|
|
@@ -59,12 +63,15 @@ exports.recordedAgentHandle = recordedAgentHandle;
|
|
|
59
63
|
exports.extractEndpointResult = extractEndpointResult;
|
|
60
64
|
exports.agentHandle = agentHandle;
|
|
61
65
|
exports.prepareAgentSpawn = prepareAgentSpawn;
|
|
66
|
+
exports.prepareEndpointJob = prepareEndpointJob;
|
|
62
67
|
exports.reconcileBatchOutcomes = reconcileBatchOutcomes;
|
|
63
68
|
exports.runAgentBatchOutcomes = runAgentBatchOutcomes;
|
|
69
|
+
exports.runEndpointBatchOutcomes = runEndpointBatchOutcomes;
|
|
64
70
|
exports.shouldStreamAgentStderr = shouldStreamAgentStderr;
|
|
65
71
|
exports.runAgentProcess = runAgentProcess;
|
|
66
72
|
const fs = __importStar(require("node:fs"));
|
|
67
73
|
const path = __importStar(require("node:path"));
|
|
74
|
+
const os = __importStar(require("node:os"));
|
|
68
75
|
const node_child_process_1 = require("node:child_process");
|
|
69
76
|
const local_1 = require("./local");
|
|
70
77
|
const envelopes_1 = require("./envelopes");
|
|
@@ -85,16 +92,73 @@ const AGENT_PROVIDER_KEY_ENV_RE = /^(CW_|ANTHROPIC_|OPENAI_|GEMINI_|DEEPSEEK_|CO
|
|
|
85
92
|
* call it, so the allowlist cannot drift into a second silent copy again.
|
|
86
93
|
* Returns the forwarded var NAMES too (never values) for the
|
|
87
94
|
* worker.agent-env trust-audit event. */
|
|
95
|
+
/** A unique sidecar path a shipped wrapper writes its vendor child's PID to
|
|
96
|
+
* (via the env CW_AGENT_VENDOR_PIDFILE we set below), so cw can reap the
|
|
97
|
+
* vendor if it has to SIGKILL the wrapper on a timeout. */
|
|
98
|
+
function vendorPidFilePath() {
|
|
99
|
+
return path.join(os.tmpdir(), `cw-agent-vendor-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.pid`);
|
|
100
|
+
}
|
|
101
|
+
/** Reap the vendor process a shipped wrapper recorded, then remove the sidecar.
|
|
102
|
+
* A wrapper that exited cleanly already removed the file, so this is a no-op
|
|
103
|
+
* then; only a wrapper cw SIGKILLed on a timeout leaves a live vendor PID
|
|
104
|
+
* here. Best-effort and race-tolerant: a missing file, an unparseable/too-low
|
|
105
|
+
* PID, or an already-gone process are all silently fine. Scope: kills the
|
|
106
|
+
* vendor process itself (the token spender), not a deeper grandchild tree,
|
|
107
|
+
* and only for the shipped vendor wrappers (claude, codex, gemini, and
|
|
108
|
+
* opencode/deepseek) -- an arbitrary CW_AGENT_COMMAND records no PID and is
|
|
109
|
+
* not covered. Returns true only if it
|
|
110
|
+
* actually signalled a process. Exported for tests; agent.ts is not part of
|
|
111
|
+
* the package public surface (index.ts). */
|
|
112
|
+
function reapRecordedVendor(pidFile) {
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = fs.readFileSync(pidFile, "utf8");
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false; // no sidecar => the wrapper cleaned up (or never recorded one)
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
fs.unlinkSync(pidFile);
|
|
122
|
+
}
|
|
123
|
+
catch { /* already gone */ }
|
|
124
|
+
const pid = Number(raw.trim());
|
|
125
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
126
|
+
return false;
|
|
127
|
+
try {
|
|
128
|
+
process.kill(pid, "SIGKILL");
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false; // already exited, or a pid we may not signal
|
|
133
|
+
}
|
|
134
|
+
}
|
|
88
135
|
function buildAgentChildEnv(policy, baseEnv = process.env) {
|
|
89
136
|
const env = (0, local_1.buildChildEnv)(policy, baseEnv);
|
|
137
|
+
// deny must win here too: buildChildEnv already applied it above, but the
|
|
138
|
+
// provider-key/USER re-add below exists specifically to put vars BACK
|
|
139
|
+
// that buildChildEnv's readonly/locked-down defaults strip — without this
|
|
140
|
+
// check that re-add silently overrode an operator's explicit deny (e.g.
|
|
141
|
+
// deny:["AWS_SECRET_ACCESS_KEY"] still forwarded it, since AWS_ matches
|
|
142
|
+
// AGENT_PROVIDER_KEY_ENV_RE).
|
|
143
|
+
const denied = new Set(policy.env.deny || []);
|
|
90
144
|
const forwarded = [];
|
|
91
145
|
for (const key of Object.keys(baseEnv)) {
|
|
146
|
+
if (denied.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
// Parent-only CW secrets are never re-added, even though they match the
|
|
149
|
+
// CW_ arm of AGENT_PROVIDER_KEY_ENV_RE and even if the operator forgot to
|
|
150
|
+
// deny them. buildChildEnv already stripped them above; this keeps the
|
|
151
|
+
// re-add loop from putting them back (and out of the trust-audit
|
|
152
|
+
// `forwarded` list). CW_AGENT_ATTEST_PRIVKEY is intentionally NOT in that
|
|
153
|
+
// set, so the attest wrapper still gets its signing key.
|
|
154
|
+
if (local_1.CW_NEVER_FORWARD_ENV.has(key))
|
|
155
|
+
continue;
|
|
92
156
|
if (AGENT_PROVIDER_KEY_ENV_RE.test(key)) {
|
|
93
157
|
env[key] = baseEnv[key];
|
|
94
158
|
forwarded.push(key);
|
|
95
159
|
}
|
|
96
160
|
}
|
|
97
|
-
if (baseEnv.USER !== undefined && env.USER === undefined) {
|
|
161
|
+
if (!denied.has("USER") && baseEnv.USER !== undefined && env.USER === undefined) {
|
|
98
162
|
env.USER = baseEnv.USER;
|
|
99
163
|
forwarded.push("USER");
|
|
100
164
|
}
|
|
@@ -288,9 +352,9 @@ function agentHandle(request, env = process.env) {
|
|
|
288
352
|
},
|
|
289
353
|
};
|
|
290
354
|
}
|
|
291
|
-
/** Resolve a request to a spawn-style batch job, or undefined when
|
|
292
|
-
* is endpoint-configured
|
|
293
|
-
* path). */
|
|
355
|
+
/** Resolve a request to a spawn-style (CLI-binary) batch job, or undefined when
|
|
356
|
+
* the agent is endpoint-configured (see prepareEndpointJob) or unconfigured
|
|
357
|
+
* (those settle through the serial path). */
|
|
294
358
|
function prepareAgentSpawn(request) {
|
|
295
359
|
const resolved = resolveAgentInvocation(request);
|
|
296
360
|
if (!resolved.binary)
|
|
@@ -303,6 +367,27 @@ function prepareAgentSpawn(request) {
|
|
|
303
367
|
timeoutMs: resolved.timeoutMs || 600000,
|
|
304
368
|
};
|
|
305
369
|
}
|
|
370
|
+
/** Resolve a request to an endpoint (HTTP-delegate) batch job, or undefined when
|
|
371
|
+
* the agent has a CLI binary (see prepareAgentSpawn) or is unconfigured. The
|
|
372
|
+
* POST body mirrors runAgentEndpoint's exactly: {manifest, prompt, model,
|
|
373
|
+
* resultPath, sandboxProfileId} — sandboxProfileId comes from
|
|
374
|
+
* request.sandboxPolicy.id, which runBackend sets to the same policy the serial
|
|
375
|
+
* path passes (registry.ts: `const policy = request.sandboxPolicy`), so the two
|
|
376
|
+
* paths post identical bytes. */
|
|
377
|
+
function prepareEndpointJob(request) {
|
|
378
|
+
const resolved = resolveAgentInvocation(request);
|
|
379
|
+
if (resolved.binary || !resolved.endpoint)
|
|
380
|
+
return undefined;
|
|
381
|
+
const manifest = request.manifest;
|
|
382
|
+
const job = JSON.stringify({
|
|
383
|
+
manifest,
|
|
384
|
+
prompt: manifest?.prompt,
|
|
385
|
+
model: resolved.model,
|
|
386
|
+
resultPath: manifest?.resultPath,
|
|
387
|
+
sandboxProfileId: request.sandboxPolicy.id,
|
|
388
|
+
});
|
|
389
|
+
return { endpoint: resolved.endpoint, job, timeoutMs: resolved.timeoutMs || 600000 };
|
|
390
|
+
}
|
|
306
391
|
// __dirname is dist/shell/execution-backend at runtime — THREE levels up
|
|
307
392
|
// (execution-backend -> shell -> dist -> package root) reaches scripts/,
|
|
308
393
|
// a sibling of dist/, not two (that was a bug: it resolved to a
|
|
@@ -367,6 +452,36 @@ function runAgentBatchOutcomes(jobs) {
|
|
|
367
452
|
}
|
|
368
453
|
return reconcileBatchOutcomes(jobs, child);
|
|
369
454
|
}
|
|
455
|
+
// Same package-root depth fix as BATCH_DELEGATE_CHILD_SCRIPT above.
|
|
456
|
+
const HTTP_BATCH_DELEGATE_CHILD_SCRIPT = path.resolve(__dirname, "..", "..", "..", "scripts", "children", "http-batch-delegate-child.js");
|
|
457
|
+
/** Run a batch of ENDPOINT (HTTP-delegate) jobs concurrently; outcomes
|
|
458
|
+
* index-align with jobs. The endpoint sibling of runAgentBatchOutcomes: one
|
|
459
|
+
* spawnSync of http-batch-delegate-child.js POSTs all N at once from a single
|
|
460
|
+
* process, so `--concurrency N` with an endpoint agent runs N delegations in
|
|
461
|
+
* parallel instead of serially. The child streams the SAME NDJSON line shape as
|
|
462
|
+
* batch-delegate-child.js — `{i, spawnError?, exitCode, stdout}` — so the same
|
|
463
|
+
* reconcileBatchOutcomes reads it (byte-boundary split on a Buffer, index-aligned,
|
|
464
|
+
* every job that produced no full line failed closed). Env is inherited (the
|
|
465
|
+
* serial runAgentEndpoint likewise runs the child with the full process env for
|
|
466
|
+
* any endpoint auth); no sandbox child-env is built here, matching the serial
|
|
467
|
+
* endpoint path. */
|
|
468
|
+
function runEndpointBatchOutcomes(jobs) {
|
|
469
|
+
if (!jobs.length)
|
|
470
|
+
return [];
|
|
471
|
+
const maxTimeout = Math.max(...jobs.map((job) => job.timeoutMs));
|
|
472
|
+
let child;
|
|
473
|
+
try {
|
|
474
|
+
child = (0, node_child_process_1.spawnSync)(process.execPath, [HTTP_BATCH_DELEGATE_CHILD_SCRIPT], {
|
|
475
|
+
input: JSON.stringify(jobs),
|
|
476
|
+
maxBuffer: 34 * 1024 * 1024 * jobs.length,
|
|
477
|
+
timeout: maxTimeout + 30000,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
child = { error: error instanceof Error ? error : new Error(String(error)), status: null, stdout: null };
|
|
482
|
+
}
|
|
483
|
+
return reconcileBatchOutcomes(jobs, child);
|
|
484
|
+
}
|
|
370
485
|
// ---------------------------------------------------------------------------
|
|
371
486
|
// The stateful agent runners (spawn + settle). Byte-exact port of
|
|
372
487
|
// src/execution-backend.ts's runAgentProcess (:915-995), runAgentEndpoint
|
|
@@ -406,9 +521,15 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
|
|
|
406
521
|
else {
|
|
407
522
|
const streamStderr = shouldStreamAgentStderr(process.env, Boolean(process.stderr.isTTY));
|
|
408
523
|
const built = buildAgentChildEnv(policy);
|
|
409
|
-
const childEnv = built.env;
|
|
410
524
|
forwardedEnvVars = built.forwarded;
|
|
411
525
|
const timeoutMs = resolved.timeoutMs || 600000;
|
|
526
|
+
// A shipped wrapper (claude/codex/gemini) records its vendor child's PID
|
|
527
|
+
// here so we can reap the vendor if the timeout below SIGKILLs the
|
|
528
|
+
// wrapper -- a SIGKILL is uncatchable, so the wrapper cannot forward the
|
|
529
|
+
// stop itself. Not a forwarded secret; it is cw plumbing, so it stays out
|
|
530
|
+
// of `built.forwarded` / the trust-audit forwarded list.
|
|
531
|
+
const vendorPidFile = vendorPidFilePath();
|
|
532
|
+
const childEnv = { ...built.env, CW_AGENT_VENDOR_PIDFILE: vendorPidFile };
|
|
412
533
|
const child = (0, node_child_process_1.spawnSync)(resolved.binary, realArgs, {
|
|
413
534
|
cwd: request.cwd,
|
|
414
535
|
env: childEnv,
|
|
@@ -430,12 +551,30 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
|
|
|
430
551
|
// and the null-exit-code "timed out or killed" branch was dead code
|
|
431
552
|
// for real timeouts.
|
|
432
553
|
if (child.error && child.error.code === "ETIMEDOUT") {
|
|
554
|
+
// spawnSync SIGKILLed the wrapper (uncatchable, so the wrapper could
|
|
555
|
+
// not forward the stop to its vendor child). Reap the vendor a shipped
|
|
556
|
+
// wrapper recorded, so a timed-out agent's vendor process does not live
|
|
557
|
+
// on as an orphan and keep spending. Scoped: reaps the vendor process
|
|
558
|
+
// itself for the shipped wrappers (claude, codex, gemini, opencode/
|
|
559
|
+
// deepseek), not a deeper grandchild tree nor an arbitrary
|
|
560
|
+
// CW_AGENT_COMMAND (which records no PID). We do
|
|
561
|
+
// NOT use `detached:true` -- that would make the wrapper its own group
|
|
562
|
+
// leader, so an interactive Ctrl-C / a group SIGINT to cw would no
|
|
563
|
+
// longer reach it (strictly worse); reaping from the recorded PID keeps
|
|
564
|
+
// the wrapper in cw's group.
|
|
565
|
+
reapRecordedVendor(vendorPidFile);
|
|
433
566
|
const handleOut = recordedAgentHandle(resolved.binary, undefined, recordedArgs, resolved.model, "unreported", undefined, undefined, forwardedEnvVars);
|
|
434
567
|
return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", `agent process timed out after ${timeoutMs}ms and was killed (SIGKILL)`, {
|
|
435
568
|
...attestation,
|
|
436
569
|
handle: handleOut,
|
|
437
570
|
});
|
|
438
571
|
}
|
|
572
|
+
// Non-timeout paths: a wrapper that ran to any normal end removed its own
|
|
573
|
+
// sidecar on exit; clear any stray file best-effort so tmp cannot grow.
|
|
574
|
+
try {
|
|
575
|
+
fs.unlinkSync(vendorPidFile);
|
|
576
|
+
}
|
|
577
|
+
catch { /* usually already gone */ }
|
|
439
578
|
outcome = {
|
|
440
579
|
...(child.error ? { spawnError: messageOf(child.error) } : {}),
|
|
441
580
|
exitCode: typeof child.status === "number" ? child.status : null,
|
|
@@ -464,12 +603,70 @@ function runAgentProcess(descriptor, policy, request, label, attestation) {
|
|
|
464
603
|
}
|
|
465
604
|
return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-target-missing", `Backend ${descriptor.id} has no command-template or endpoint configured`, attestation);
|
|
466
605
|
}
|
|
606
|
+
/** Settle a completed endpoint delegation (a numeric exitCode + stdout) into
|
|
607
|
+
* its envelope: parse the agent report, write result.md as transport when the
|
|
608
|
+
* runner returned a body and none exists yet, record the handle. Shared by the
|
|
609
|
+
* serial spawn path and the concurrent prepared-outcome path so the two never
|
|
610
|
+
* drift. The result-write stays HERE (the cw shell process), never in the batch
|
|
611
|
+
* child, so the child does no filesystem writes into the run dir. */
|
|
612
|
+
function settleEndpointResult(descriptor, label, endpoint, resolvedModel, manifest, attestation, exitCode, stdout) {
|
|
613
|
+
const report = parseAgentReport(stdout);
|
|
614
|
+
if (manifest?.resultPath && report.usage === undefined) {
|
|
615
|
+
const body = extractEndpointResult(stdout);
|
|
616
|
+
if (body && !fs.existsSync(manifest.resultPath)) {
|
|
617
|
+
try {
|
|
618
|
+
fs.writeFileSync(manifest.resultPath, body, "utf8");
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
/* the accept layer will fail closed on a missing result.md */
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
|
|
626
|
+
const handleOut = recordedAgentHandle(undefined, endpoint, [], resolvedModel, reportedModel, report.usage, report.usageSignature);
|
|
627
|
+
return (0, envelopes_1.delegatedEnvelope)(descriptor, label, handleOut, { ...attestation, handle: handleOut }, "agent-endpoint", [endpoint], exitCode, stdout);
|
|
628
|
+
}
|
|
467
629
|
/** Agent HTTP endpoint variant — POSTs the worker manifest/prompt to a
|
|
468
630
|
* configured agent endpoint via the shared Node delegate child. Byte-exact
|
|
469
|
-
* port of src/execution-backend.ts:1002-1062
|
|
631
|
+
* port of src/execution-backend.ts:1002-1062, plus a prepared-outcome branch:
|
|
632
|
+
* in a concurrent round the POST already ran in the batch child
|
|
633
|
+
* (runEndpointBatchOutcomes), so settle that pre-collected outcome instead of
|
|
634
|
+
* spawning again. The serial (no prepared outcome) path is unchanged. */
|
|
470
635
|
function runAgentEndpoint(descriptor, policy, request, label, resolved, attestation) {
|
|
471
636
|
const endpoint = resolved.endpoint;
|
|
472
637
|
const manifest = request.manifest;
|
|
638
|
+
const baseHandle = recordedAgentHandle(undefined, endpoint, [], resolved.model, "unreported");
|
|
639
|
+
// Concurrent round: the batch child already POSTed and settled this job. Any
|
|
640
|
+
// failure (network, non-2xx, poll, unparseable, timeout, no exitCode) is a
|
|
641
|
+
// spawnError; a success carries a numeric exitCode + stdout.
|
|
642
|
+
if (request.preparedAgentOutcome) {
|
|
643
|
+
const prep = request.preparedAgentOutcome;
|
|
644
|
+
if (prep.spawnError) {
|
|
645
|
+
// Match the SERIAL path's refusal wording so a concurrent round records the
|
|
646
|
+
// same reason bytes as a serial one. A per-job HTTP failure (non-2xx, poll,
|
|
647
|
+
// timeout, no exitCode) is the serial `{error}` branch — `agent endpoint
|
|
648
|
+
// error:`. A whole-batch-child process failure surfaces as
|
|
649
|
+
// reconcileBatchOutcomes' `batch delegate failed:` fallback — the serial
|
|
650
|
+
// analog is `child.error` — `agent endpoint delegation failed:`.
|
|
651
|
+
const summary = prep.spawnError.startsWith("batch delegate failed:")
|
|
652
|
+
? `agent endpoint delegation failed: ${prep.spawnError}`
|
|
653
|
+
: `agent endpoint error: ${prep.spawnError}`;
|
|
654
|
+
return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", summary, {
|
|
655
|
+
...attestation,
|
|
656
|
+
handle: baseHandle,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// Defensive: the batch child always pairs a null exitCode with a spawnError,
|
|
660
|
+
// so this is unreachable in practice — kept as a fail-closed backstop with
|
|
661
|
+
// the same wording the serial no-exitCode refusal uses.
|
|
662
|
+
if (typeof prep.exitCode !== "number") {
|
|
663
|
+
return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", `agent endpoint error: no exitCode reported`, {
|
|
664
|
+
...attestation,
|
|
665
|
+
handle: baseHandle,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
return settleEndpointResult(descriptor, label, endpoint, resolved.model, manifest, attestation, prep.exitCode, prep.stdout);
|
|
669
|
+
}
|
|
473
670
|
const job = JSON.stringify({
|
|
474
671
|
manifest,
|
|
475
672
|
prompt: manifest?.prompt,
|
|
@@ -484,7 +681,6 @@ function runAgentEndpoint(descriptor, policy, request, label, resolved, attestat
|
|
|
484
681
|
timeout: resolved.timeoutMs || 600000,
|
|
485
682
|
maxBuffer: 32 * 1024 * 1024,
|
|
486
683
|
});
|
|
487
|
-
const baseHandle = recordedAgentHandle(undefined, endpoint, [], resolved.model, "unreported");
|
|
488
684
|
if (child.error) {
|
|
489
685
|
return (0, envelopes_1.refusedEnvelope)(descriptor, policy, label, "delegation-failed", `agent endpoint delegation failed: ${messageOf(child.error)}`, {
|
|
490
686
|
...attestation,
|
|
@@ -507,20 +703,5 @@ function runAgentEndpoint(descriptor, policy, request, label, resolved, attestat
|
|
|
507
703
|
handle: baseHandle,
|
|
508
704
|
});
|
|
509
705
|
}
|
|
510
|
-
|
|
511
|
-
const report = parseAgentReport(stdout);
|
|
512
|
-
if (manifest?.resultPath && report.usage === undefined) {
|
|
513
|
-
const body = extractEndpointResult(stdout);
|
|
514
|
-
if (body && !fs.existsSync(manifest.resultPath)) {
|
|
515
|
-
try {
|
|
516
|
-
fs.writeFileSync(manifest.resultPath, body, "utf8");
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
/* the accept layer will fail closed on a missing result.md */
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
const reportedModel = report.model && report.model.trim() ? report.model.trim() : "unreported";
|
|
524
|
-
const handleOut = recordedAgentHandle(undefined, endpoint, [], resolved.model, reportedModel, report.usage, report.usageSignature);
|
|
525
|
-
return (0, envelopes_1.delegatedEnvelope)(descriptor, label, handleOut, { ...attestation, handle: handleOut }, "agent-endpoint", [endpoint], parsed.exitCode, stdout);
|
|
706
|
+
return settleEndpointResult(descriptor, label, endpoint, resolved.model, manifest, attestation, parsed.exitCode, String(parsed.stdout || ""));
|
|
526
707
|
}
|