harnery 0.12.1 → 0.13.0
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/README.md +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +193 -0
- package/dist/core/agents/coord-client.d.ts +8 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +18 -2
- package/dist/core/policy/types.d.ts +1 -1
- package/dist/core/policy/types.d.ts.map +1 -1
- package/dist/core/scratch/index.d.ts.map +1 -1
- package/dist/core/scratch/index.js +3 -3
- package/dist/core/workflow/approvals.d.ts +71 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -0
- package/dist/core/workflow/approvals.js +288 -0
- package/dist/core/workflow/engine.d.ts +6 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +224 -43
- package/dist/core/workflow/index.d.ts +6 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +3 -1
- package/dist/core/workflow/run-state.d.ts +43 -0
- package/dist/core/workflow/run-state.d.ts.map +1 -0
- package/dist/core/workflow/run-state.js +231 -0
- package/dist/core/workflow/types.d.ts +9 -0
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/workflow.ts +226 -0
- package/src/core/agents/coord-client.ts +19 -2
- package/src/core/policy/types.ts +1 -1
- package/src/core/scratch/index.ts +3 -2
- package/src/core/workflow/approvals.ts +393 -0
- package/src/core/workflow/engine.ts +268 -44
- package/src/core/workflow/index.ts +31 -1
- package/src/core/workflow/run-state.ts +287 -0
- package/src/core/workflow/types.ts +9 -0
|
@@ -30,8 +30,10 @@ import { isAbsolute, join, resolve } from "node:path";
|
|
|
30
30
|
import { pathToFileURL } from "node:url";
|
|
31
31
|
import { snapshotRepo } from "../context/index.js";
|
|
32
32
|
import { evaluatePolicy, normalizePolicy, PolicyDeniedError, policyDigest, summarizePolicyRequest, } from "../policy/index.js";
|
|
33
|
+
import { createWorkflowApproval } from "./approvals.js";
|
|
33
34
|
import { probeBilling } from "./billing.js";
|
|
34
35
|
import { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, writeWorkflowProof, } from "./proof.js";
|
|
36
|
+
import { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
|
|
35
37
|
import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
|
|
36
38
|
import { parseStageOutput, validateAgainstSchema } from "./validate.js";
|
|
37
39
|
const DEFAULT_MAX_AGENTS = 50;
|
|
@@ -51,8 +53,48 @@ export class WorkflowRunError extends Error {
|
|
|
51
53
|
this.proofPath = proofPath;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
56
|
+
export class WorkflowParkedError extends Error {
|
|
57
|
+
runId;
|
|
58
|
+
approvalId;
|
|
59
|
+
journalPath;
|
|
60
|
+
constructor(message, runId, approvalId, journalPath) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.name = "WorkflowParkedError";
|
|
63
|
+
this.runId = runId;
|
|
64
|
+
this.approvalId = approvalId;
|
|
65
|
+
this.journalPath = journalPath;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
54
68
|
export async function runWorkflow(scriptPath, opts) {
|
|
69
|
+
if (opts.resumeRunId && opts.resumeFrom) {
|
|
70
|
+
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
71
|
+
}
|
|
72
|
+
const resumeState = opts.resumeRunId
|
|
73
|
+
? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
|
|
74
|
+
: undefined;
|
|
55
75
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
76
|
+
if (resumeState) {
|
|
77
|
+
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
78
|
+
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different script`);
|
|
79
|
+
}
|
|
80
|
+
assertWorkflowScriptUnchanged(resumeState.manifest);
|
|
81
|
+
}
|
|
82
|
+
const releaseResumeLease = opts.resumeRunId
|
|
83
|
+
? acquireWorkflowResumeLease(opts.coordRoot, opts.resumeRunId)
|
|
84
|
+
: undefined;
|
|
85
|
+
try {
|
|
86
|
+
if (resumeState) {
|
|
87
|
+
// Recheck under the exclusive lease. Another process may have completed
|
|
88
|
+
// the run between the optimistic read and lease acquisition.
|
|
89
|
+
assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
|
|
90
|
+
}
|
|
91
|
+
return await executeWorkflow(scriptPath, absScript, opts, resumeState);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
releaseResumeLease?.();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function executeWorkflow(scriptPath, absScript, opts, resumeState) {
|
|
56
98
|
const mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(absScript).href)));
|
|
57
99
|
if (typeof mod.default !== "function") {
|
|
58
100
|
throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
|
|
@@ -60,20 +102,56 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
60
102
|
const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
|
|
61
103
|
const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
62
104
|
const name = meta.name;
|
|
63
|
-
const runId =
|
|
105
|
+
const runId = opts.resumeRunId ??
|
|
106
|
+
`wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
|
|
64
107
|
const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
|
|
65
108
|
mkdirSync(runDir, { recursive: true });
|
|
66
109
|
const journalPath = join(runDir, "journal.jsonl");
|
|
67
110
|
const proofPath = join(runDir, "proof.json");
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
111
|
+
const frozen = resumeState?.manifest.execution;
|
|
112
|
+
const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
|
|
113
|
+
const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
|
|
114
|
+
assertPositiveWorkflowBound(maxAgents, "maxAgents");
|
|
115
|
+
assertPositiveWorkflowBound(concurrency, "concurrency");
|
|
116
|
+
const cwd = frozen?.cwd ?? opts.cwd ?? opts.coordRoot;
|
|
71
117
|
const log = opts.onLog ?? ((line) => process.stderr.write(`${line}\n`));
|
|
72
|
-
const defaultHarness = opts.defaultHarness ?? "claude-code";
|
|
73
|
-
const isolation = opts.isolation ?? "shared";
|
|
74
|
-
const networkAccess = opts.networkAccess ?? "unknown";
|
|
75
|
-
const policy = opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined;
|
|
76
|
-
const
|
|
118
|
+
const defaultHarness = frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
|
|
119
|
+
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
120
|
+
const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
|
|
121
|
+
const policy = frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
|
|
122
|
+
const approvalMode = frozen?.approval_mode ?? opts.approvalMode ?? "deny";
|
|
123
|
+
const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
|
|
124
|
+
const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
|
|
125
|
+
const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
|
|
126
|
+
const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
|
|
127
|
+
const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
|
|
128
|
+
const t0 = resumeState ? Date.parse(startedAt) : Date.now();
|
|
129
|
+
if (!resumeState) {
|
|
130
|
+
writeWorkflowRunManifest({
|
|
131
|
+
coordRoot: opts.coordRoot,
|
|
132
|
+
manifest: {
|
|
133
|
+
schema_version: 1,
|
|
134
|
+
run_id: runId,
|
|
135
|
+
name,
|
|
136
|
+
started_at: startedAt,
|
|
137
|
+
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
138
|
+
repository_before: repoBefore,
|
|
139
|
+
execution: {
|
|
140
|
+
cwd,
|
|
141
|
+
default_harness: defaultHarness,
|
|
142
|
+
max_agents: maxAgents,
|
|
143
|
+
concurrency,
|
|
144
|
+
subscription_only: subscriptionOnly,
|
|
145
|
+
allow_api_billing: allowApiBilling,
|
|
146
|
+
approval_mode: approvalMode,
|
|
147
|
+
approval_addressee: approvalAddressee,
|
|
148
|
+
isolation,
|
|
149
|
+
network_access: networkAccess,
|
|
150
|
+
policy: policy ? policy : undefined,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
77
155
|
// Per-child fixed context overhead: children spawn in `cwd` and load its
|
|
78
156
|
// repo-instructions file into their system prompt, cache-writing it once
|
|
79
157
|
// per child. A fan-out multiplies this, so surface it BEFORE the burn.
|
|
@@ -83,12 +161,14 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
83
161
|
`instructions from ${cwd}; a fan-out multiplies this per agent`);
|
|
84
162
|
}
|
|
85
163
|
// Resume: journaled results of a prior run, keyed by agent-call identity.
|
|
86
|
-
const
|
|
87
|
-
|
|
164
|
+
const resumeSource = opts.resumeRunId ?? opts.resumeFrom;
|
|
165
|
+
const resumeCache = resumeSource
|
|
166
|
+
? loadResumeCache(opts.coordRoot, resumeSource)
|
|
88
167
|
: new Map();
|
|
89
|
-
|
|
168
|
+
const history = opts.resumeRunId ? loadRunHistory(journalPath) : undefined;
|
|
169
|
+
let agentsSpawned = history?.agentsSpawned ?? 0;
|
|
90
170
|
let agentsCached = 0;
|
|
91
|
-
let costUsd = 0;
|
|
171
|
+
let costUsd = history?.costUsd ?? 0;
|
|
92
172
|
let reservedCostUsd = 0;
|
|
93
173
|
let currentStage = "";
|
|
94
174
|
let agentSeq = 0;
|
|
@@ -97,7 +177,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
97
177
|
const billingProbed = new Map();
|
|
98
178
|
const agentProofs = new Map();
|
|
99
179
|
const evidenceRecords = [];
|
|
100
|
-
const policyDecisions = [];
|
|
180
|
+
const policyDecisions = new Map(history?.policyDecisions.map((decision) => [decision.id, decision]));
|
|
101
181
|
const acceptanceIds = new Set(meta.acceptance.map((criterion) => criterion.id));
|
|
102
182
|
const journal = (event, data) => {
|
|
103
183
|
const line = JSON.stringify({
|
|
@@ -149,7 +229,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
149
229
|
if (!policy) {
|
|
150
230
|
throw new Error("external mutation authorization requires a host policy");
|
|
151
231
|
}
|
|
152
|
-
if (policyDecisions.
|
|
232
|
+
if (policyDecisions.size >= MAX_POLICY_DECISIONS && !policyDecisions.has(`p${policySeq + 1}`)) {
|
|
153
233
|
throw new Error(`workflow policy decision cap reached (${MAX_POLICY_DECISIONS}); split the workflow or reduce protected actions`);
|
|
154
234
|
}
|
|
155
235
|
const request = summarizePolicyRequest(rawRequest);
|
|
@@ -169,10 +249,8 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
169
249
|
let reason = evaluation.reason;
|
|
170
250
|
if (evaluation.verdict === "ask") {
|
|
171
251
|
resolvedBy = "fail_closed";
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
252
|
+
let immediateResolution = false;
|
|
253
|
+
if (opts.resolvePolicyAsk) {
|
|
176
254
|
try {
|
|
177
255
|
const resolution = await withTimeout(Promise.resolve(opts.resolvePolicyAsk(request, evaluation)), opts.policyAskTimeoutMs ?? DEFAULT_POLICY_ASK_TIMEOUT_MS);
|
|
178
256
|
if (!resolution || (resolution.verdict !== "allow" && resolution.verdict !== "deny")) {
|
|
@@ -182,12 +260,54 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
182
260
|
verdict = resolution.verdict;
|
|
183
261
|
resolvedBy = "host";
|
|
184
262
|
reason = boundedPolicyReason(resolution.reason ?? evaluation.reason);
|
|
263
|
+
immediateResolution = true;
|
|
185
264
|
}
|
|
186
265
|
}
|
|
187
266
|
catch (error) {
|
|
188
267
|
reason = `${evaluation.reason}; approval failed closed: ${error.message}`;
|
|
189
268
|
}
|
|
190
269
|
}
|
|
270
|
+
else {
|
|
271
|
+
reason = `${evaluation.reason}; no host approval resolver is configured`;
|
|
272
|
+
}
|
|
273
|
+
if (!immediateResolution && approvalMode === "park") {
|
|
274
|
+
const stored = createWorkflowApproval({
|
|
275
|
+
coordRoot: opts.coordRoot,
|
|
276
|
+
runId,
|
|
277
|
+
decisionId: id,
|
|
278
|
+
addressedTo: approvalAddressee,
|
|
279
|
+
policy: { name: policy.name, sha256: policyDigest(policy) },
|
|
280
|
+
request,
|
|
281
|
+
evaluation,
|
|
282
|
+
});
|
|
283
|
+
if (stored.approval.status === "pending") {
|
|
284
|
+
if (stored.created) {
|
|
285
|
+
journal("approval.requested", {
|
|
286
|
+
approval_id: stored.approval.request.id,
|
|
287
|
+
decision_id: id,
|
|
288
|
+
addressed_to: stored.approval.request.addressed_to,
|
|
289
|
+
request_sha256: stored.approval.request.request_sha256,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
journal("run.parked", {
|
|
293
|
+
approval_id: stored.approval.request.id,
|
|
294
|
+
decision_id: id,
|
|
295
|
+
phase: request.phase,
|
|
296
|
+
action: request.action,
|
|
297
|
+
});
|
|
298
|
+
throw new WorkflowParkedError(`workflow parked for approval ${stored.approval.request.id}: ${evaluation.reason}`, runId, stored.approval.request.id, journalPath);
|
|
299
|
+
}
|
|
300
|
+
verdict = stored.approval.decision.verdict;
|
|
301
|
+
resolvedBy = "approval";
|
|
302
|
+
reason = boundedPolicyReason(stored.approval.decision.reason ?? evaluation.reason);
|
|
303
|
+
journal("approval.consumed", {
|
|
304
|
+
approval_id: stored.approval.request.id,
|
|
305
|
+
decision_id: id,
|
|
306
|
+
verdict,
|
|
307
|
+
actor: stored.approval.decision.actor,
|
|
308
|
+
decided_at: stored.approval.decision.decided_at,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
191
311
|
}
|
|
192
312
|
const decision = {
|
|
193
313
|
id,
|
|
@@ -201,7 +321,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
201
321
|
rule_codes: evaluation.rules.map((rule) => rule.code),
|
|
202
322
|
request,
|
|
203
323
|
};
|
|
204
|
-
policyDecisions.
|
|
324
|
+
policyDecisions.set(decision.id, decision);
|
|
205
325
|
journal("policy.resolve", { ...decision });
|
|
206
326
|
if (decision.verdict === "deny") {
|
|
207
327
|
throw new PolicyDeniedError(`policy ${JSON.stringify(policy.name)} denied ${request.phase} ${JSON.stringify(request.action)}: ${decision.reason}`, decision.id);
|
|
@@ -236,6 +356,11 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
236
356
|
const key = agentCallKey(currentStage, harness, agentOpts, prompt);
|
|
237
357
|
const cached = resumeCache.get(key);
|
|
238
358
|
if (cached) {
|
|
359
|
+
// Exact-run replay skips dispatch authorization because no dispatch
|
|
360
|
+
// occurs, but it must still reserve the original policy slot so a later
|
|
361
|
+
// durable ASK resolves against the same pN identity.
|
|
362
|
+
if (policy && opts.resumeRunId)
|
|
363
|
+
policySeq++;
|
|
239
364
|
agentsCached++;
|
|
240
365
|
agentProof.status = "cached";
|
|
241
366
|
agentProof.result = digestResult(cached.value, cached.kind);
|
|
@@ -247,7 +372,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
247
372
|
model: agentOpts.model ?? null,
|
|
248
373
|
kind: cached.kind,
|
|
249
374
|
});
|
|
250
|
-
log(`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${
|
|
375
|
+
log(`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${resumeSource})`);
|
|
251
376
|
return cached.value;
|
|
252
377
|
}
|
|
253
378
|
if (policy) {
|
|
@@ -327,19 +452,19 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
327
452
|
billingProbed.set(harness, probe);
|
|
328
453
|
journal("billing.probe", {
|
|
329
454
|
harness,
|
|
330
|
-
mode:
|
|
455
|
+
mode: subscriptionOnly ? "subscription" : probe.mode,
|
|
331
456
|
api_key_source: probe.apiKeySource,
|
|
332
457
|
login: probe.login,
|
|
333
|
-
subscription_only:
|
|
458
|
+
subscription_only: subscriptionOnly,
|
|
334
459
|
});
|
|
335
|
-
if (
|
|
460
|
+
if (subscriptionOnly) {
|
|
336
461
|
if (probe.login === "absent") {
|
|
337
462
|
throw new Error(`subscription-only: no stored login detected for ${harness}; ` +
|
|
338
463
|
`log the harness CLI in (or drop --subscription-only for a key-only host)`);
|
|
339
464
|
}
|
|
340
465
|
log(`[billing] ${harness}: subscription-only (API-key vars scrubbed from child env)`);
|
|
341
466
|
}
|
|
342
|
-
else if (probe.mode === "api-key-override" && !
|
|
467
|
+
else if (probe.mode === "api-key-override" && !allowApiBilling) {
|
|
343
468
|
throw new Error(`${probe.apiKeySource} is set AND a stored ${harness} login exists — the key silently ` +
|
|
344
469
|
`overrides your subscription auth, so children would bill per-token API rates. ` +
|
|
345
470
|
`Either unset ${probe.apiKeySource}, run with --subscription-only to scrub it from ` +
|
|
@@ -392,7 +517,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
392
517
|
maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
393
518
|
cwd,
|
|
394
519
|
runId,
|
|
395
|
-
subscriptionOnly
|
|
520
|
+
subscriptionOnly,
|
|
396
521
|
});
|
|
397
522
|
agentProof.attempts = attempt;
|
|
398
523
|
agentProof.duration_ms += last.durationMs;
|
|
@@ -413,6 +538,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
413
538
|
key,
|
|
414
539
|
attempts: attempt,
|
|
415
540
|
cost_usd: last.costUsd,
|
|
541
|
+
total_cost_usd: agentCostUsd,
|
|
416
542
|
duration_ms: last.durationMs,
|
|
417
543
|
session_id: last.sessionId,
|
|
418
544
|
result_kind: "text",
|
|
@@ -435,6 +561,7 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
435
561
|
key,
|
|
436
562
|
attempts: attempt,
|
|
437
563
|
cost_usd: last.costUsd,
|
|
564
|
+
total_cost_usd: agentCostUsd,
|
|
438
565
|
duration_ms: last.durationMs,
|
|
439
566
|
session_id: last.sessionId,
|
|
440
567
|
result_kind: "json",
|
|
@@ -511,20 +638,27 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
511
638
|
});
|
|
512
639
|
};
|
|
513
640
|
const ctx = { agent, parallel, stage, log, evidence, authorize };
|
|
514
|
-
const t0 = Date.now();
|
|
515
|
-
const startedAt = new Date(t0).toISOString();
|
|
516
|
-
journal("run.start", {
|
|
517
|
-
name,
|
|
518
|
-
script: absScript,
|
|
519
|
-
objective: meta.objective ?? null,
|
|
520
|
-
acceptance: meta.acceptance,
|
|
521
|
-
max_agents: maxAgents,
|
|
522
|
-
concurrency,
|
|
523
|
-
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
524
|
-
isolation,
|
|
525
|
-
network_access: networkAccess,
|
|
526
|
-
});
|
|
527
641
|
try {
|
|
642
|
+
if (resumeState) {
|
|
643
|
+
journal("run.resume", {
|
|
644
|
+
approval_id: resumeState.approvalId,
|
|
645
|
+
script: absScript,
|
|
646
|
+
resumed_at: new Date().toISOString(),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
journal("run.start", {
|
|
651
|
+
name,
|
|
652
|
+
script: absScript,
|
|
653
|
+
objective: meta.objective ?? null,
|
|
654
|
+
acceptance: meta.acceptance,
|
|
655
|
+
max_agents: maxAgents,
|
|
656
|
+
concurrency,
|
|
657
|
+
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
658
|
+
isolation,
|
|
659
|
+
network_access: networkAccess,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
528
662
|
const result = await mod.default(ctx);
|
|
529
663
|
const endedAt = new Date().toISOString();
|
|
530
664
|
const durationMs = Date.now() - t0;
|
|
@@ -551,7 +685,12 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
551
685
|
evidence: evidenceRecords,
|
|
552
686
|
harnessEvidence: opts.harnessEvidence,
|
|
553
687
|
policy: policy
|
|
554
|
-
? {
|
|
688
|
+
? {
|
|
689
|
+
config: policy,
|
|
690
|
+
decisions: Array.from(policyDecisions.values()),
|
|
691
|
+
isolation,
|
|
692
|
+
networkAccess,
|
|
693
|
+
}
|
|
555
694
|
: undefined,
|
|
556
695
|
result,
|
|
557
696
|
});
|
|
@@ -574,14 +713,14 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
574
713
|
contextTokensPerChildEstimate,
|
|
575
714
|
billing: Array.from(billingProbed.values()).map((p) => ({
|
|
576
715
|
harness: p.harness,
|
|
577
|
-
mode:
|
|
716
|
+
mode: subscriptionOnly ? "subscription" : p.mode,
|
|
578
717
|
})),
|
|
579
718
|
policy: proof.policy?.summary,
|
|
580
719
|
};
|
|
581
720
|
return report;
|
|
582
721
|
}
|
|
583
722
|
catch (err) {
|
|
584
|
-
if (err instanceof WorkflowRunError)
|
|
723
|
+
if (err instanceof WorkflowParkedError || err instanceof WorkflowRunError)
|
|
585
724
|
throw err;
|
|
586
725
|
const endedAt = new Date().toISOString();
|
|
587
726
|
const durationMs = Date.now() - t0;
|
|
@@ -608,7 +747,12 @@ export async function runWorkflow(scriptPath, opts) {
|
|
|
608
747
|
evidence: evidenceRecords,
|
|
609
748
|
harnessEvidence: opts.harnessEvidence,
|
|
610
749
|
policy: policy
|
|
611
|
-
? {
|
|
750
|
+
? {
|
|
751
|
+
config: policy,
|
|
752
|
+
decisions: Array.from(policyDecisions.values()),
|
|
753
|
+
isolation,
|
|
754
|
+
networkAccess,
|
|
755
|
+
}
|
|
612
756
|
: undefined,
|
|
613
757
|
error: err.message,
|
|
614
758
|
});
|
|
@@ -676,6 +820,38 @@ function loadResumeCache(coordRoot, resumeFrom) {
|
|
|
676
820
|
}
|
|
677
821
|
return cache;
|
|
678
822
|
}
|
|
823
|
+
function loadRunHistory(path) {
|
|
824
|
+
const agentIds = new Set();
|
|
825
|
+
const agentCosts = new Map();
|
|
826
|
+
const decisions = new Map();
|
|
827
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
828
|
+
if (!line.trim())
|
|
829
|
+
continue;
|
|
830
|
+
const event = JSON.parse(line);
|
|
831
|
+
if (event.event === "agent.start" && typeof event.id === "string") {
|
|
832
|
+
agentIds.add(event.id);
|
|
833
|
+
}
|
|
834
|
+
if (event.event === "agent.end" && typeof event.id === "string") {
|
|
835
|
+
const cost = event.total_cost_usd ?? event.cost_usd;
|
|
836
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
837
|
+
agentCosts.set(event.id, cost);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (event.event === "policy.resolve" &&
|
|
841
|
+
typeof event.id === "string" &&
|
|
842
|
+
(event.verdict === "allow" || event.verdict === "deny") &&
|
|
843
|
+
typeof event.resolved_by === "string" &&
|
|
844
|
+
event.request &&
|
|
845
|
+
typeof event.request === "object") {
|
|
846
|
+
decisions.set(event.id, event);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
agentsSpawned: agentIds.size,
|
|
851
|
+
costUsd: round4(Array.from(agentCosts.values()).reduce((sum, value) => sum + value, 0)),
|
|
852
|
+
policyDecisions: Array.from(decisions.values()),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
679
855
|
function round4(n) {
|
|
680
856
|
return Math.round(n * 10_000) / 10_000;
|
|
681
857
|
}
|
|
@@ -684,6 +860,11 @@ function assertAgentCapacity(agentsSpawned, maxAgents) {
|
|
|
684
860
|
throw new Error(`workflow agent cap reached (${maxAgents}); raise --max-agents deliberately if the fan-out is intended`);
|
|
685
861
|
}
|
|
686
862
|
}
|
|
863
|
+
function assertPositiveWorkflowBound(value, field) {
|
|
864
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
865
|
+
throw new Error(`${field} must be a positive safe integer`);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
687
868
|
function boundedPolicyReason(value) {
|
|
688
869
|
const normalized = Array.from(value, (character) => {
|
|
689
870
|
const code = character.charCodeAt(0);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { CreateWorkflowApprovalInput, ResolveWorkflowApprovalInput, WorkflowApproval, WorkflowApprovalDecision, WorkflowApprovalRequest, WorkflowApprovalStatus, } from "./approvals.js";
|
|
2
|
+
export { assertWorkflowRunId, createWorkflowApproval, listWorkflowApprovals, readWorkflowApproval, renderWorkflowApproval, resolveWorkflowApproval, WORKFLOW_APPROVAL_SCHEMA_VERSION, workflowApprovalId, } from "./approvals.js";
|
|
3
|
+
export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js";
|
|
2
4
|
export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
|
|
3
|
-
export type {
|
|
5
|
+
export type { WorkflowRunManifest } from "./run-state.js";
|
|
6
|
+
export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, readWorkflowRunManifest, WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION, workflowRunManifestPath, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
|
|
7
|
+
export type { AcceptanceCriterion, AcceptanceResult, AcceptanceStatus, AcceptanceSummary, AgentOpts, EngineOpts, EvidenceKind, EvidenceSource, EvidenceStatus, HarnessEvidenceCapability, HarnessEvidenceCoverage, HarnessName, ResultDigest, RunReport, Spawner, SpawnRequest, SpawnResult, StageSchema, WorkflowAgentProof, WorkflowApprovalMode, WorkflowContext, WorkflowEvidenceInput, WorkflowEvidenceRecord, WorkflowMeta, WorkflowModule, WorkflowPolicyProof, WorkflowProof, WorkflowProofUnknown, WorkflowRepoEvidence, WorkflowRepoSnapshot, WorkflowRunStatus, } from "./types.js";
|
|
4
8
|
export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
|
|
5
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,2BAA2B,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,EACrB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,gCAAgC,EAChC,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,YAAY,EACZ,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EACL,0BAA0B,EAC1B,0BAA0B,EAC1B,6BAA6B,EAC7B,uBAAuB,EACvB,oCAAoC,EACpC,uBAAuB,EACvB,oBAAoB,EACpB,wBAAwB,GACzB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,SAAS,EACT,UAAU,EACV,YAAY,EACZ,cAAc,EACd,cAAc,EACd,yBAAyB,EACzB,uBAAuB,EACvB,WAAW,EACX,YAAY,EACZ,SAAS,EACT,OAAO,EACP,YAAY,EACZ,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { assertWorkflowRunId, createWorkflowApproval, listWorkflowApprovals, readWorkflowApproval, renderWorkflowApproval, resolveWorkflowApproval, WORKFLOW_APPROVAL_SCHEMA_VERSION, workflowApprovalId, } from "./approvals.js";
|
|
2
|
+
export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.js";
|
|
2
3
|
export { buildWorkflowProof, createEvidenceRecord, digestResult, normalizeWorkflowMeta, readWorkflowProof, renderWorkflowProof, rollupAcceptance, writeWorkflowProof, } from "./proof.js";
|
|
4
|
+
export { acquireWorkflowResumeLease, assertWorkflowRunResumable, assertWorkflowScriptUnchanged, readWorkflowRunManifest, WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION, workflowRunManifestPath, workflowScriptDigest, writeWorkflowRunManifest, } from "./run-state.js";
|
|
3
5
|
export { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { RepoSnapshot } from "../context/index.js";
|
|
2
|
+
import { type NormalizedPolicy, type PolicyIsolation, type PolicyNetworkAccess } from "../policy/index.js";
|
|
3
|
+
export declare const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION: 1;
|
|
4
|
+
export interface WorkflowRunManifest {
|
|
5
|
+
schema_version: typeof WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION;
|
|
6
|
+
run_id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
started_at: string;
|
|
9
|
+
script: {
|
|
10
|
+
path: string;
|
|
11
|
+
sha256: string;
|
|
12
|
+
};
|
|
13
|
+
repository_before: RepoSnapshot;
|
|
14
|
+
execution: {
|
|
15
|
+
cwd: string;
|
|
16
|
+
default_harness: string;
|
|
17
|
+
max_agents: number;
|
|
18
|
+
concurrency: number;
|
|
19
|
+
subscription_only: boolean;
|
|
20
|
+
allow_api_billing: boolean;
|
|
21
|
+
approval_mode: "deny" | "park";
|
|
22
|
+
approval_addressee: string;
|
|
23
|
+
isolation: PolicyIsolation;
|
|
24
|
+
network_access: PolicyNetworkAccess;
|
|
25
|
+
policy?: NormalizedPolicy;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface WriteWorkflowRunManifestInput {
|
|
29
|
+
coordRoot: string;
|
|
30
|
+
manifest: WorkflowRunManifest;
|
|
31
|
+
}
|
|
32
|
+
export declare function workflowScriptDigest(scriptPath: string): string;
|
|
33
|
+
export declare function writeWorkflowRunManifest(input: WriteWorkflowRunManifestInput): string;
|
|
34
|
+
export declare function readWorkflowRunManifest(coordRoot: string, runId: string): WorkflowRunManifest;
|
|
35
|
+
export declare function assertWorkflowRunResumable(coordRoot: string, runId: string): {
|
|
36
|
+
manifest: WorkflowRunManifest;
|
|
37
|
+
approvalId: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function assertWorkflowScriptUnchanged(manifest: WorkflowRunManifest): void;
|
|
40
|
+
export declare function workflowRunManifestPath(coordRoot: string, runId: string): string;
|
|
41
|
+
/** Acquire an exclusive, crash-recoverable lease for one resume segment. */
|
|
42
|
+
export declare function acquireWorkflowResumeLease(coordRoot: string, runId: string): () => void;
|
|
43
|
+
//# sourceMappingURL=run-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-state.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/run-state.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAEzB,MAAM,oBAAoB,CAAC;AAG5B,eAAO,MAAM,oCAAoC,EAAG,CAAU,CAAC;AAK/D,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,OAAO,oCAAoC,CAAC;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,iBAAiB,EAAE,YAAY,CAAC;IAChC,SAAS,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,iBAAiB,EAAE,OAAO,CAAC;QAC3B,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,kBAAkB,EAAE,MAAM,CAAC;QAC3B,SAAS,EAAE,eAAe,CAAC;QAC3B,cAAc,EAAE,mBAAmB,CAAC;QACpC,MAAM,CAAC,EAAE,gBAAgB,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,GAAG,MAAM,CAmBrF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CA8B7F;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ;IAAE,QAAQ,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CA4BvD;AAED,wBAAgB,6BAA6B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CASjF;AAED,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,4EAA4E;AAC5E,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,IAAI,CA0CvF"}
|