harnery 0.12.2 → 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/policy/types.d.ts +1 -1
- package/dist/core/policy/types.d.ts.map +1 -1
- 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/policy/types.ts +1 -1
- 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
|
@@ -22,7 +22,12 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "n
|
|
|
22
22
|
import { isAbsolute, join, resolve } from "node:path";
|
|
23
23
|
import { pathToFileURL } from "node:url";
|
|
24
24
|
import { snapshotRepo } from "../context/index.ts";
|
|
25
|
-
import type {
|
|
25
|
+
import type {
|
|
26
|
+
ExternalMutationRequest,
|
|
27
|
+
NormalizedPolicy,
|
|
28
|
+
PolicyDecision,
|
|
29
|
+
PolicyRequest,
|
|
30
|
+
} from "../policy/index.ts";
|
|
26
31
|
import {
|
|
27
32
|
evaluatePolicy,
|
|
28
33
|
normalizePolicy,
|
|
@@ -30,6 +35,7 @@ import {
|
|
|
30
35
|
policyDigest,
|
|
31
36
|
summarizePolicyRequest,
|
|
32
37
|
} from "../policy/index.ts";
|
|
38
|
+
import { createWorkflowApproval } from "./approvals.ts";
|
|
33
39
|
import { type BillingProbe, probeBilling } from "./billing.ts";
|
|
34
40
|
import {
|
|
35
41
|
buildWorkflowProof,
|
|
@@ -38,6 +44,13 @@ import {
|
|
|
38
44
|
normalizeWorkflowMeta,
|
|
39
45
|
writeWorkflowProof,
|
|
40
46
|
} from "./proof.ts";
|
|
47
|
+
import {
|
|
48
|
+
acquireWorkflowResumeLease,
|
|
49
|
+
assertWorkflowRunResumable,
|
|
50
|
+
assertWorkflowScriptUnchanged,
|
|
51
|
+
workflowScriptDigest,
|
|
52
|
+
writeWorkflowRunManifest,
|
|
53
|
+
} from "./run-state.ts";
|
|
41
54
|
import type {
|
|
42
55
|
AgentOpts,
|
|
43
56
|
EngineOpts,
|
|
@@ -73,8 +86,55 @@ export class WorkflowRunError extends Error {
|
|
|
73
86
|
}
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
export class WorkflowParkedError extends Error {
|
|
90
|
+
readonly runId: string;
|
|
91
|
+
readonly approvalId: string;
|
|
92
|
+
readonly journalPath: string;
|
|
93
|
+
|
|
94
|
+
constructor(message: string, runId: string, approvalId: string, journalPath: string) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "WorkflowParkedError";
|
|
97
|
+
this.runId = runId;
|
|
98
|
+
this.approvalId = approvalId;
|
|
99
|
+
this.journalPath = journalPath;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport> {
|
|
104
|
+
if (opts.resumeRunId && opts.resumeFrom) {
|
|
105
|
+
throw new Error("resumeRunId and resumeFrom are mutually exclusive");
|
|
106
|
+
}
|
|
107
|
+
const resumeState = opts.resumeRunId
|
|
108
|
+
? assertWorkflowRunResumable(opts.coordRoot, opts.resumeRunId)
|
|
109
|
+
: undefined;
|
|
77
110
|
const absScript = isAbsolute(scriptPath) ? scriptPath : resolve(process.cwd(), scriptPath);
|
|
111
|
+
if (resumeState) {
|
|
112
|
+
if (resolve(resumeState.manifest.script.path) !== resolve(absScript)) {
|
|
113
|
+
throw new Error(`workflow run ${opts.resumeRunId} belongs to a different script`);
|
|
114
|
+
}
|
|
115
|
+
assertWorkflowScriptUnchanged(resumeState.manifest);
|
|
116
|
+
}
|
|
117
|
+
const releaseResumeLease = opts.resumeRunId
|
|
118
|
+
? acquireWorkflowResumeLease(opts.coordRoot, opts.resumeRunId)
|
|
119
|
+
: undefined;
|
|
120
|
+
try {
|
|
121
|
+
if (resumeState) {
|
|
122
|
+
// Recheck under the exclusive lease. Another process may have completed
|
|
123
|
+
// the run between the optimistic read and lease acquisition.
|
|
124
|
+
assertWorkflowRunResumable(opts.coordRoot, resumeState.manifest.run_id);
|
|
125
|
+
}
|
|
126
|
+
return await executeWorkflow(scriptPath, absScript, opts, resumeState);
|
|
127
|
+
} finally {
|
|
128
|
+
releaseResumeLease?.();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function executeWorkflow(
|
|
133
|
+
scriptPath: string,
|
|
134
|
+
absScript: string,
|
|
135
|
+
opts: EngineOpts,
|
|
136
|
+
resumeState: ReturnType<typeof assertWorkflowRunResumable> | undefined,
|
|
137
|
+
): Promise<RunReport> {
|
|
78
138
|
const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
|
|
79
139
|
if (typeof mod.default !== "function") {
|
|
80
140
|
throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
|
|
@@ -83,21 +143,61 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
83
143
|
const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
|
|
84
144
|
const name = meta.name;
|
|
85
145
|
|
|
86
|
-
const runId =
|
|
146
|
+
const runId =
|
|
147
|
+
opts.resumeRunId ??
|
|
148
|
+
`wf-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomBytes(3).toString("hex")}`;
|
|
87
149
|
const runDir = join(opts.coordRoot, ".harnery", "workflows", runId);
|
|
88
150
|
mkdirSync(runDir, { recursive: true });
|
|
89
151
|
const journalPath = join(runDir, "journal.jsonl");
|
|
90
152
|
const proofPath = join(runDir, "proof.json");
|
|
91
153
|
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
154
|
+
const frozen = resumeState?.manifest.execution;
|
|
155
|
+
const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
|
|
156
|
+
const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
|
|
157
|
+
assertPositiveWorkflowBound(maxAgents, "maxAgents");
|
|
158
|
+
assertPositiveWorkflowBound(concurrency, "concurrency");
|
|
159
|
+
const cwd = frozen?.cwd ?? opts.cwd ?? opts.coordRoot;
|
|
95
160
|
const log = opts.onLog ?? ((line: string) => process.stderr.write(`${line}\n`));
|
|
96
|
-
const defaultHarness: HarnessName =
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
const
|
|
161
|
+
const defaultHarness: HarnessName =
|
|
162
|
+
frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
|
|
163
|
+
const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
|
|
164
|
+
const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
|
|
165
|
+
const policy =
|
|
166
|
+
frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
|
|
167
|
+
const approvalMode = frozen?.approval_mode ?? opts.approvalMode ?? "deny";
|
|
168
|
+
const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
|
|
169
|
+
const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
|
|
170
|
+
const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
|
|
171
|
+
const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
|
|
172
|
+
const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
|
|
173
|
+
const t0 = resumeState ? Date.parse(startedAt) : Date.now();
|
|
174
|
+
|
|
175
|
+
if (!resumeState) {
|
|
176
|
+
writeWorkflowRunManifest({
|
|
177
|
+
coordRoot: opts.coordRoot,
|
|
178
|
+
manifest: {
|
|
179
|
+
schema_version: 1,
|
|
180
|
+
run_id: runId,
|
|
181
|
+
name,
|
|
182
|
+
started_at: startedAt,
|
|
183
|
+
script: { path: absScript, sha256: workflowScriptDigest(absScript) },
|
|
184
|
+
repository_before: repoBefore,
|
|
185
|
+
execution: {
|
|
186
|
+
cwd,
|
|
187
|
+
default_harness: defaultHarness,
|
|
188
|
+
max_agents: maxAgents,
|
|
189
|
+
concurrency,
|
|
190
|
+
subscription_only: subscriptionOnly,
|
|
191
|
+
allow_api_billing: allowApiBilling,
|
|
192
|
+
approval_mode: approvalMode,
|
|
193
|
+
approval_addressee: approvalAddressee,
|
|
194
|
+
isolation,
|
|
195
|
+
network_access: networkAccess,
|
|
196
|
+
policy: policy ? (policy as NormalizedPolicy) : undefined,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
101
201
|
|
|
102
202
|
// Per-child fixed context overhead: children spawn in `cwd` and load its
|
|
103
203
|
// repo-instructions file into their system prompt, cache-writing it once
|
|
@@ -111,13 +211,16 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
111
211
|
}
|
|
112
212
|
|
|
113
213
|
// Resume: journaled results of a prior run, keyed by agent-call identity.
|
|
114
|
-
const
|
|
115
|
-
|
|
214
|
+
const resumeSource = opts.resumeRunId ?? opts.resumeFrom;
|
|
215
|
+
const resumeCache = resumeSource
|
|
216
|
+
? loadResumeCache(opts.coordRoot, resumeSource)
|
|
116
217
|
: new Map<string, { kind: "json" | "text"; value: unknown }>();
|
|
117
218
|
|
|
118
|
-
|
|
219
|
+
const history = opts.resumeRunId ? loadRunHistory(journalPath) : undefined;
|
|
220
|
+
|
|
221
|
+
let agentsSpawned = history?.agentsSpawned ?? 0;
|
|
119
222
|
let agentsCached = 0;
|
|
120
|
-
let costUsd = 0;
|
|
223
|
+
let costUsd = history?.costUsd ?? 0;
|
|
121
224
|
let reservedCostUsd = 0;
|
|
122
225
|
let currentStage = "";
|
|
123
226
|
let agentSeq = 0;
|
|
@@ -126,7 +229,9 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
126
229
|
const billingProbed = new Map<HarnessName, BillingProbe>();
|
|
127
230
|
const agentProofs = new Map<string, WorkflowAgentProof>();
|
|
128
231
|
const evidenceRecords: WorkflowEvidenceRecord[] = [];
|
|
129
|
-
const policyDecisions
|
|
232
|
+
const policyDecisions = new Map<string, PolicyDecision>(
|
|
233
|
+
history?.policyDecisions.map((decision) => [decision.id, decision]),
|
|
234
|
+
);
|
|
130
235
|
const acceptanceIds = new Set(meta.acceptance.map((criterion) => criterion.id));
|
|
131
236
|
|
|
132
237
|
const journal = (event: string, data: Record<string, unknown>): void => {
|
|
@@ -181,7 +286,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
181
286
|
if (!policy) {
|
|
182
287
|
throw new Error("external mutation authorization requires a host policy");
|
|
183
288
|
}
|
|
184
|
-
if (policyDecisions.
|
|
289
|
+
if (policyDecisions.size >= MAX_POLICY_DECISIONS && !policyDecisions.has(`p${policySeq + 1}`)) {
|
|
185
290
|
throw new Error(
|
|
186
291
|
`workflow policy decision cap reached (${MAX_POLICY_DECISIONS}); split the workflow or reduce protected actions`,
|
|
187
292
|
);
|
|
@@ -204,9 +309,8 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
204
309
|
let reason = evaluation.reason;
|
|
205
310
|
if (evaluation.verdict === "ask") {
|
|
206
311
|
resolvedBy = "fail_closed";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} else {
|
|
312
|
+
let immediateResolution = false;
|
|
313
|
+
if (opts.resolvePolicyAsk) {
|
|
210
314
|
try {
|
|
211
315
|
const resolution = await withTimeout(
|
|
212
316
|
Promise.resolve(opts.resolvePolicyAsk(request, evaluation)),
|
|
@@ -218,10 +322,57 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
218
322
|
verdict = resolution.verdict;
|
|
219
323
|
resolvedBy = "host";
|
|
220
324
|
reason = boundedPolicyReason(resolution.reason ?? evaluation.reason);
|
|
325
|
+
immediateResolution = true;
|
|
221
326
|
}
|
|
222
327
|
} catch (error) {
|
|
223
328
|
reason = `${evaluation.reason}; approval failed closed: ${(error as Error).message}`;
|
|
224
329
|
}
|
|
330
|
+
} else {
|
|
331
|
+
reason = `${evaluation.reason}; no host approval resolver is configured`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!immediateResolution && approvalMode === "park") {
|
|
335
|
+
const stored = createWorkflowApproval({
|
|
336
|
+
coordRoot: opts.coordRoot,
|
|
337
|
+
runId,
|
|
338
|
+
decisionId: id,
|
|
339
|
+
addressedTo: approvalAddressee,
|
|
340
|
+
policy: { name: policy.name, sha256: policyDigest(policy) },
|
|
341
|
+
request,
|
|
342
|
+
evaluation,
|
|
343
|
+
});
|
|
344
|
+
if (stored.approval.status === "pending") {
|
|
345
|
+
if (stored.created) {
|
|
346
|
+
journal("approval.requested", {
|
|
347
|
+
approval_id: stored.approval.request.id,
|
|
348
|
+
decision_id: id,
|
|
349
|
+
addressed_to: stored.approval.request.addressed_to,
|
|
350
|
+
request_sha256: stored.approval.request.request_sha256,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
journal("run.parked", {
|
|
354
|
+
approval_id: stored.approval.request.id,
|
|
355
|
+
decision_id: id,
|
|
356
|
+
phase: request.phase,
|
|
357
|
+
action: request.action,
|
|
358
|
+
});
|
|
359
|
+
throw new WorkflowParkedError(
|
|
360
|
+
`workflow parked for approval ${stored.approval.request.id}: ${evaluation.reason}`,
|
|
361
|
+
runId,
|
|
362
|
+
stored.approval.request.id,
|
|
363
|
+
journalPath,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
verdict = stored.approval.decision!.verdict;
|
|
367
|
+
resolvedBy = "approval";
|
|
368
|
+
reason = boundedPolicyReason(stored.approval.decision!.reason ?? evaluation.reason);
|
|
369
|
+
journal("approval.consumed", {
|
|
370
|
+
approval_id: stored.approval.request.id,
|
|
371
|
+
decision_id: id,
|
|
372
|
+
verdict,
|
|
373
|
+
actor: stored.approval.decision!.actor,
|
|
374
|
+
decided_at: stored.approval.decision!.decided_at,
|
|
375
|
+
});
|
|
225
376
|
}
|
|
226
377
|
}
|
|
227
378
|
|
|
@@ -237,7 +388,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
237
388
|
rule_codes: evaluation.rules.map((rule) => rule.code),
|
|
238
389
|
request,
|
|
239
390
|
};
|
|
240
|
-
policyDecisions.
|
|
391
|
+
policyDecisions.set(decision.id, decision);
|
|
241
392
|
journal("policy.resolve", { ...decision });
|
|
242
393
|
if (decision.verdict === "deny") {
|
|
243
394
|
throw new PolicyDeniedError(
|
|
@@ -279,6 +430,10 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
279
430
|
const key = agentCallKey(currentStage, harness, agentOpts, prompt);
|
|
280
431
|
const cached = resumeCache.get(key);
|
|
281
432
|
if (cached) {
|
|
433
|
+
// Exact-run replay skips dispatch authorization because no dispatch
|
|
434
|
+
// occurs, but it must still reserve the original policy slot so a later
|
|
435
|
+
// durable ASK resolves against the same pN identity.
|
|
436
|
+
if (policy && opts.resumeRunId) policySeq++;
|
|
282
437
|
agentsCached++;
|
|
283
438
|
agentProof.status = "cached";
|
|
284
439
|
agentProof.result = digestResult(cached.value, cached.kind);
|
|
@@ -291,7 +446,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
291
446
|
kind: cached.kind,
|
|
292
447
|
});
|
|
293
448
|
log(
|
|
294
|
-
`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${
|
|
449
|
+
`[${name}] ${currentStage || "(no stage)"} → ${id} ${label} (cached from ${resumeSource})`,
|
|
295
450
|
);
|
|
296
451
|
return cached.value;
|
|
297
452
|
}
|
|
@@ -370,12 +525,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
370
525
|
billingProbed.set(harness, probe);
|
|
371
526
|
journal("billing.probe", {
|
|
372
527
|
harness,
|
|
373
|
-
mode:
|
|
528
|
+
mode: subscriptionOnly ? "subscription" : probe.mode,
|
|
374
529
|
api_key_source: probe.apiKeySource,
|
|
375
530
|
login: probe.login,
|
|
376
|
-
subscription_only:
|
|
531
|
+
subscription_only: subscriptionOnly,
|
|
377
532
|
});
|
|
378
|
-
if (
|
|
533
|
+
if (subscriptionOnly) {
|
|
379
534
|
if (probe.login === "absent") {
|
|
380
535
|
throw new Error(
|
|
381
536
|
`subscription-only: no stored login detected for ${harness}; ` +
|
|
@@ -383,7 +538,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
383
538
|
);
|
|
384
539
|
}
|
|
385
540
|
log(`[billing] ${harness}: subscription-only (API-key vars scrubbed from child env)`);
|
|
386
|
-
} else if (probe.mode === "api-key-override" && !
|
|
541
|
+
} else if (probe.mode === "api-key-override" && !allowApiBilling) {
|
|
387
542
|
throw new Error(
|
|
388
543
|
`${probe.apiKeySource} is set AND a stored ${harness} login exists — the key silently ` +
|
|
389
544
|
`overrides your subscription auth, so children would bill per-token API rates. ` +
|
|
@@ -440,7 +595,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
440
595
|
maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
|
|
441
596
|
cwd,
|
|
442
597
|
runId,
|
|
443
|
-
subscriptionOnly
|
|
598
|
+
subscriptionOnly,
|
|
444
599
|
});
|
|
445
600
|
agentProof.attempts = attempt;
|
|
446
601
|
agentProof.duration_ms += last.durationMs;
|
|
@@ -462,6 +617,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
462
617
|
key,
|
|
463
618
|
attempts: attempt,
|
|
464
619
|
cost_usd: last.costUsd,
|
|
620
|
+
total_cost_usd: agentCostUsd,
|
|
465
621
|
duration_ms: last.durationMs,
|
|
466
622
|
session_id: last.sessionId,
|
|
467
623
|
result_kind: "text",
|
|
@@ -486,6 +642,7 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
486
642
|
key,
|
|
487
643
|
attempts: attempt,
|
|
488
644
|
cost_usd: last.costUsd,
|
|
645
|
+
total_cost_usd: agentCostUsd,
|
|
489
646
|
duration_ms: last.durationMs,
|
|
490
647
|
session_id: last.sessionId,
|
|
491
648
|
result_kind: "json",
|
|
@@ -572,21 +729,26 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
572
729
|
};
|
|
573
730
|
|
|
574
731
|
const ctx: WorkflowContext = { agent, parallel, stage, log, evidence, authorize };
|
|
575
|
-
|
|
576
|
-
const t0 = Date.now();
|
|
577
|
-
const startedAt = new Date(t0).toISOString();
|
|
578
|
-
journal("run.start", {
|
|
579
|
-
name,
|
|
580
|
-
script: absScript,
|
|
581
|
-
objective: meta.objective ?? null,
|
|
582
|
-
acceptance: meta.acceptance,
|
|
583
|
-
max_agents: maxAgents,
|
|
584
|
-
concurrency,
|
|
585
|
-
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
586
|
-
isolation,
|
|
587
|
-
network_access: networkAccess,
|
|
588
|
-
});
|
|
589
732
|
try {
|
|
733
|
+
if (resumeState) {
|
|
734
|
+
journal("run.resume", {
|
|
735
|
+
approval_id: resumeState.approvalId,
|
|
736
|
+
script: absScript,
|
|
737
|
+
resumed_at: new Date().toISOString(),
|
|
738
|
+
});
|
|
739
|
+
} else {
|
|
740
|
+
journal("run.start", {
|
|
741
|
+
name,
|
|
742
|
+
script: absScript,
|
|
743
|
+
objective: meta.objective ?? null,
|
|
744
|
+
acceptance: meta.acceptance,
|
|
745
|
+
max_agents: maxAgents,
|
|
746
|
+
concurrency,
|
|
747
|
+
policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
|
|
748
|
+
isolation,
|
|
749
|
+
network_access: networkAccess,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
590
752
|
const result = await mod.default(ctx);
|
|
591
753
|
const endedAt = new Date().toISOString();
|
|
592
754
|
const durationMs = Date.now() - t0;
|
|
@@ -613,7 +775,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
613
775
|
evidence: evidenceRecords,
|
|
614
776
|
harnessEvidence: opts.harnessEvidence,
|
|
615
777
|
policy: policy
|
|
616
|
-
? {
|
|
778
|
+
? {
|
|
779
|
+
config: policy,
|
|
780
|
+
decisions: Array.from(policyDecisions.values()),
|
|
781
|
+
isolation,
|
|
782
|
+
networkAccess,
|
|
783
|
+
}
|
|
617
784
|
: undefined,
|
|
618
785
|
result,
|
|
619
786
|
});
|
|
@@ -640,13 +807,13 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
640
807
|
contextTokensPerChildEstimate,
|
|
641
808
|
billing: Array.from(billingProbed.values()).map((p) => ({
|
|
642
809
|
harness: p.harness,
|
|
643
|
-
mode:
|
|
810
|
+
mode: subscriptionOnly ? "subscription" : p.mode,
|
|
644
811
|
})),
|
|
645
812
|
policy: proof.policy?.summary,
|
|
646
813
|
};
|
|
647
814
|
return report;
|
|
648
815
|
} catch (err) {
|
|
649
|
-
if (err instanceof WorkflowRunError) throw err;
|
|
816
|
+
if (err instanceof WorkflowParkedError || err instanceof WorkflowRunError) throw err;
|
|
650
817
|
const endedAt = new Date().toISOString();
|
|
651
818
|
const durationMs = Date.now() - t0;
|
|
652
819
|
journal("run.end", {
|
|
@@ -672,7 +839,12 @@ export async function runWorkflow(scriptPath: string, opts: EngineOpts): Promise
|
|
|
672
839
|
evidence: evidenceRecords,
|
|
673
840
|
harnessEvidence: opts.harnessEvidence,
|
|
674
841
|
policy: policy
|
|
675
|
-
? {
|
|
842
|
+
? {
|
|
843
|
+
config: policy,
|
|
844
|
+
decisions: Array.from(policyDecisions.values()),
|
|
845
|
+
isolation,
|
|
846
|
+
networkAccess,
|
|
847
|
+
}
|
|
676
848
|
: undefined,
|
|
677
849
|
error: (err as Error).message,
|
|
678
850
|
});
|
|
@@ -758,6 +930,52 @@ function loadResumeCache(
|
|
|
758
930
|
return cache;
|
|
759
931
|
}
|
|
760
932
|
|
|
933
|
+
function loadRunHistory(path: string): {
|
|
934
|
+
agentsSpawned: number;
|
|
935
|
+
costUsd: number;
|
|
936
|
+
policyDecisions: PolicyDecision[];
|
|
937
|
+
} {
|
|
938
|
+
const agentIds = new Set<string>();
|
|
939
|
+
const agentCosts = new Map<string, number>();
|
|
940
|
+
const decisions = new Map<string, PolicyDecision>();
|
|
941
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
942
|
+
if (!line.trim()) continue;
|
|
943
|
+
const event = JSON.parse(line) as {
|
|
944
|
+
event?: string;
|
|
945
|
+
id?: string;
|
|
946
|
+
cost_usd?: number;
|
|
947
|
+
total_cost_usd?: number;
|
|
948
|
+
verdict?: unknown;
|
|
949
|
+
resolved_by?: unknown;
|
|
950
|
+
request?: unknown;
|
|
951
|
+
};
|
|
952
|
+
if (event.event === "agent.start" && typeof event.id === "string") {
|
|
953
|
+
agentIds.add(event.id);
|
|
954
|
+
}
|
|
955
|
+
if (event.event === "agent.end" && typeof event.id === "string") {
|
|
956
|
+
const cost = event.total_cost_usd ?? event.cost_usd;
|
|
957
|
+
if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
958
|
+
agentCosts.set(event.id, cost);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (
|
|
962
|
+
event.event === "policy.resolve" &&
|
|
963
|
+
typeof event.id === "string" &&
|
|
964
|
+
(event.verdict === "allow" || event.verdict === "deny") &&
|
|
965
|
+
typeof event.resolved_by === "string" &&
|
|
966
|
+
event.request &&
|
|
967
|
+
typeof event.request === "object"
|
|
968
|
+
) {
|
|
969
|
+
decisions.set(event.id, event as PolicyDecision);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
agentsSpawned: agentIds.size,
|
|
974
|
+
costUsd: round4(Array.from(agentCosts.values()).reduce((sum, value) => sum + value, 0)),
|
|
975
|
+
policyDecisions: Array.from(decisions.values()),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
761
979
|
function round4(n: number): number {
|
|
762
980
|
return Math.round(n * 10_000) / 10_000;
|
|
763
981
|
}
|
|
@@ -770,6 +988,12 @@ function assertAgentCapacity(agentsSpawned: number, maxAgents: number): void {
|
|
|
770
988
|
}
|
|
771
989
|
}
|
|
772
990
|
|
|
991
|
+
function assertPositiveWorkflowBound(value: number, field: string): void {
|
|
992
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
993
|
+
throw new Error(`${field} must be a positive safe integer`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
773
997
|
function boundedPolicyReason(value: string): string {
|
|
774
998
|
const normalized = Array.from(value, (character) => {
|
|
775
999
|
const code = character.charCodeAt(0);
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type {
|
|
2
|
+
CreateWorkflowApprovalInput,
|
|
3
|
+
ResolveWorkflowApprovalInput,
|
|
4
|
+
WorkflowApproval,
|
|
5
|
+
WorkflowApprovalDecision,
|
|
6
|
+
WorkflowApprovalRequest,
|
|
7
|
+
WorkflowApprovalStatus,
|
|
8
|
+
} from "./approvals.ts";
|
|
9
|
+
export {
|
|
10
|
+
assertWorkflowRunId,
|
|
11
|
+
createWorkflowApproval,
|
|
12
|
+
listWorkflowApprovals,
|
|
13
|
+
readWorkflowApproval,
|
|
14
|
+
renderWorkflowApproval,
|
|
15
|
+
resolveWorkflowApproval,
|
|
16
|
+
WORKFLOW_APPROVAL_SCHEMA_VERSION,
|
|
17
|
+
workflowApprovalId,
|
|
18
|
+
} from "./approvals.ts";
|
|
19
|
+
export { runWorkflow, WorkflowParkedError, WorkflowRunError } from "./engine.ts";
|
|
2
20
|
export {
|
|
3
21
|
buildWorkflowProof,
|
|
4
22
|
createEvidenceRecord,
|
|
@@ -9,6 +27,17 @@ export {
|
|
|
9
27
|
rollupAcceptance,
|
|
10
28
|
writeWorkflowProof,
|
|
11
29
|
} from "./proof.ts";
|
|
30
|
+
export type { WorkflowRunManifest } from "./run-state.ts";
|
|
31
|
+
export {
|
|
32
|
+
acquireWorkflowResumeLease,
|
|
33
|
+
assertWorkflowRunResumable,
|
|
34
|
+
assertWorkflowScriptUnchanged,
|
|
35
|
+
readWorkflowRunManifest,
|
|
36
|
+
WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION,
|
|
37
|
+
workflowRunManifestPath,
|
|
38
|
+
workflowScriptDigest,
|
|
39
|
+
writeWorkflowRunManifest,
|
|
40
|
+
} from "./run-state.ts";
|
|
12
41
|
export type {
|
|
13
42
|
AcceptanceCriterion,
|
|
14
43
|
AcceptanceResult,
|
|
@@ -29,6 +58,7 @@ export type {
|
|
|
29
58
|
SpawnResult,
|
|
30
59
|
StageSchema,
|
|
31
60
|
WorkflowAgentProof,
|
|
61
|
+
WorkflowApprovalMode,
|
|
32
62
|
WorkflowContext,
|
|
33
63
|
WorkflowEvidenceInput,
|
|
34
64
|
WorkflowEvidenceRecord,
|