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 CHANGED
@@ -21,7 +21,7 @@
21
21
 
22
22
  - **Multi-agent coordination:** per-agent heartbeats in `.harnery/active/`, claim-time and commit-time guards, the canonical event stream, councils, a decision docket, per-agent scratchpads, and harness adapters for Claude Code / Cursor / Codex.
23
23
  - **Cross-machine presence:** sessions on every machine that shares a repo see each other, labeled by machine. Zero-config over the repo's own git remote (presence refs), with an optional live-socket upgrade via an end-to-end-encrypted relay — the public `relay.harnery.com`, or self-host with `harn relay serve` / the bundled Cloudflare Worker (`relay/worker/`). The relay sees only ciphertext in opaquely-named rooms.
24
- - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with fail-closed `ALLOW`, `ASK`, and `DENY` decisions recorded in the run proof.
24
+ - **Coordination-aware workflows:** `harn workflow run <script>` executes bounded, schema-gated multi-subagent workflow scripts — plain JS stages fanning out to headless harness-CLI subagents that are born coordination-registered (heartbeats, events, claim guards), with deterministic code deciding the routing between stages. An optional host-owned policy gates dispatch and declared external mutations with `ALLOW`, `ASK`, and `DENY`; CLI `ASK` requests park durably for an explicit approve/deny and same-run resume, while library callers remain fail-closed by default.
25
25
  - **Standalone web UI:** `harn web up` boots a local Next.js dashboard for the coord layer, councils, per-project state, and a local coding-agent status view (Claude Code / Codex / Cursor usage, backed by `harn devtools`). Ships with the git clone, not the npm package (see [Install](#install)).
26
26
  - **Backup + sync:** `harn backup` snapshots `.harnery/` via [restic](https://restic.net/); `harn sync` keeps a curated subset live across machines via [rclone](https://rclone.org/) (Google Drive or any rclone remote).
27
27
 
@@ -1 +1 @@
1
- {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAkCnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAuKjF"}
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../../src/commands/workflow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAgDnD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CA2XjF"}
@@ -2,6 +2,7 @@ import { workflowSubscriptionOnly } from "../core/config.js";
2
2
  import { createBuiltinHarnessRegistry } from "../core/harnesses/index.js";
3
3
  import { findCoordRoot } from "../core/hooks/resolve/coord-root.js";
4
4
  import { loadPolicyFile } from "../core/policy/index.js";
5
+ import { WorkflowParkedError } from "../core/workflow/engine.js";
5
6
  export function registerWorkflowCommand(program, emit) {
6
7
  const registry = createBuiltinHarnessRegistry();
7
8
  const harnesses = registry.ids();
@@ -23,6 +24,7 @@ export function registerWorkflowCommand(program, emit) {
23
24
  "without this the engine refuses that silent-override state")
24
25
  .option("--policy <file>", "Host policy JSON/JSONC applied before dispatch and external mutation")
25
26
  .option("--isolation <mode>", "Host-created execution boundary: shared | worktree | sandbox | remote (default shared)")
27
+ .option("--approval-to <address>", "Address durable ASK requests (default: operator)")
26
28
  .option("--json", "Emit the full RunReport as JSON")
27
29
  .action(async (script, opts) => {
28
30
  const coordRoot = findCoordRoot();
@@ -77,6 +79,8 @@ export function registerWorkflowCommand(program, emit) {
77
79
  { toolEvidence: adapter.profile.capabilities.toolEvidence },
78
80
  ])),
79
81
  policy: opts.policy ? loadPolicyFile(opts.policy) : undefined,
82
+ approvalMode: "park",
83
+ approvalAddressee: opts.approvalTo,
80
84
  isolation: opts.isolation,
81
85
  // CLI harness subprocesses inherit the host network. A policy that
82
86
  // forbids network must therefore deny dispatch unless a future host
@@ -101,6 +105,23 @@ export function registerWorkflowCommand(program, emit) {
101
105
  `result: ${typeof report.result === "string" ? report.result : JSON.stringify(report.result, null, 2)}\n`);
102
106
  }
103
107
  catch (err) {
108
+ if (err instanceof WorkflowParkedError) {
109
+ if (opts.json) {
110
+ emit.config({ format: "json" });
111
+ emit.data({
112
+ status: "parked",
113
+ runId: err.runId,
114
+ approvalId: err.approvalId,
115
+ journalPath: err.journalPath,
116
+ });
117
+ }
118
+ else {
119
+ emit.text(`run ${err.runId} parked\napproval: ${err.approvalId}\n` +
120
+ `journal: ${err.journalPath}\n` +
121
+ `resume after resolution: harn workflow resume ${err.runId}\n`);
122
+ }
123
+ return;
124
+ }
104
125
  const proofPath = typeof err === "object" && err !== null && "proofPath" in err
105
126
  ? String(err.proofPath)
106
127
  : undefined;
@@ -111,6 +132,178 @@ export function registerWorkflowCommand(program, emit) {
111
132
  process.exit(1);
112
133
  }
113
134
  });
135
+ workflow
136
+ .command("resume <run-id>")
137
+ .description("Resume a parked workflow after its durable approval has been resolved.")
138
+ .option("--json", "Emit the full RunReport or parked result as JSON")
139
+ .action(async (runId, opts) => {
140
+ const coordRoot = findCoordRoot();
141
+ if (!coordRoot) {
142
+ emit.error({
143
+ code: "no_coord_root",
144
+ message: "no .harnery/ coordination root found; run `init` first",
145
+ });
146
+ process.exit(1);
147
+ }
148
+ try {
149
+ const { assertWorkflowRunResumable, runWorkflow } = await import("../core/workflow/index.js");
150
+ const { manifest } = assertWorkflowRunResumable(coordRoot, runId);
151
+ const report = await runWorkflow(manifest.script.path, {
152
+ coordRoot,
153
+ spawners: registry.spawners(),
154
+ resumeRunId: runId,
155
+ harnessEvidence: Object.fromEntries(registry
156
+ .list()
157
+ .map((adapter) => [
158
+ adapter.profile.id,
159
+ { toolEvidence: adapter.profile.capabilities.toolEvidence },
160
+ ])),
161
+ });
162
+ if (opts.json) {
163
+ emit.config({ format: "json" });
164
+ emit.data(report);
165
+ return;
166
+ }
167
+ const cachedPart = report.agentsCached > 0 ? ` (+${report.agentsCached} cached)` : "";
168
+ emit.text(`run ${report.runId} (${report.name}) finished: ${report.agentsSpawned} agent(s)${cachedPart}, ` +
169
+ `$${report.costUsd.toFixed(4)}, ${Math.round(report.durationMs / 1000)}s\n` +
170
+ `acceptance: ${report.acceptance.satisfied} satisfied, ${report.acceptance.unsatisfied} unsatisfied, ` +
171
+ `${report.acceptance.unknown} unknown\n` +
172
+ `journal: ${report.journalPath}\nproof: ${report.proofPath}\n` +
173
+ `result: ${typeof report.result === "string" ? report.result : JSON.stringify(report.result, null, 2)}\n`);
174
+ }
175
+ catch (err) {
176
+ if (err instanceof WorkflowParkedError) {
177
+ if (opts.json) {
178
+ emit.config({ format: "json" });
179
+ emit.data({
180
+ status: "parked",
181
+ runId: err.runId,
182
+ approvalId: err.approvalId,
183
+ journalPath: err.journalPath,
184
+ });
185
+ }
186
+ else {
187
+ emit.text(`run ${err.runId} parked again\napproval: ${err.approvalId}\n` +
188
+ `journal: ${err.journalPath}\n`);
189
+ }
190
+ return;
191
+ }
192
+ const proofPath = typeof err === "object" && err !== null && "proofPath" in err
193
+ ? String(err.proofPath)
194
+ : undefined;
195
+ emit.error({
196
+ code: "workflow_resume_failed",
197
+ message: `${err.message}${proofPath ? `\nproof: ${proofPath}` : ""}`,
198
+ });
199
+ process.exit(1);
200
+ }
201
+ });
202
+ const approvals = workflow
203
+ .command("approvals")
204
+ .description("Inspect and resolve durable workflow policy approvals.");
205
+ approvals
206
+ .command("list")
207
+ .description("List durable workflow approvals.")
208
+ .option("--status <status>", "Filter: pending | approved | denied")
209
+ .option("--json", "Emit approval records as JSON")
210
+ .action(async (opts) => {
211
+ const coordRoot = findCoordRoot();
212
+ if (!coordRoot) {
213
+ emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
214
+ process.exit(1);
215
+ }
216
+ if (opts.status && !["pending", "approved", "denied"].includes(opts.status)) {
217
+ emit.error({
218
+ code: "bad_approval_status",
219
+ message: `unknown approval status ${JSON.stringify(opts.status)}`,
220
+ });
221
+ process.exit(1);
222
+ }
223
+ const { listWorkflowApprovals } = await import("../core/workflow/approvals.js");
224
+ const records = listWorkflowApprovals(coordRoot, { status: opts.status });
225
+ if (opts.json) {
226
+ emit.config({ format: "json" });
227
+ emit.data(records);
228
+ return;
229
+ }
230
+ if (records.length === 0) {
231
+ emit.text("no workflow approvals\n");
232
+ return;
233
+ }
234
+ emit.text(`${records
235
+ .map(({ request, status }) => `${request.id} ${status.padEnd(8)} ${request.run_id} ${request.request.phase} ${request.request.action} -> ${request.addressed_to}`)
236
+ .join("\n")}\n`);
237
+ });
238
+ approvals
239
+ .command("show <approval-id>")
240
+ .description("Show one durable workflow approval.")
241
+ .option("--json", "Emit the approval record as JSON")
242
+ .action(async (approvalId, opts) => {
243
+ const coordRoot = findCoordRoot();
244
+ if (!coordRoot) {
245
+ emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
246
+ process.exit(1);
247
+ }
248
+ try {
249
+ const { readWorkflowApproval, renderWorkflowApproval } = await import("../core/workflow/approvals.js");
250
+ const approval = readWorkflowApproval(coordRoot, approvalId);
251
+ if (opts.json) {
252
+ emit.config({ format: "json" });
253
+ emit.data(approval);
254
+ }
255
+ else {
256
+ emit.text(renderWorkflowApproval(approval));
257
+ }
258
+ }
259
+ catch (err) {
260
+ emit.error({ code: "workflow_approval_read_failed", message: err.message });
261
+ process.exit(1);
262
+ }
263
+ });
264
+ const registerApprovalDecision = (command, verdict) => {
265
+ approvals
266
+ .command(`${command} <approval-id>`)
267
+ .description(`${command === "approve" ? "Approve" : "Deny"} a pending workflow approval.`)
268
+ .option("--actor <name>", "Decision actor recorded in the receipt")
269
+ .option("--reason <text>", "Bounded decision reason")
270
+ .option("--json", "Emit the resolved approval as JSON")
271
+ .action(async (approvalId, opts) => {
272
+ const coordRoot = findCoordRoot();
273
+ if (!coordRoot) {
274
+ emit.error({ code: "no_coord_root", message: "no .harnery/ coordination root found" });
275
+ process.exit(1);
276
+ }
277
+ try {
278
+ const { resolveWorkflowApproval, renderWorkflowApproval } = await import("../core/workflow/approvals.js");
279
+ const resolved = resolveWorkflowApproval({
280
+ coordRoot,
281
+ approvalId,
282
+ verdict,
283
+ actor: opts.actor ?? process.env.USER ?? process.env.USERNAME ?? "operator",
284
+ reason: opts.reason,
285
+ });
286
+ if (opts.json) {
287
+ emit.config({ format: "json" });
288
+ emit.data(resolved);
289
+ }
290
+ else {
291
+ emit.text(`${renderWorkflowApproval(resolved.approval)}` +
292
+ `${resolved.applied ? "decision recorded" : "decision already recorded"}\n` +
293
+ `resume: harn workflow resume ${resolved.approval.request.run_id}\n`);
294
+ }
295
+ }
296
+ catch (err) {
297
+ emit.error({
298
+ code: "workflow_approval_resolution_failed",
299
+ message: err.message,
300
+ });
301
+ process.exit(1);
302
+ }
303
+ });
304
+ };
305
+ registerApprovalDecision("approve", "allow");
306
+ registerApprovalDecision("deny", "deny");
114
307
  workflow
115
308
  .command("proof <run-id>")
116
309
  .description("Show the bounded proof packet for a completed workflow run.")
@@ -73,7 +73,7 @@ export interface PolicyDecision {
73
73
  phase: PolicyPhase;
74
74
  initial_verdict: PolicyVerdict;
75
75
  verdict: "allow" | "deny";
76
- resolved_by: "policy" | "host" | "fail_closed";
76
+ resolved_by: "policy" | "host" | "approval" | "fail_closed";
77
77
  reason: string;
78
78
  rule_codes: string[];
79
79
  request: PolicyRequestSummary;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/policy/types.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,EAAG,CAAU,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,mBAAmB,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAC3E,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAErE,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,cAAc,CAAC,EAAE,OAAO,qBAAqB,CAAC;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,4DAA4D;IAC5D,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,gBAAgB,CAAC,EAAE,aAAa,CAAC;IACjC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CAChD;AAED,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,OAAO,qBAAqB,CAAC;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;IAC9C,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,aAAa,CAAC;IAChC,iBAAiB,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;CAChD;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,eAAe,CAAC;IAC3B,cAAc,EAAE,mBAAmB,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,MAAM,oBAAoB,GAAG,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,iBAAiB,GAAG,CAC9B,OAAO,EAAE,QAAQ,CAAC,oBAAoB,CAAC,EACvC,UAAU,EAAE,QAAQ,CAAC,gBAAgB,CAAC,KACnC,OAAO,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,eAAe,EAAE,aAAa,CAAC;IAC/B,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,WAAW,EAAE,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,oBAAoB,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,OAAO,EAAE,QAAQ,CAAC,oBAAoB,CAAC,KACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/policy/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,71 @@
1
+ import type { PolicyEvaluation, PolicyRequestSummary, PolicyVerdict } from "../policy/index.js";
2
+ export declare const WORKFLOW_APPROVAL_SCHEMA_VERSION: 1;
3
+ export type WorkflowApprovalStatus = "pending" | "approved" | "denied";
4
+ export interface WorkflowApprovalRequest {
5
+ schema_version: typeof WORKFLOW_APPROVAL_SCHEMA_VERSION;
6
+ id: string;
7
+ run_id: string;
8
+ decision_id: string;
9
+ requested_at: string;
10
+ addressed_to: string;
11
+ policy: {
12
+ name: string;
13
+ sha256: string;
14
+ };
15
+ initial_verdict: Extract<PolicyVerdict, "ask">;
16
+ request: PolicyRequestSummary;
17
+ evaluation: PolicyEvaluation;
18
+ request_sha256: string;
19
+ }
20
+ export interface WorkflowApprovalDecision {
21
+ schema_version: typeof WORKFLOW_APPROVAL_SCHEMA_VERSION;
22
+ approval_id: string;
23
+ run_id: string;
24
+ verdict: "allow" | "deny";
25
+ actor: string;
26
+ reason?: string;
27
+ decided_at: string;
28
+ }
29
+ export interface WorkflowApproval {
30
+ request: WorkflowApprovalRequest;
31
+ decision?: WorkflowApprovalDecision;
32
+ status: WorkflowApprovalStatus;
33
+ }
34
+ export interface CreateWorkflowApprovalInput {
35
+ coordRoot: string;
36
+ runId: string;
37
+ decisionId: string;
38
+ addressedTo?: string;
39
+ policy: {
40
+ name: string;
41
+ sha256: string;
42
+ };
43
+ request: PolicyRequestSummary;
44
+ evaluation: PolicyEvaluation;
45
+ now?: () => string;
46
+ }
47
+ export interface ResolveWorkflowApprovalInput {
48
+ coordRoot: string;
49
+ approvalId: string;
50
+ verdict: "allow" | "deny";
51
+ actor: string;
52
+ reason?: string;
53
+ now?: () => string;
54
+ }
55
+ export declare function workflowApprovalId(runId: string, decisionId: string): string;
56
+ export declare function createWorkflowApproval(input: CreateWorkflowApprovalInput): {
57
+ approval: WorkflowApproval;
58
+ created: boolean;
59
+ };
60
+ export declare function resolveWorkflowApproval(input: ResolveWorkflowApprovalInput): {
61
+ approval: WorkflowApproval;
62
+ applied: boolean;
63
+ };
64
+ export declare function readWorkflowApproval(coordRoot: string, approvalId: string): WorkflowApproval;
65
+ export declare function listWorkflowApprovals(coordRoot: string, options?: {
66
+ status?: WorkflowApprovalStatus;
67
+ runId?: string;
68
+ }): WorkflowApproval[];
69
+ export declare function renderWorkflowApproval(approval: WorkflowApproval): string;
70
+ export declare function assertWorkflowRunId(runId: string): void;
71
+ //# sourceMappingURL=approvals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approvals.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/approvals.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,288 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { chmodSync, existsSync, linkSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ export const WORKFLOW_APPROVAL_SCHEMA_VERSION = 1;
5
+ const RECORD_LIMIT_BYTES = 128 * 1024;
6
+ const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
7
+ const APPROVAL_ID = /^apr-[a-f0-9]{20}$/;
8
+ const DECISION_ID = /^p[1-9][0-9]{0,4}$/;
9
+ const MAX_ACTOR = 200;
10
+ const MAX_REASON = 2_000;
11
+ const MAX_ADDRESSEE = 200;
12
+ export function workflowApprovalId(runId, decisionId) {
13
+ assertRunId(runId);
14
+ if (!DECISION_ID.test(decisionId)) {
15
+ throw new Error(`invalid workflow policy decision id ${JSON.stringify(decisionId)}`);
16
+ }
17
+ return `apr-${createHash("sha256").update(`${runId}\0${decisionId}`).digest("hex").slice(0, 20)}`;
18
+ }
19
+ export function createWorkflowApproval(input) {
20
+ const id = workflowApprovalId(input.runId, input.decisionId);
21
+ const addressedTo = bounded(input.addressedTo ?? "operator", "approval addressee", MAX_ADDRESSEE);
22
+ const requestSha256 = approvalRequestDigest(input.policy.sha256, input.request, input.evaluation);
23
+ const record = {
24
+ schema_version: WORKFLOW_APPROVAL_SCHEMA_VERSION,
25
+ id,
26
+ run_id: input.runId,
27
+ decision_id: input.decisionId,
28
+ requested_at: (input.now ?? (() => new Date().toISOString()))(),
29
+ addressed_to: addressedTo,
30
+ policy: {
31
+ name: bounded(input.policy.name, "approval policy name", MAX_ACTOR),
32
+ sha256: sha256(input.policy.sha256, "approval policy sha256"),
33
+ },
34
+ initial_verdict: "ask",
35
+ request: input.request,
36
+ evaluation: input.evaluation,
37
+ request_sha256: requestSha256,
38
+ };
39
+ const dir = approvalDir(input.coordRoot, id);
40
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
41
+ chmodSync(dir, 0o700);
42
+ const created = writeExclusiveJson(join(dir, "request.json"), record);
43
+ const approval = readWorkflowApproval(input.coordRoot, id);
44
+ if (approval.request.run_id !== record.run_id ||
45
+ approval.request.decision_id !== record.decision_id ||
46
+ approval.request.request_sha256 !== record.request_sha256 ||
47
+ approval.request.policy.sha256 !== record.policy.sha256) {
48
+ throw new Error(`approval ${id} already exists for a different policy request`);
49
+ }
50
+ return { approval, created };
51
+ }
52
+ export function resolveWorkflowApproval(input) {
53
+ const approval = readWorkflowApproval(input.coordRoot, input.approvalId);
54
+ const decision = {
55
+ schema_version: WORKFLOW_APPROVAL_SCHEMA_VERSION,
56
+ approval_id: approval.request.id,
57
+ run_id: approval.request.run_id,
58
+ verdict: input.verdict,
59
+ actor: bounded(input.actor, "approval actor", MAX_ACTOR),
60
+ reason: input.reason === undefined ? undefined : bounded(input.reason, "approval reason", MAX_REASON),
61
+ decided_at: (input.now ?? (() => new Date().toISOString()))(),
62
+ };
63
+ const applied = writeExclusiveJson(join(approvalDir(input.coordRoot, input.approvalId), "decision.json"), decision);
64
+ const resolved = readWorkflowApproval(input.coordRoot, input.approvalId);
65
+ if (resolved.decision?.verdict !== input.verdict) {
66
+ throw new Error(`approval ${input.approvalId} is already ${resolved.status}; conflicting ${input.verdict === "allow" ? "approval" : "denial"} refused`);
67
+ }
68
+ if (applied)
69
+ appendResolutionJournal(input.coordRoot, resolved);
70
+ return { approval: resolved, applied };
71
+ }
72
+ export function readWorkflowApproval(coordRoot, approvalId) {
73
+ assertApprovalId(approvalId);
74
+ const dir = approvalDir(coordRoot, approvalId);
75
+ const request = readJsonRecord(join(dir, "request.json"));
76
+ validateRequest(request, approvalId);
77
+ const decisionPath = join(dir, "decision.json");
78
+ const decision = existsSync(decisionPath)
79
+ ? readJsonRecord(decisionPath)
80
+ : undefined;
81
+ if (decision)
82
+ validateDecision(decision, request);
83
+ return {
84
+ request,
85
+ decision,
86
+ status: decision?.verdict === "allow" ? "approved" : decision ? "denied" : "pending",
87
+ };
88
+ }
89
+ export function listWorkflowApprovals(coordRoot, options = {}) {
90
+ if (options.runId)
91
+ assertRunId(options.runId);
92
+ const root = approvalsRoot(coordRoot);
93
+ if (!existsSync(root))
94
+ return [];
95
+ const records = [];
96
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
97
+ if (!entry.isDirectory() || !APPROVAL_ID.test(entry.name))
98
+ continue;
99
+ try {
100
+ const approval = readWorkflowApproval(coordRoot, entry.name);
101
+ if (options.status && approval.status !== options.status)
102
+ continue;
103
+ if (options.runId && approval.request.run_id !== options.runId)
104
+ continue;
105
+ records.push(approval);
106
+ }
107
+ catch {
108
+ // A malformed record fails closed when addressed directly, but one bad
109
+ // directory must not hide the rest of the operator's inbox.
110
+ }
111
+ }
112
+ return records.sort((a, b) => b.request.requested_at.localeCompare(a.request.requested_at));
113
+ }
114
+ export function renderWorkflowApproval(approval) {
115
+ const lines = [
116
+ `${approval.request.id}: ${approval.status}`,
117
+ `run: ${approval.request.run_id}`,
118
+ `addressed to: ${approval.request.addressed_to}`,
119
+ `requested: ${approval.request.requested_at}`,
120
+ `policy: ${approval.request.policy.name}`,
121
+ `phase: ${approval.request.request.phase}`,
122
+ `action: ${approval.request.request.action}`,
123
+ `reason: ${approval.request.evaluation.reason}`,
124
+ ];
125
+ if (approval.decision) {
126
+ lines.push(`decided: ${approval.decision.decided_at} by ${approval.decision.actor}`);
127
+ if (approval.decision.reason)
128
+ lines.push(`decision reason: ${approval.decision.reason}`);
129
+ }
130
+ return `${lines.join("\n")}\n`;
131
+ }
132
+ export function assertWorkflowRunId(runId) {
133
+ assertRunId(runId);
134
+ }
135
+ function appendResolutionJournal(coordRoot, approval) {
136
+ const journalPath = join(coordRoot, ".harnery", "workflows", approval.request.run_id, "journal.jsonl");
137
+ if (!existsSync(journalPath) || !approval.decision)
138
+ return;
139
+ const event = {
140
+ schema_version: 1,
141
+ run_id: approval.request.run_id,
142
+ ts: approval.decision.decided_at,
143
+ event: "approval.resolved",
144
+ stage: "",
145
+ approval_id: approval.request.id,
146
+ decision_id: approval.request.decision_id,
147
+ verdict: approval.decision.verdict,
148
+ actor: approval.decision.actor,
149
+ reason: approval.decision.reason ?? null,
150
+ };
151
+ writeFileSync(journalPath, `${JSON.stringify(event)}\n`, { encoding: "utf8", flag: "a" });
152
+ }
153
+ function approvalRequestDigest(policySha256, request, evaluation) {
154
+ return createHash("sha256")
155
+ .update(JSON.stringify({ policy_sha256: policySha256, request, evaluation }))
156
+ .digest("hex");
157
+ }
158
+ function writeExclusiveJson(path, value) {
159
+ const body = `${JSON.stringify(value, null, 2)}\n`;
160
+ if (Buffer.byteLength(body) > RECORD_LIMIT_BYTES) {
161
+ throw new Error(`approval record exceeds ${RECORD_LIMIT_BYTES} bytes`);
162
+ }
163
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
164
+ const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
165
+ try {
166
+ writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
167
+ try {
168
+ linkSync(temporary, path);
169
+ }
170
+ catch (error) {
171
+ if (error.code === "EEXIST")
172
+ return false;
173
+ throw error;
174
+ }
175
+ chmodSync(path, 0o600);
176
+ return true;
177
+ }
178
+ finally {
179
+ if (existsSync(temporary))
180
+ unlinkSync(temporary);
181
+ }
182
+ }
183
+ function readJsonRecord(path) {
184
+ if (!existsSync(path))
185
+ throw new Error(`approval record not found at ${path}`);
186
+ const bytes = statSync(path).size;
187
+ if (bytes <= 0 || bytes > RECORD_LIMIT_BYTES) {
188
+ throw new Error(`approval record at ${path} has invalid size ${bytes}`);
189
+ }
190
+ try {
191
+ return JSON.parse(readFileSync(path, "utf8"));
192
+ }
193
+ catch (error) {
194
+ throw new Error(`cannot parse approval record at ${path}: ${error.message}`);
195
+ }
196
+ }
197
+ function validateRequest(request, approvalId) {
198
+ if (request.schema_version !== WORKFLOW_APPROVAL_SCHEMA_VERSION ||
199
+ request.id !== approvalId ||
200
+ !RUN_ID.test(request.run_id) ||
201
+ !DECISION_ID.test(request.decision_id) ||
202
+ workflowApprovalId(request.run_id, request.decision_id) !== approvalId ||
203
+ !validTimestamp(request.requested_at) ||
204
+ bounded(request.addressed_to, "approval addressee", MAX_ADDRESSEE) !== request.addressed_to ||
205
+ bounded(request.policy?.name, "approval policy name", MAX_ACTOR) !== request.policy.name ||
206
+ request.initial_verdict !== "ask" ||
207
+ !/^[a-f0-9]{64}$/.test(request.request_sha256) ||
208
+ !/^[a-f0-9]{64}$/.test(request.policy?.sha256) ||
209
+ !validPolicyRequest(request.request) ||
210
+ !validPolicyEvaluation(request.evaluation) ||
211
+ approvalRequestDigest(request.policy.sha256, request.request, request.evaluation) !==
212
+ request.request_sha256) {
213
+ throw new Error(`approval ${approvalId} has an unsupported or mismatched request record`);
214
+ }
215
+ }
216
+ function validateDecision(decision, request) {
217
+ if (decision.schema_version !== WORKFLOW_APPROVAL_SCHEMA_VERSION ||
218
+ decision.approval_id !== request.id ||
219
+ decision.run_id !== request.run_id ||
220
+ (decision.verdict !== "allow" && decision.verdict !== "deny") ||
221
+ bounded(decision.actor, "approval actor", MAX_ACTOR) !== decision.actor ||
222
+ (decision.reason !== undefined &&
223
+ bounded(decision.reason, "approval reason", MAX_REASON) !== decision.reason) ||
224
+ !validTimestamp(decision.decided_at)) {
225
+ throw new Error(`approval ${request.id} has an unsupported or mismatched decision record`);
226
+ }
227
+ }
228
+ function validPolicyRequest(request) {
229
+ if (!request || typeof request !== "object")
230
+ return false;
231
+ return ((request.phase === "dispatch" || request.phase === "external_mutation") &&
232
+ typeof request.action === "string" &&
233
+ request.action.length > 0 &&
234
+ ["shared", "worktree", "sandbox", "remote"].includes(request.isolation) &&
235
+ ["enabled", "disabled", "unknown"].includes(request.network_access));
236
+ }
237
+ function validPolicyEvaluation(evaluation) {
238
+ if (!evaluation || typeof evaluation !== "object")
239
+ return false;
240
+ return (evaluation.verdict === "ask" &&
241
+ typeof evaluation.reason === "string" &&
242
+ evaluation.reason.length > 0 &&
243
+ Array.isArray(evaluation.rules) &&
244
+ evaluation.rules.every((rule) => rule &&
245
+ typeof rule.code === "string" &&
246
+ (rule.verdict === "allow" || rule.verdict === "ask" || rule.verdict === "deny") &&
247
+ typeof rule.reason === "string"));
248
+ }
249
+ function validTimestamp(value) {
250
+ return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
251
+ }
252
+ function approvalsRoot(coordRoot) {
253
+ return join(coordRoot, ".harnery", "approvals");
254
+ }
255
+ function approvalDir(coordRoot, approvalId) {
256
+ assertApprovalId(approvalId);
257
+ return join(approvalsRoot(coordRoot), approvalId);
258
+ }
259
+ function assertApprovalId(approvalId) {
260
+ if (!APPROVAL_ID.test(approvalId)) {
261
+ throw new Error(`invalid workflow approval id ${JSON.stringify(approvalId)}`);
262
+ }
263
+ }
264
+ function assertRunId(runId) {
265
+ if (!RUN_ID.test(runId))
266
+ throw new Error(`invalid workflow run id ${JSON.stringify(runId)}`);
267
+ }
268
+ function sha256(value, field) {
269
+ if (!/^[a-f0-9]{64}$/.test(value))
270
+ throw new Error(`${field} must be a SHA-256 digest`);
271
+ return value;
272
+ }
273
+ function bounded(value, field, max) {
274
+ if (typeof value !== "string")
275
+ throw new Error(`${field} must be a string`);
276
+ const normalized = Array.from(value, (character) => {
277
+ const code = character.charCodeAt(0);
278
+ return code < 32 || code === 127 ? " " : character;
279
+ })
280
+ .join("")
281
+ .replace(/\s+/g, " ")
282
+ .trim();
283
+ if (!normalized)
284
+ throw new Error(`${field} must not be empty`);
285
+ if (normalized.length > max)
286
+ throw new Error(`${field} exceeds ${max} characters`);
287
+ return normalized;
288
+ }
@@ -22,5 +22,11 @@ export declare class WorkflowRunError extends Error {
22
22
  readonly proofPath: string;
23
23
  constructor(message: string, runId: string, proofPath: string, cause?: unknown);
24
24
  }
25
+ export declare class WorkflowParkedError extends Error {
26
+ readonly runId: string;
27
+ readonly approvalId: string;
28
+ readonly journalPath: string;
29
+ constructor(message: string, runId: string, approvalId: string, journalPath: string);
30
+ }
25
31
  export declare function runWorkflow(scriptPath: string, opts: EngineOpts): Promise<RunReport>;
26
32
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAuBH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAOV,MAAM,YAAY,CAAC;AAYpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAsmB1F"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/workflow/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAoCH,OAAO,KAAK,EAEV,UAAU,EAEV,SAAS,EAOV,MAAM,YAAY,CAAC;AAYpB,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAM/E;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;CAOpF;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA2B1F"}