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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { normalizePolicy, policyDigest, } from "../policy/index.js";
|
|
6
|
+
import { assertWorkflowRunId, readWorkflowApproval } from "./approvals.js";
|
|
7
|
+
export const WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION = 1;
|
|
8
|
+
const MANIFEST_LIMIT_BYTES = 256 * 1024;
|
|
9
|
+
const FOREIGN_LEASE_STALE_MS = 24 * 60 * 60 * 1_000;
|
|
10
|
+
export function workflowScriptDigest(scriptPath) {
|
|
11
|
+
return createHash("sha256").update(readFileSync(scriptPath)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
export function writeWorkflowRunManifest(input) {
|
|
14
|
+
assertWorkflowRunId(input.manifest.run_id);
|
|
15
|
+
const path = workflowRunManifestPath(input.coordRoot, input.manifest.run_id);
|
|
16
|
+
if (existsSync(path))
|
|
17
|
+
throw new Error(`workflow run ${input.manifest.run_id} already has a manifest`);
|
|
18
|
+
const body = `${JSON.stringify(input.manifest, null, 2)}\n`;
|
|
19
|
+
if (Buffer.byteLength(body) > MANIFEST_LIMIT_BYTES) {
|
|
20
|
+
throw new Error(`workflow run manifest exceeds ${MANIFEST_LIMIT_BYTES} bytes`);
|
|
21
|
+
}
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
26
|
+
renameSync(temporary, path);
|
|
27
|
+
chmodSync(path, 0o600);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
if (existsSync(temporary))
|
|
31
|
+
unlinkSync(temporary);
|
|
32
|
+
}
|
|
33
|
+
return path;
|
|
34
|
+
}
|
|
35
|
+
export function readWorkflowRunManifest(coordRoot, runId) {
|
|
36
|
+
assertWorkflowRunId(runId);
|
|
37
|
+
const path = workflowRunManifestPath(coordRoot, runId);
|
|
38
|
+
if (!existsSync(path))
|
|
39
|
+
throw new Error(`workflow run ${runId} has no resume manifest at ${path}`);
|
|
40
|
+
const bytes = statSync(path).size;
|
|
41
|
+
if (bytes <= 0 || bytes > MANIFEST_LIMIT_BYTES) {
|
|
42
|
+
throw new Error(`workflow run manifest at ${path} has invalid size ${bytes}`);
|
|
43
|
+
}
|
|
44
|
+
let manifest;
|
|
45
|
+
try {
|
|
46
|
+
manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`cannot parse workflow run manifest at ${path}: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
if (manifest.schema_version !== WORKFLOW_RUN_MANIFEST_SCHEMA_VERSION ||
|
|
52
|
+
manifest.run_id !== runId ||
|
|
53
|
+
typeof manifest.name !== "string" ||
|
|
54
|
+
manifest.name.length === 0 ||
|
|
55
|
+
manifest.name.length > 200 ||
|
|
56
|
+
!validTimestamp(manifest.started_at) ||
|
|
57
|
+
!manifest.script?.path ||
|
|
58
|
+
!isAbsolute(manifest.script.path) ||
|
|
59
|
+
!/^[a-f0-9]{64}$/.test(manifest.script.sha256) ||
|
|
60
|
+
!validRepoSnapshot(manifest.repository_before) ||
|
|
61
|
+
!validExecution(manifest.execution)) {
|
|
62
|
+
throw new Error(`workflow run manifest at ${path} has an unsupported or mismatched schema`);
|
|
63
|
+
}
|
|
64
|
+
return manifest;
|
|
65
|
+
}
|
|
66
|
+
export function assertWorkflowRunResumable(coordRoot, runId) {
|
|
67
|
+
const manifest = readWorkflowRunManifest(coordRoot, runId);
|
|
68
|
+
const journalPath = join(coordRoot, ".harnery", "workflows", runId, "journal.jsonl");
|
|
69
|
+
if (!existsSync(journalPath))
|
|
70
|
+
throw new Error(`workflow run ${runId} has no journal`);
|
|
71
|
+
let approvalId;
|
|
72
|
+
let terminal = false;
|
|
73
|
+
for (const line of readFileSync(journalPath, "utf8").split("\n")) {
|
|
74
|
+
if (!line.trim())
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const event = JSON.parse(line);
|
|
78
|
+
if (event.event === "run.parked" && typeof event.approval_id === "string") {
|
|
79
|
+
approvalId = event.approval_id;
|
|
80
|
+
}
|
|
81
|
+
if (event.event === "run.end")
|
|
82
|
+
terminal = true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error(`workflow run ${runId} has a malformed journal line`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (terminal)
|
|
89
|
+
throw new Error(`workflow run ${runId} is already terminal`);
|
|
90
|
+
if (!approvalId)
|
|
91
|
+
throw new Error(`workflow run ${runId} is not parked on an approval`);
|
|
92
|
+
const approval = readWorkflowApproval(coordRoot, approvalId);
|
|
93
|
+
if (approval.request.run_id !== runId) {
|
|
94
|
+
throw new Error(`workflow run ${runId} has a mismatched parked approval`);
|
|
95
|
+
}
|
|
96
|
+
if (approval.status === "pending") {
|
|
97
|
+
throw new Error(`workflow run ${runId} is still waiting on approval ${approvalId}`);
|
|
98
|
+
}
|
|
99
|
+
return { manifest, approvalId };
|
|
100
|
+
}
|
|
101
|
+
export function assertWorkflowScriptUnchanged(manifest) {
|
|
102
|
+
const path = resolve(manifest.script.path);
|
|
103
|
+
if (!existsSync(path))
|
|
104
|
+
throw new Error(`workflow script no longer exists at ${path}`);
|
|
105
|
+
const actual = workflowScriptDigest(path);
|
|
106
|
+
if (actual !== manifest.script.sha256) {
|
|
107
|
+
throw new Error(`workflow script changed since run ${manifest.run_id} parked; start a new run instead of reusing its approval`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function workflowRunManifestPath(coordRoot, runId) {
|
|
111
|
+
assertWorkflowRunId(runId);
|
|
112
|
+
return join(coordRoot, ".harnery", "workflows", runId, "run.json");
|
|
113
|
+
}
|
|
114
|
+
/** Acquire an exclusive, crash-recoverable lease for one resume segment. */
|
|
115
|
+
export function acquireWorkflowResumeLease(coordRoot, runId) {
|
|
116
|
+
assertWorkflowRunId(runId);
|
|
117
|
+
const path = join(coordRoot, ".harnery", "workflows", runId, "resume.lock");
|
|
118
|
+
const owner = { pid: process.pid, host: hostname(), created_at: new Date().toISOString() };
|
|
119
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
120
|
+
const acquire = () => {
|
|
121
|
+
try {
|
|
122
|
+
const fd = openSync(path, "wx", 0o600);
|
|
123
|
+
try {
|
|
124
|
+
writeFileSync(fd, `${JSON.stringify(owner)}\n`, "utf8");
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
chmodSync(path, 0o600);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error.code !== "EEXIST")
|
|
134
|
+
throw error;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
if (!acquire()) {
|
|
139
|
+
const existing = readResumeLease(path);
|
|
140
|
+
if (existing && resumeLeaseIsLive(existing)) {
|
|
141
|
+
throw new Error(`workflow run ${runId} is already being resumed by pid ${existing.pid} on ${existing.host}`);
|
|
142
|
+
}
|
|
143
|
+
unlinkSync(path);
|
|
144
|
+
if (!acquire())
|
|
145
|
+
throw new Error(`workflow run ${runId} resume lease raced with another process`);
|
|
146
|
+
}
|
|
147
|
+
return () => {
|
|
148
|
+
try {
|
|
149
|
+
const existing = readResumeLease(path);
|
|
150
|
+
if (existing?.pid === owner.pid && existing.host === owner.host)
|
|
151
|
+
unlinkSync(path);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Best effort. A stale lease is recoverable on the next resume attempt.
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function readResumeLease(path) {
|
|
159
|
+
try {
|
|
160
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
161
|
+
return typeof value.pid === "number" &&
|
|
162
|
+
Number.isSafeInteger(value.pid) &&
|
|
163
|
+
value.pid > 0 &&
|
|
164
|
+
typeof value.host === "string" &&
|
|
165
|
+
typeof value.created_at === "string"
|
|
166
|
+
? { pid: value.pid, host: value.host, created_at: value.created_at }
|
|
167
|
+
: null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function resumeLeaseIsLive(lease) {
|
|
174
|
+
if (lease.host !== hostname()) {
|
|
175
|
+
const age = Date.now() - Date.parse(lease.created_at);
|
|
176
|
+
return Number.isFinite(age) && age < FOREIGN_LEASE_STALE_MS;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
process.kill(lease.pid, 0);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return error.code === "EPERM";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validExecution(value) {
|
|
187
|
+
if (!value || typeof value !== "object")
|
|
188
|
+
return false;
|
|
189
|
+
return (isAbsolute(value.cwd) &&
|
|
190
|
+
typeof value.default_harness === "string" &&
|
|
191
|
+
value.default_harness.length > 0 &&
|
|
192
|
+
positiveSafeInteger(value.max_agents) &&
|
|
193
|
+
positiveSafeInteger(value.concurrency) &&
|
|
194
|
+
typeof value.subscription_only === "boolean" &&
|
|
195
|
+
typeof value.allow_api_billing === "boolean" &&
|
|
196
|
+
(value.approval_mode === "deny" || value.approval_mode === "park") &&
|
|
197
|
+
typeof value.approval_addressee === "string" &&
|
|
198
|
+
value.approval_addressee.length > 0 &&
|
|
199
|
+
value.approval_addressee.length <= 200 &&
|
|
200
|
+
["shared", "worktree", "sandbox", "remote"].includes(value.isolation) &&
|
|
201
|
+
["enabled", "disabled", "unknown"].includes(value.network_access) &&
|
|
202
|
+
validFrozenPolicy(value.policy, value.cwd));
|
|
203
|
+
}
|
|
204
|
+
function validFrozenPolicy(policy, cwd) {
|
|
205
|
+
if (policy === undefined)
|
|
206
|
+
return true;
|
|
207
|
+
try {
|
|
208
|
+
const normalized = normalizePolicy(policy, { baseDir: cwd });
|
|
209
|
+
return policyDigest(normalized) === policyDigest(policy);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function validRepoSnapshot(value) {
|
|
216
|
+
return (Boolean(value) &&
|
|
217
|
+
typeof value === "object" &&
|
|
218
|
+
isAbsolute(value.cwd) &&
|
|
219
|
+
(value.root === undefined || isAbsolute(value.root)) &&
|
|
220
|
+
(value.branch === undefined || typeof value.branch === "string") &&
|
|
221
|
+
(value.head === undefined || typeof value.head === "string") &&
|
|
222
|
+
Array.isArray(value.dirty_paths) &&
|
|
223
|
+
value.dirty_paths.every((path) => typeof path === "string") &&
|
|
224
|
+
(value.dirty_paths_truncated === undefined || typeof value.dirty_paths_truncated === "boolean"));
|
|
225
|
+
}
|
|
226
|
+
function positiveSafeInteger(value) {
|
|
227
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
228
|
+
}
|
|
229
|
+
function validTimestamp(value) {
|
|
230
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
|
|
231
|
+
}
|
|
@@ -14,6 +14,7 @@ export type EvidenceStatus = "passed" | "failed" | "observed" | "unknown";
|
|
|
14
14
|
export type EvidenceSource = "workflow" | "engine";
|
|
15
15
|
export type AcceptanceStatus = "satisfied" | "unsatisfied" | "unknown";
|
|
16
16
|
export type WorkflowRunStatus = "succeeded" | "failed";
|
|
17
|
+
export type WorkflowApprovalMode = "deny" | "park";
|
|
17
18
|
export interface AcceptanceCriterion {
|
|
18
19
|
/** Stable identifier referenced by evidence, for example `tests-pass`. */
|
|
19
20
|
id: string;
|
|
@@ -269,6 +270,10 @@ export interface EngineOpts {
|
|
|
269
270
|
* agent() calls whose (stage, prompt, model, maxTurns, schema) key matches a
|
|
270
271
|
* completed prior agent return the journaled result without spawning. */
|
|
271
272
|
resumeFrom?: string;
|
|
273
|
+
/** Continue a parked run in its original directory after its durable
|
|
274
|
+
* approval has been resolved. The frozen run manifest supplies execution
|
|
275
|
+
* options and the original repository-before snapshot. */
|
|
276
|
+
resumeRunId?: string;
|
|
272
277
|
/** Total-agent ceiling for the run (default 50): the runaway backstop. */
|
|
273
278
|
maxAgents?: number;
|
|
274
279
|
/** Concurrent-subagent cap for parallel() (default 4). */
|
|
@@ -299,6 +304,10 @@ export interface EngineOpts {
|
|
|
299
304
|
estimateDispatchCost?: DispatchCostEstimator;
|
|
300
305
|
/** Maximum wait for an ASK resolver (default 60 seconds). */
|
|
301
306
|
policyAskTimeoutMs?: number;
|
|
307
|
+
/** Missing or unavailable ASK resolver behavior (library default: deny). */
|
|
308
|
+
approvalMode?: WorkflowApprovalMode;
|
|
309
|
+
/** Bounded address recorded on newly parked approval requests. */
|
|
310
|
+
approvalAddressee?: string;
|
|
302
311
|
/** Execution boundary created by the host (default shared). */
|
|
303
312
|
isolation?: PolicyIsolation;
|
|
304
313
|
/** Network state of spawned harness subprocesses (default unknown). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,UAAU,EACX,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE/D,eAAO,MAAM,6BAA6B,EAAG,CAAU,CAAC;AAExD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,aAAa,CAAC;AACjG,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAC1E,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,QAAQ,CAAC;AACnD,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,aAAa,GAAG,SAAS,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/workflow.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { createBuiltinHarnessRegistry } from "../core/harnesses/index.ts";
|
|
|
5
5
|
import { findCoordRoot } from "../core/hooks/resolve/coord-root.ts";
|
|
6
6
|
import type { PolicyIsolation } from "../core/policy/index.ts";
|
|
7
7
|
import { loadPolicyFile } from "../core/policy/index.ts";
|
|
8
|
+
import type { WorkflowApprovalStatus } from "../core/workflow/approvals.ts";
|
|
9
|
+
import { WorkflowParkedError } from "../core/workflow/engine.ts";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* `workflow run <script>`: execute a workflow script — bounded, schema-gated,
|
|
@@ -26,6 +28,7 @@ interface WorkflowRunOpts {
|
|
|
26
28
|
allowApiBilling?: boolean;
|
|
27
29
|
policy?: string;
|
|
28
30
|
isolation?: PolicyIsolation;
|
|
31
|
+
approvalTo?: string;
|
|
29
32
|
json?: boolean;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -33,6 +36,17 @@ interface WorkflowProofOpts {
|
|
|
33
36
|
json?: boolean;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
interface WorkflowApprovalListOpts {
|
|
40
|
+
status?: WorkflowApprovalStatus;
|
|
41
|
+
json?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface WorkflowApprovalDecisionOpts {
|
|
45
|
+
actor?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
json?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export function registerWorkflowCommand(program: Command, emit: EmitContext): void {
|
|
37
51
|
const registry = createBuiltinHarnessRegistry();
|
|
38
52
|
const harnesses = registry.ids();
|
|
@@ -75,6 +89,7 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
75
89
|
"--isolation <mode>",
|
|
76
90
|
"Host-created execution boundary: shared | worktree | sandbox | remote (default shared)",
|
|
77
91
|
)
|
|
92
|
+
.option("--approval-to <address>", "Address durable ASK requests (default: operator)")
|
|
78
93
|
.option("--json", "Emit the full RunReport as JSON")
|
|
79
94
|
.action(async (script: string, opts: WorkflowRunOpts) => {
|
|
80
95
|
const coordRoot = findCoordRoot();
|
|
@@ -134,6 +149,8 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
134
149
|
]),
|
|
135
150
|
),
|
|
136
151
|
policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
|
|
152
|
+
approvalMode: "park",
|
|
153
|
+
approvalAddressee: opts.approvalTo,
|
|
137
154
|
isolation: opts.isolation,
|
|
138
155
|
// CLI harness subprocesses inherit the host network. A policy that
|
|
139
156
|
// forbids network must therefore deny dispatch unless a future host
|
|
@@ -159,6 +176,24 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
159
176
|
`result: ${typeof report.result === "string" ? report.result : JSON.stringify(report.result, null, 2)}\n`,
|
|
160
177
|
);
|
|
161
178
|
} catch (err) {
|
|
179
|
+
if (err instanceof WorkflowParkedError) {
|
|
180
|
+
if (opts.json) {
|
|
181
|
+
emit.config({ format: "json" });
|
|
182
|
+
emit.data({
|
|
183
|
+
status: "parked",
|
|
184
|
+
runId: err.runId,
|
|
185
|
+
approvalId: err.approvalId,
|
|
186
|
+
journalPath: err.journalPath,
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
emit.text(
|
|
190
|
+
`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
|
|
191
|
+
`journal: ${err.journalPath}\n` +
|
|
192
|
+
`resume after resolution: harn workflow resume ${err.runId}\n`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
162
197
|
const proofPath =
|
|
163
198
|
typeof err === "object" && err !== null && "proofPath" in err
|
|
164
199
|
? String(err.proofPath)
|
|
@@ -171,6 +206,197 @@ export function registerWorkflowCommand(program: Command, emit: EmitContext): vo
|
|
|
171
206
|
}
|
|
172
207
|
});
|
|
173
208
|
|
|
209
|
+
workflow
|
|
210
|
+
.command("resume <run-id>")
|
|
211
|
+
.description("Resume a parked workflow after its durable approval has been resolved.")
|
|
212
|
+
.option("--json", "Emit the full RunReport or parked result as JSON")
|
|
213
|
+
.action(async (runId: string, opts: { json?: boolean }) => {
|
|
214
|
+
const coordRoot = findCoordRoot();
|
|
215
|
+
if (!coordRoot) {
|
|
216
|
+
emit.error({
|
|
217
|
+
code: "no_coord_root",
|
|
218
|
+
message: "no .harnery/ coordination root found; run `init` first",
|
|
219
|
+
});
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const { assertWorkflowRunResumable, runWorkflow } = await import(
|
|
224
|
+
"../core/workflow/index.ts"
|
|
225
|
+
);
|
|
226
|
+
const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
|
|
227
|
+
const report = await runWorkflow(manifest.script.path, {
|
|
228
|
+
coordRoot,
|
|
229
|
+
spawners: registry.spawners(),
|
|
230
|
+
resumeRunId: runId,
|
|
231
|
+
harnessEvidence: Object.fromEntries(
|
|
232
|
+
registry
|
|
233
|
+
.list()
|
|
234
|
+
.map((adapter) => [
|
|
235
|
+
adapter.profile.id,
|
|
236
|
+
{ toolEvidence: adapter.profile.capabilities.toolEvidence },
|
|
237
|
+
]),
|
|
238
|
+
),
|
|
239
|
+
});
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
emit.config({ format: "json" });
|
|
242
|
+
emit.data(report);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const cachedPart = report.agentsCached > 0 ? ` (+${report.agentsCached} cached)` : "";
|
|
246
|
+
emit.text(
|
|
247
|
+
`run ${report.runId} (${report.name}) finished: ${report.agentsSpawned} agent(s)${cachedPart}, ` +
|
|
248
|
+
`$${report.costUsd.toFixed(4)}, ${Math.round(report.durationMs / 1000)}s\n` +
|
|
249
|
+
`acceptance: ${report.acceptance.satisfied} satisfied, ${report.acceptance.unsatisfied} unsatisfied, ` +
|
|
250
|
+
`${report.acceptance.unknown} unknown\n` +
|
|
251
|
+
`journal: ${report.journalPath}\nproof: ${report.proofPath}\n` +
|
|
252
|
+
`result: ${typeof report.result === "string" ? report.result : JSON.stringify(report.result, null, 2)}\n`,
|
|
253
|
+
);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err instanceof WorkflowParkedError) {
|
|
256
|
+
if (opts.json) {
|
|
257
|
+
emit.config({ format: "json" });
|
|
258
|
+
emit.data({
|
|
259
|
+
status: "parked",
|
|
260
|
+
runId: err.runId,
|
|
261
|
+
approvalId: err.approvalId,
|
|
262
|
+
journalPath: err.journalPath,
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
emit.text(
|
|
266
|
+
`run ${err.runId} parked again\napproval: ${err.approvalId}\n` +
|
|
267
|
+
`journal: ${err.journalPath}\n`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const proofPath =
|
|
273
|
+
typeof err === "object" && err !== null && "proofPath" in err
|
|
274
|
+
? String(err.proofPath)
|
|
275
|
+
: undefined;
|
|
276
|
+
emit.error({
|
|
277
|
+
code: "workflow_resume_failed",
|
|
278
|
+
message: `${(err as Error).message}${proofPath ? `\nproof: ${proofPath}` : ""}`,
|
|
279
|
+
});
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const approvals = workflow
|
|
285
|
+
.command("approvals")
|
|
286
|
+
.description("Inspect and resolve durable workflow policy approvals.");
|
|
287
|
+
|
|
288
|
+
approvals
|
|
289
|
+
.command("list")
|
|
290
|
+
.description("List durable workflow approvals.")
|
|
291
|
+
.option("--status <status>", "Filter: pending | approved | denied")
|
|
292
|
+
.option("--json", "Emit approval records as JSON")
|
|
293
|
+
.action(async (opts: WorkflowApprovalListOpts) => {
|
|
294
|
+
const coordRoot = findCoordRoot();
|
|
295
|
+
if (!coordRoot) {
|
|
296
|
+
emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
if (opts.status && !(["pending", "approved", "denied"] as const).includes(opts.status)) {
|
|
300
|
+
emit.error({
|
|
301
|
+
code: "bad_approval_status",
|
|
302
|
+
message: `unknown approval status ${JSON.stringify(opts.status)}`,
|
|
303
|
+
});
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
const { listWorkflowApprovals } = await import("../core/workflow/approvals.ts");
|
|
307
|
+
const records = listWorkflowApprovals(coordRoot, { status: opts.status });
|
|
308
|
+
if (opts.json) {
|
|
309
|
+
emit.config({ format: "json" });
|
|
310
|
+
emit.data(records);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (records.length === 0) {
|
|
314
|
+
emit.text("no workflow approvals\n");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
emit.text(
|
|
318
|
+
`${records
|
|
319
|
+
.map(
|
|
320
|
+
({ request, status }) =>
|
|
321
|
+
`${request.id} ${status.padEnd(8)} ${request.run_id} ${request.request.phase} ${request.request.action} -> ${request.addressed_to}`,
|
|
322
|
+
)
|
|
323
|
+
.join("\n")}\n`,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
approvals
|
|
328
|
+
.command("show <approval-id>")
|
|
329
|
+
.description("Show one durable workflow approval.")
|
|
330
|
+
.option("--json", "Emit the approval record as JSON")
|
|
331
|
+
.action(async (approvalId: string, opts: { json?: boolean }) => {
|
|
332
|
+
const coordRoot = findCoordRoot();
|
|
333
|
+
if (!coordRoot) {
|
|
334
|
+
emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const { readWorkflowApproval, renderWorkflowApproval } = await import(
|
|
339
|
+
"../core/workflow/approvals.ts"
|
|
340
|
+
);
|
|
341
|
+
const approval = readWorkflowApproval(coordRoot, approvalId);
|
|
342
|
+
if (opts.json) {
|
|
343
|
+
emit.config({ format: "json" });
|
|
344
|
+
emit.data(approval);
|
|
345
|
+
} else {
|
|
346
|
+
emit.text(renderWorkflowApproval(approval));
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
emit.error({ code: "workflow_approval_read_failed", message: (err as Error).message });
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const registerApprovalDecision = (command: "approve" | "deny", verdict: "allow" | "deny") => {
|
|
355
|
+
approvals
|
|
356
|
+
.command(`${command} <approval-id>`)
|
|
357
|
+
.description(`${command === "approve" ? "Approve" : "Deny"} a pending workflow approval.`)
|
|
358
|
+
.option("--actor <name>", "Decision actor recorded in the receipt")
|
|
359
|
+
.option("--reason <text>", "Bounded decision reason")
|
|
360
|
+
.option("--json", "Emit the resolved approval as JSON")
|
|
361
|
+
.action(async (approvalId: string, opts: WorkflowApprovalDecisionOpts) => {
|
|
362
|
+
const coordRoot = findCoordRoot();
|
|
363
|
+
if (!coordRoot) {
|
|
364
|
+
emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
const { resolveWorkflowApproval, renderWorkflowApproval } = await import(
|
|
369
|
+
"../core/workflow/approvals.ts"
|
|
370
|
+
);
|
|
371
|
+
const resolved = resolveWorkflowApproval({
|
|
372
|
+
coordRoot,
|
|
373
|
+
approvalId,
|
|
374
|
+
verdict,
|
|
375
|
+
actor: opts.actor ?? process.env.USER ?? process.env.USERNAME ?? "operator",
|
|
376
|
+
reason: opts.reason,
|
|
377
|
+
});
|
|
378
|
+
if (opts.json) {
|
|
379
|
+
emit.config({ format: "json" });
|
|
380
|
+
emit.data(resolved);
|
|
381
|
+
} else {
|
|
382
|
+
emit.text(
|
|
383
|
+
`${renderWorkflowApproval(resolved.approval)}` +
|
|
384
|
+
`${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
|
|
385
|
+
`resume: harn workflow resume ${resolved.approval.request.run_id}\n`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
} catch (err) {
|
|
389
|
+
emit.error({
|
|
390
|
+
code: "workflow_approval_resolution_failed",
|
|
391
|
+
message: (err as Error).message,
|
|
392
|
+
});
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
};
|
|
397
|
+
registerApprovalDecision("approve", "allow");
|
|
398
|
+
registerApprovalDecision("deny", "deny");
|
|
399
|
+
|
|
174
400
|
workflow
|
|
175
401
|
.command("proof <run-id>")
|
|
176
402
|
.description("Show the bounded proof packet for a completed workflow run.")
|
package/src/core/policy/types.ts
CHANGED
|
@@ -86,7 +86,7 @@ export interface PolicyDecision {
|
|
|
86
86
|
phase: PolicyPhase;
|
|
87
87
|
initial_verdict: PolicyVerdict;
|
|
88
88
|
verdict: "allow" | "deny";
|
|
89
|
-
resolved_by: "policy" | "host" | "fail_closed";
|
|
89
|
+
resolved_by: "policy" | "host" | "approval" | "fail_closed";
|
|
90
90
|
reason: string;
|
|
91
91
|
rule_codes: string[];
|
|
92
92
|
request: PolicyRequestSummary;
|