harnery 0.22.0 → 0.23.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.
Files changed (103) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/workflow.d.ts.map +1 -1
  3. package/dist/commands/workflow.js +91 -2
  4. package/dist/core/work/state.d.ts +7 -0
  5. package/dist/core/work/state.d.ts.map +1 -1
  6. package/dist/core/work/state.js +112 -23
  7. package/dist/core/workflow/child-env.d.ts +10 -6
  8. package/dist/core/workflow/child-env.d.ts.map +1 -1
  9. package/dist/core/workflow/child-env.js +15 -7
  10. package/dist/core/workflow/durable-record.d.ts +7 -0
  11. package/dist/core/workflow/durable-record.d.ts.map +1 -0
  12. package/dist/core/workflow/durable-record.js +83 -0
  13. package/dist/core/workflow/engine.d.ts.map +1 -1
  14. package/dist/core/workflow/engine.js +208 -29
  15. package/dist/core/workflow/index.d.ts +2 -0
  16. package/dist/core/workflow/index.d.ts.map +1 -1
  17. package/dist/core/workflow/index.js +1 -0
  18. package/dist/core/workflow/proof.d.ts +4 -0
  19. package/dist/core/workflow/proof.d.ts.map +1 -1
  20. package/dist/core/workflow/proof.js +50 -15
  21. package/dist/core/workflow/run-state.d.ts +3 -0
  22. package/dist/core/workflow/run-state.d.ts.map +1 -1
  23. package/dist/core/workflow/run-state.js +25 -13
  24. package/dist/core/workflow/types.d.ts +10 -0
  25. package/dist/core/workflow/types.d.ts.map +1 -1
  26. package/dist/core/workflow/validate.d.ts +10 -1
  27. package/dist/core/workflow/validate.d.ts.map +1 -1
  28. package/dist/core/workflow/validate.js +75 -1
  29. package/dist/core/workflow/workspaces/attestation-error.d.ts +7 -0
  30. package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
  31. package/dist/core/workflow/workspaces/attestation-error.js +25 -0
  32. package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
  33. package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
  34. package/dist/core/workflow/workspaces/cancellation.js +95 -0
  35. package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
  36. package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
  37. package/dist/core/workflow/workspaces/cleanup.js +223 -0
  38. package/dist/core/workflow/workspaces/execution.d.ts +25 -0
  39. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
  40. package/dist/core/workflow/workspaces/execution.js +316 -0
  41. package/dist/core/workflow/workspaces/git.d.ts +25 -0
  42. package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
  43. package/dist/core/workflow/workspaces/git.js +102 -0
  44. package/dist/core/workflow/workspaces/index.d.ts +15 -0
  45. package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
  46. package/dist/core/workflow/workspaces/index.js +7 -0
  47. package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
  48. package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
  49. package/dist/core/workflow/workspaces/inspect.js +255 -0
  50. package/dist/core/workflow/workspaces/integration.d.ts +24 -0
  51. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/integration.js +617 -0
  53. package/dist/core/workflow/workspaces/leases.d.ts +37 -0
  54. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
  55. package/dist/core/workflow/workspaces/leases.js +380 -0
  56. package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
  57. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
  58. package/dist/core/workflow/workspaces/lifecycle.js +622 -0
  59. package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
  60. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
  61. package/dist/core/workflow/workspaces/local-git.js +1474 -0
  62. package/dist/core/workflow/workspaces/paths.d.ts +23 -0
  63. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
  64. package/dist/core/workflow/workspaces/paths.js +191 -0
  65. package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
  66. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
  67. package/dist/core/workflow/workspaces/recovery.js +50 -0
  68. package/dist/core/workflow/workspaces/state.d.ts +60 -0
  69. package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
  70. package/dist/core/workflow/workspaces/state.js +296 -0
  71. package/dist/core/workflow/workspaces/types.d.ts +445 -0
  72. package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
  73. package/dist/core/workflow/workspaces/types.js +2 -0
  74. package/dist/core/workflow/workspaces/validate.d.ts +14 -0
  75. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
  76. package/dist/core/workflow/workspaces/validate.js +476 -0
  77. package/package.json +5 -3
  78. package/src/commands/workflow.ts +110 -4
  79. package/src/core/work/state.ts +156 -28
  80. package/src/core/workflow/child-env.ts +18 -7
  81. package/src/core/workflow/durable-record.ts +94 -0
  82. package/src/core/workflow/engine.ts +230 -30
  83. package/src/core/workflow/index.ts +73 -0
  84. package/src/core/workflow/proof.ts +70 -22
  85. package/src/core/workflow/run-state.ts +46 -11
  86. package/src/core/workflow/types.ts +14 -0
  87. package/src/core/workflow/validate.ts +75 -1
  88. package/src/core/workflow/workspaces/attestation-error.ts +30 -0
  89. package/src/core/workflow/workspaces/cancellation.ts +137 -0
  90. package/src/core/workflow/workspaces/cleanup.ts +308 -0
  91. package/src/core/workflow/workspaces/execution.ts +412 -0
  92. package/src/core/workflow/workspaces/git.ts +125 -0
  93. package/src/core/workflow/workspaces/index.ts +85 -0
  94. package/src/core/workflow/workspaces/inspect.ts +421 -0
  95. package/src/core/workflow/workspaces/integration.ts +829 -0
  96. package/src/core/workflow/workspaces/leases.ts +495 -0
  97. package/src/core/workflow/workspaces/lifecycle.ts +792 -0
  98. package/src/core/workflow/workspaces/local-git.ts +1892 -0
  99. package/src/core/workflow/workspaces/paths.ts +247 -0
  100. package/src/core/workflow/workspaces/recovery.ts +56 -0
  101. package/src/core/workflow/workspaces/state.ts +481 -0
  102. package/src/core/workflow/workspaces/types.ts +517 -0
  103. package/src/core/workflow/workspaces/validate.ts +595 -0
@@ -38,6 +38,7 @@ import {
38
38
  import { assertWorkflowRunId, createWorkflowApproval } from "./approvals.ts";
39
39
  import { freezeWorkflowAttemptContext } from "./attempt-context.ts";
40
40
  import { type BillingProbe, probeBilling } from "./billing.ts";
41
+ import { stableDigest } from "./durable-record.ts";
41
42
  import {
42
43
  buildWorkflowProof,
43
44
  createEvidenceRecord,
@@ -71,6 +72,18 @@ import type {
71
72
  import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
72
73
  import { parseStageOutput, validateAgainstSchema } from "./validate.ts";
73
74
  import { freezeWorkflowWorkContext } from "./work-context.ts";
75
+ import {
76
+ attestTerminal,
77
+ attestWorkspaceFailure,
78
+ resolveWorkspaceBinding,
79
+ } from "./workspaces/execution.ts";
80
+ import { appendWorkflowJournalEvent } from "./workspaces/state.ts";
81
+ import type {
82
+ WorkspaceAttestation,
83
+ WorkspaceBinding,
84
+ WorkspaceUnsupportedExecutionEvidence,
85
+ } from "./workspaces/types.ts";
86
+ import { isWorkspaceAttestation } from "./workspaces/validate.ts";
74
87
 
75
88
  const DEFAULT_MAX_AGENTS = 50;
76
89
  const DEFAULT_CONCURRENCY = 4;
@@ -193,13 +206,19 @@ async function executeWorkflow(
193
206
  workContext: Readonly<WorkflowWorkContext> | undefined,
194
207
  attemptContext: Readonly<WorkflowAttemptContext> | undefined,
195
208
  ): Promise<RunReport> {
196
- const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
197
- if (typeof mod.default !== "function") {
198
- throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
209
+ const frozen = resumeState?.manifest.execution;
210
+ const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
211
+ let sharedWorkflow: WorkflowModule["default"] | undefined;
212
+ let sharedMeta: ReturnType<typeof normalizeWorkflowMeta> | undefined;
213
+ if (isolation === "shared") {
214
+ const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
215
+ if (typeof mod.default !== "function") {
216
+ throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
217
+ }
218
+ const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
219
+ sharedWorkflow = mod.default;
220
+ sharedMeta = normalizeWorkflowMeta(mod.meta, fallbackName);
199
221
  }
200
- const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
201
- const meta = normalizeWorkflowMeta(mod.meta, fallbackName);
202
- const name = meta.name;
203
222
 
204
223
  const runId =
205
224
  opts.resumeRunId ??
@@ -210,7 +229,6 @@ async function executeWorkflow(
210
229
  const journalPath = join(runDir, "journal.jsonl");
211
230
  const proofPath = join(runDir, "proof.json");
212
231
 
213
- const frozen = resumeState?.manifest.execution;
214
232
  const maxAgents = frozen?.max_agents ?? opts.maxAgents ?? DEFAULT_MAX_AGENTS;
215
233
  const concurrency = frozen?.concurrency ?? opts.concurrency ?? DEFAULT_CONCURRENCY;
216
234
  assertPositiveWorkflowBound(maxAgents, "maxAgents");
@@ -221,7 +239,6 @@ async function executeWorkflow(
221
239
  frozen?.default_harness ?? opts.defaultHarness ?? "claude-code";
222
240
  const specialists =
223
241
  frozen?.specialists ?? (resumeState ? {} : normalizeWorkflowSpecialists(opts.specialists));
224
- const isolation = frozen?.isolation ?? opts.isolation ?? "shared";
225
242
  const networkAccess = frozen?.network_access ?? opts.networkAccess ?? "unknown";
226
243
  const policy =
227
244
  frozen?.policy ?? (opts.policy ? normalizePolicy(opts.policy, { baseDir: cwd }) : undefined);
@@ -229,10 +246,102 @@ async function executeWorkflow(
229
246
  const approvalAddressee = frozen?.approval_addressee ?? opts.approvalAddressee ?? "operator";
230
247
  const subscriptionOnly = frozen?.subscription_only ?? Boolean(opts.subscriptionOnly);
231
248
  const allowApiBilling = frozen?.allow_api_billing ?? Boolean(opts.allowApiBilling);
232
- const repoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(cwd);
233
249
  const startedAt = resumeState?.manifest.started_at ?? new Date().toISOString();
234
250
  const t0 = resumeState ? Date.parse(startedAt) : Date.now();
235
-
251
+ const scriptSha256 = resumeState?.manifest.script.sha256 ?? workflowScriptDigest(absScript);
252
+ const fallbackName = scriptPath.replace(/^.*\//, "").replace(/\.[cm]?js$/, "");
253
+ const fallbackMeta = normalizeWorkflowMeta(undefined, fallbackName);
254
+ const workspaceProvider = isolation === "shared" ? undefined : opts.workspace?.provider;
255
+ let workspaceBinding: WorkspaceBinding | undefined;
256
+ let workspaceFallback: WorkspaceUnsupportedExecutionEvidence | undefined =
257
+ resumeState?.manifest.execution.workspace_fallback;
258
+ try {
259
+ workspaceBinding = await resolveWorkspaceBinding({
260
+ opts,
261
+ resumeState,
262
+ runId,
263
+ cwd,
264
+ isolation,
265
+ absScript,
266
+ scriptSha256,
267
+ provider: workspaceProvider,
268
+ workContext,
269
+ attemptContext,
270
+ policy,
271
+ onUnsupportedFallback: (evidence) => {
272
+ workspaceFallback = evidence;
273
+ },
274
+ });
275
+ } catch (error) {
276
+ const frozenBinding = resumeState?.manifest.execution.workspace_binding;
277
+ if (!resumeState || !frozenBinding) throw error;
278
+ const failure = error instanceof Error ? error : new Error(String(error));
279
+ const endedAt = new Date().toISOString();
280
+ const durationMs = Date.now() - t0;
281
+ const failedAttestation = await attestWorkspaceFailure(workspaceProvider, frozenBinding, error);
282
+ appendWorkflowJournalEvent(opts.coordRoot, runId, "run.resume", {
283
+ approval_id: resumeState.approvalId,
284
+ script: absScript,
285
+ resumed_at: new Date().toISOString(),
286
+ });
287
+ appendWorkflowJournalEvent(opts.coordRoot, runId, "workspace.reattach.failed", {
288
+ binding_id: frozenBinding.binding_id,
289
+ provider_id: frozenBinding.provider.id,
290
+ status: failedAttestation.status,
291
+ reason: failure.message,
292
+ attestation_sha256: stableDigest(failedAttestation),
293
+ });
294
+ appendWorkflowJournalEvent(opts.coordRoot, runId, "run.end", {
295
+ ok: false,
296
+ error: failure.message,
297
+ agents: 0,
298
+ cached: 0,
299
+ cost_usd: 0,
300
+ duration_ms: durationMs,
301
+ });
302
+ try {
303
+ const proof = buildWorkflowProof({
304
+ runId,
305
+ workItemId: opts.workItemId ?? resumeState.manifest.work_item_id,
306
+ workContext,
307
+ attemptContext,
308
+ meta: fallbackMeta,
309
+ status: "failed",
310
+ startedAt,
311
+ endedAt,
312
+ durationMs,
313
+ journalPath,
314
+ before: resumeState.manifest.repository_before,
315
+ after: resumeState.manifest.repository_before,
316
+ agents: [],
317
+ evidence: [],
318
+ harnessEvidence: opts.harnessEvidence,
319
+ policy: policy
320
+ ? {
321
+ config: policy,
322
+ decisions: [],
323
+ isolation,
324
+ networkAccess,
325
+ }
326
+ : undefined,
327
+ workspaceBinding: frozenBinding,
328
+ workspaceAttestation: failedAttestation,
329
+ error: failure.message,
330
+ });
331
+ writeWorkflowProof(proofPath, proof);
332
+ } catch (proofError) {
333
+ throw new WorkflowRunError(
334
+ `${failure.message}; recovery proof packet write also failed: ${(proofError as Error).message}`,
335
+ runId,
336
+ proofPath,
337
+ failure,
338
+ );
339
+ }
340
+ throw new WorkflowRunError(failure.message, runId, proofPath, failure);
341
+ }
342
+ const effectiveIsolation = workspaceFallback?.effective_isolation ?? isolation;
343
+ const executionCwd = workspaceBinding?.active_root ?? cwd;
344
+ const executionRepoBefore = resumeState?.manifest.repository_before ?? snapshotRepo(executionCwd);
236
345
  if (!resumeState) {
237
346
  writeWorkflowRunManifest({
238
347
  coordRoot: opts.coordRoot,
@@ -242,12 +351,12 @@ async function executeWorkflow(
242
351
  work_item_id: opts.workItemId,
243
352
  work_context: workContext,
244
353
  attempt_context: attemptContext,
245
- name,
354
+ name: sharedMeta?.name ?? fallbackMeta.name,
246
355
  started_at: startedAt,
247
- script: { path: absScript, sha256: workflowScriptDigest(absScript) },
248
- repository_before: repoBefore,
356
+ script: { path: absScript, sha256: scriptSha256 },
357
+ repository_before: executionRepoBefore,
249
358
  execution: {
250
- cwd,
359
+ cwd: executionCwd,
251
360
  default_harness: defaultHarness,
252
361
  max_agents: maxAgents,
253
362
  concurrency,
@@ -259,19 +368,80 @@ async function executeWorkflow(
259
368
  network_access: networkAccess,
260
369
  policy: policy ? (policy as NormalizedPolicy) : undefined,
261
370
  specialists,
371
+ workspace_binding: workspaceBinding,
372
+ workspace_fallback: workspaceFallback,
262
373
  },
263
374
  },
264
375
  });
265
376
  }
266
377
 
378
+ let workflow = sharedWorkflow;
379
+ let meta = sharedMeta ?? fallbackMeta;
380
+ let initializationError: Error | undefined;
381
+ let initializationAttestation: WorkspaceAttestation | undefined;
382
+ if (isolation !== "shared") {
383
+ try {
384
+ const mod = (await import(pathToFileURL(absScript).href)) as WorkflowModule;
385
+ if (typeof mod.default !== "function") {
386
+ throw new Error(`${scriptPath}: workflow script must \`export default async (ctx) => …\``);
387
+ }
388
+ workflow = mod.default;
389
+ meta = normalizeWorkflowMeta(mod.meta, fallbackName);
390
+ } catch (error) {
391
+ initializationError = error instanceof Error ? error : new Error(String(error));
392
+ }
393
+ }
394
+ if (workspaceBinding && workspaceProvider) {
395
+ try {
396
+ const postImport = await workspaceProvider.reattach(workspaceBinding);
397
+ if (!isWorkspaceAttestation(postImport, workspaceBinding) || postImport.status !== "ok") {
398
+ initializationAttestation = isWorkspaceAttestation(postImport, workspaceBinding)
399
+ ? postImport
400
+ : undefined;
401
+ throw new Error("workspace authority changed during workflow module initialization");
402
+ }
403
+ appendWorkflowJournalEvent(opts.coordRoot, runId, "workspace.reattach.module_initialized", {
404
+ binding_id: workspaceBinding.binding_id,
405
+ provider_id: workspaceBinding.provider.id,
406
+ attestation_sha256: stableDigest(postImport),
407
+ });
408
+ } catch (error) {
409
+ const reattachmentError = error instanceof Error ? error : new Error(String(error));
410
+ initializationAttestation ??= await attestWorkspaceFailure(
411
+ workspaceProvider,
412
+ workspaceBinding,
413
+ error,
414
+ );
415
+ initializationError = initializationError
416
+ ? new Error(`${initializationError.message}; ${reattachmentError.message}`, {
417
+ cause: reattachmentError,
418
+ })
419
+ : reattachmentError;
420
+ appendWorkflowJournalEvent(
421
+ opts.coordRoot,
422
+ runId,
423
+ "workspace.reattach.module_initialization_failed",
424
+ {
425
+ binding_id: workspaceBinding.binding_id,
426
+ provider_id: workspaceBinding.provider.id,
427
+ status: initializationAttestation.status,
428
+ reason: reattachmentError.message,
429
+ attestation_sha256: stableDigest(initializationAttestation),
430
+ },
431
+ );
432
+ }
433
+ }
434
+ const name = meta.name;
435
+ let observedWorkspaceAttestation = initializationAttestation;
436
+
267
437
  // Per-child fixed context overhead: children spawn in `cwd` and load its
268
438
  // repo-instructions file into their system prompt, cache-writing it once
269
439
  // per child. A fan-out multiplies this, so surface it BEFORE the burn.
270
- const contextTokensPerChildEstimate = estimateInstructionTokens(cwd);
440
+ const contextTokensPerChildEstimate = estimateInstructionTokens(executionCwd);
271
441
  if (contextTokensPerChildEstimate > 0) {
272
442
  log(
273
443
  `[context] each child cache-writes ~${Math.round(contextTokensPerChildEstimate / 1000)}K tokens of repo ` +
274
- `instructions from ${cwd}; a fan-out multiplies this per agent`,
444
+ `instructions from ${executionCwd}; a fan-out multiplies this per agent`,
275
445
  );
276
446
  }
277
447
 
@@ -528,7 +698,7 @@ async function executeWorkflow(
528
698
  const estimateRequest = summarizePolicyRequest({
529
699
  phase: "dispatch",
530
700
  action: "spawn agent",
531
- path: cwd,
701
+ path: executionCwd,
532
702
  harness,
533
703
  model: agentOpts.model,
534
704
  effort: agentOpts.effort,
@@ -536,7 +706,7 @@ async function executeWorkflow(
536
706
  max_turns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
537
707
  timeout_ms: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
538
708
  prompt_bytes: Buffer.byteLength(dispatchPrompt),
539
- isolation,
709
+ isolation: effectiveIsolation,
540
710
  network_access: networkAccess,
541
711
  current_cost_usd: round4(costUsd + reservedCostUsd),
542
712
  projected_cost_usd: null,
@@ -667,7 +837,7 @@ async function executeWorkflow(
667
837
  effort: agentOpts.effort,
668
838
  timeoutMs: agentOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
669
839
  maxTurns: agentOpts.maxTurns ?? DEFAULT_MAX_TURNS,
670
- cwd,
840
+ cwd: executionCwd,
671
841
  runId,
672
842
  subscriptionOnly,
673
843
  });
@@ -797,8 +967,8 @@ async function executeWorkflow(
797
967
  return authorizePolicyRequest({
798
968
  phase: "external_mutation",
799
969
  action: input.action,
800
- path: input.path ? resolve(cwd, input.path) : undefined,
801
- isolation,
970
+ path: input.path ? resolve(executionCwd, input.path) : undefined,
971
+ isolation: effectiveIsolation,
802
972
  network_access: network,
803
973
  service: input.service,
804
974
  target: input.target,
@@ -836,13 +1006,29 @@ async function executeWorkflow(
836
1006
  concurrency,
837
1007
  specialists: Object.keys(specialists),
838
1008
  policy: policy ? { name: policy.name, sha256: policyDigest(policy) } : null,
839
- isolation,
1009
+ isolation: effectiveIsolation,
840
1010
  network_access: networkAccess,
1011
+ workspace_binding: workspaceBinding
1012
+ ? { binding_id: workspaceBinding.binding_id, provider_id: workspaceBinding.provider.id }
1013
+ : null,
841
1014
  });
842
1015
  }
843
- const result = await mod.default(ctx);
1016
+ if (initializationError) throw initializationError;
1017
+ if (!workflow) throw new Error("workflow module initialization did not produce an executable");
1018
+ const result = await workflow(ctx);
844
1019
  const endedAt = new Date().toISOString();
845
1020
  const durationMs = Date.now() - t0;
1021
+ const workspaceAttestation = workspaceBinding
1022
+ ? await attestTerminal(workspaceProvider, workspaceBinding)
1023
+ : undefined;
1024
+ observedWorkspaceAttestation = workspaceAttestation;
1025
+ if (workspaceAttestation && workspaceAttestation.status !== "ok") {
1026
+ throw new Error(
1027
+ `terminal workspace attestation ${workspaceAttestation.status}: ${
1028
+ workspaceAttestation.provider_drift.join("; ") || "provider could not prove the workspace"
1029
+ }`,
1030
+ );
1031
+ }
846
1032
  journal("run.end", {
847
1033
  ok: true,
848
1034
  agents: agentsSpawned,
@@ -863,8 +1049,8 @@ async function executeWorkflow(
863
1049
  endedAt,
864
1050
  durationMs,
865
1051
  journalPath,
866
- before: repoBefore,
867
- after: snapshotRepo(cwd),
1052
+ before: executionRepoBefore,
1053
+ after: snapshotRepo(executionCwd),
868
1054
  agents: Array.from(agentProofs.values()),
869
1055
  evidence: evidenceRecords,
870
1056
  harnessEvidence: opts.harnessEvidence,
@@ -872,10 +1058,13 @@ async function executeWorkflow(
872
1058
  ? {
873
1059
  config: policy,
874
1060
  decisions: Array.from(policyDecisions.values()),
875
- isolation,
1061
+ isolation: effectiveIsolation,
876
1062
  networkAccess,
877
1063
  }
878
1064
  : undefined,
1065
+ workspaceBinding,
1066
+ workspaceAttestation,
1067
+ workspaceFallback,
879
1068
  result,
880
1069
  });
881
1070
  writeWorkflowProof(proofPath, proof);
@@ -905,10 +1094,14 @@ async function executeWorkflow(
905
1094
  mode: subscriptionOnly ? "subscription" : p.mode,
906
1095
  })),
907
1096
  policy: proof.policy?.summary,
1097
+ workspaceBinding,
908
1098
  };
909
1099
  return report;
910
1100
  } catch (err) {
911
- if (err instanceof WorkflowParkedError || err instanceof WorkflowRunError) throw err;
1101
+ if (err instanceof WorkflowParkedError) {
1102
+ throw err;
1103
+ }
1104
+ if (err instanceof WorkflowRunError) throw err;
912
1105
  const endedAt = new Date().toISOString();
913
1106
  const durationMs = Date.now() - t0;
914
1107
  journal("run.end", {
@@ -920,6 +1113,10 @@ async function executeWorkflow(
920
1113
  duration_ms: durationMs,
921
1114
  });
922
1115
  try {
1116
+ const workspaceAttestation = workspaceBinding
1117
+ ? (observedWorkspaceAttestation ??
1118
+ (await attestTerminal(workspaceProvider, workspaceBinding)))
1119
+ : undefined;
923
1120
  const proof = buildWorkflowProof({
924
1121
  runId,
925
1122
  workItemId: opts.workItemId ?? resumeState?.manifest.work_item_id,
@@ -931,8 +1128,8 @@ async function executeWorkflow(
931
1128
  endedAt,
932
1129
  durationMs,
933
1130
  journalPath,
934
- before: repoBefore,
935
- after: snapshotRepo(cwd),
1131
+ before: executionRepoBefore,
1132
+ after: snapshotRepo(executionCwd),
936
1133
  agents: Array.from(agentProofs.values()),
937
1134
  evidence: evidenceRecords,
938
1135
  harnessEvidence: opts.harnessEvidence,
@@ -940,10 +1137,13 @@ async function executeWorkflow(
940
1137
  ? {
941
1138
  config: policy,
942
1139
  decisions: Array.from(policyDecisions.values()),
943
- isolation,
1140
+ isolation: effectiveIsolation,
944
1141
  networkAccess,
945
1142
  }
946
1143
  : undefined,
1144
+ workspaceBinding,
1145
+ workspaceAttestation,
1146
+ workspaceFallback,
947
1147
  error: (err as Error).message,
948
1148
  });
949
1149
  writeWorkflowProof(proofPath, proof);
@@ -87,3 +87,76 @@ export {
87
87
  WORKFLOW_PROOF_SCHEMA_VERSION,
88
88
  WORKFLOW_WORK_CONTEXT_SCHEMA_VERSION,
89
89
  } from "./types.ts";
90
+ export type {
91
+ ApplyIntegrationInput,
92
+ AuthorizedIntegrationPlan,
93
+ AuthorizedProviderIntegrationInput,
94
+ CancelWorkspaceInput,
95
+ CleanupWorkspaceInput,
96
+ FilesystemIdentity,
97
+ IntegrationApplyAttempt,
98
+ IntegrationApplyReceipt,
99
+ IntegrationAuthorization,
100
+ IntegrationPlan,
101
+ IntegrationReviewRecord,
102
+ IntegrationTarget,
103
+ LocalGitWorktreeProviderOptions,
104
+ PrepareIntegrationInput,
105
+ ProviderIntegrationInput,
106
+ ProviderIntegrationPreview,
107
+ ProviderIntegrationResult,
108
+ ProviderUnknown,
109
+ ProviderUnsupportedReason,
110
+ RepositoryBinding,
111
+ ValidatedFilesystemPath,
112
+ WorkflowWorkspaceInspection,
113
+ WorkflowWorkspaceSelection,
114
+ WorkflowWorkspaceStatus,
115
+ WorkflowWorkspaceVerification,
116
+ WorkspaceAllocationRequest,
117
+ WorkspaceAttestation,
118
+ WorkspaceBinding,
119
+ WorkspaceBoundExecutionEvidence,
120
+ WorkspaceCancellationReceipt,
121
+ WorkspaceCancellationResult,
122
+ WorkspaceCleanupAttempt,
123
+ WorkspaceCleanupIntent,
124
+ WorkspaceCleanupMode,
125
+ WorkspaceCleanupReceipt,
126
+ WorkspaceCleanupResult,
127
+ WorkspaceCompatibilityExecutionEvidence,
128
+ WorkspaceExecutionEvidence,
129
+ WorkspaceIntegrationState,
130
+ WorkspaceIsolation,
131
+ WorkspaceLifecycleProjection,
132
+ WorkspaceLifecycleProjectionInput,
133
+ WorkspaceLifecycleState,
134
+ WorkspaceObservation,
135
+ WorkspaceOwner,
136
+ WorkspaceProbeInput,
137
+ WorkspaceProbeResult,
138
+ WorkspaceProofOutcome,
139
+ WorkspaceProvider,
140
+ WorkspaceProviderCapabilities,
141
+ WorkspaceProviderRef,
142
+ WorkspaceResourceState,
143
+ WorkspaceUnsupportedExecutionEvidence,
144
+ } from "./workspaces/index.ts";
145
+ export {
146
+ applyIntegration,
147
+ assertWorkspaceLifecycleTransition,
148
+ cancelWorkspace,
149
+ cleanupWorkspace,
150
+ createLocalGitWorktreeProvider,
151
+ deriveWorkspaceLifecycle,
152
+ inspectWorkflowWorkspace,
153
+ isWorkspaceLifecycleState,
154
+ listWorkflowWorkspaceInspections,
155
+ prepareIntegration,
156
+ probeLocalGitWorktreeProvider,
157
+ readWorkflowWorkspaceStatus,
158
+ renderWorkflowWorkspaceStatus,
159
+ validateProviderEventChain,
160
+ WORKSPACE_BINDING_SCHEMA_VERSION,
161
+ WORKSPACE_RECEIPT_SCHEMA_VERSION,
162
+ } from "./workspaces/index.ts";
@@ -1,15 +1,6 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- chmodSync,
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- statSync,
9
- unlinkSync,
10
- writeFileSync,
11
- } from "node:fs";
12
- import { dirname, join } from "node:path";
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
13
4
  import type { RepoSnapshot } from "../context/index.ts";
14
5
  import type {
15
6
  NormalizedPolicy,
@@ -19,6 +10,8 @@ import type {
19
10
  } from "../policy/index.ts";
20
11
  import { policyDigest } from "../policy/index.ts";
21
12
  import { isCanonicalWorkflowAttemptContext } from "./attempt-context.ts";
13
+ import { stableDigest, writeImmutableJson } from "./durable-record.ts";
14
+ import { readWorkflowRunManifest } from "./run-state.ts";
22
15
  import type {
23
16
  AcceptanceCriterion,
24
17
  AcceptanceResult,
@@ -40,6 +33,18 @@ import type {
40
33
  } from "./types.ts";
41
34
  import { WORKFLOW_PROOF_SCHEMA_VERSION } from "./types.ts";
42
35
  import { isCanonicalWorkflowWorkContext } from "./work-context.ts";
36
+ import type {
37
+ WorkspaceAttestation,
38
+ WorkspaceBinding,
39
+ WorkspaceExecutionEvidence,
40
+ WorkspaceUnsupportedExecutionEvidence,
41
+ } from "./workspaces/index.ts";
42
+ import {
43
+ isWorkspaceBoundExecutionEvidence,
44
+ isWorkspaceExecutionEvidence,
45
+ isWorkspaceUnsupportedExecutionEvidence,
46
+ workspaceProofLifecycle,
47
+ } from "./workspaces/validate.ts";
43
48
 
44
49
  const MAX_ACCEPTANCE_CRITERIA = 50;
45
50
  const MAX_EVIDENCE_RECORDS = 200;
@@ -82,6 +87,9 @@ export interface BuildWorkflowProofInput {
82
87
  isolation: PolicyIsolation;
83
88
  networkAccess: PolicyNetworkAccess;
84
89
  };
90
+ workspaceBinding?: WorkspaceBinding;
91
+ workspaceAttestation?: WorkspaceAttestation;
92
+ workspaceFallback?: WorkspaceUnsupportedExecutionEvidence;
85
93
  result?: unknown;
86
94
  error?: string;
87
95
  }
@@ -237,6 +245,10 @@ export function buildWorkflowProof(input: BuildWorkflowProofInput): WorkflowProo
237
245
  agents,
238
246
  evidence: input.evidence,
239
247
  policy: input.policy ? buildPolicyProof(input.policy) : undefined,
248
+ execution:
249
+ input.workspaceBinding && input.workspaceAttestation
250
+ ? buildExecutionEvidence(input.status, input.workspaceBinding, input.workspaceAttestation)
251
+ : input.workspaceFallback,
240
252
  repository,
241
253
  harnesses,
242
254
  unknowns,
@@ -286,15 +298,7 @@ export function writeWorkflowProof(path: string, proof: WorkflowProof): void {
286
298
  if (bytes > MAX_PACKET_BYTES) {
287
299
  throw new Error(`workflow proof is ${bytes} bytes; limit is ${MAX_PACKET_BYTES}`);
288
300
  }
289
- mkdirSync(dirname(path), { recursive: true });
290
- const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
291
- try {
292
- writeFileSync(temporary, body, { encoding: "utf8", flag: "wx", mode: 0o600 });
293
- renameSync(temporary, path);
294
- chmodSync(path, 0o600);
295
- } finally {
296
- if (existsSync(temporary)) unlinkSync(temporary);
297
- }
301
+ writeImmutableJson(path, proof);
298
302
  }
299
303
 
300
304
  export function readWorkflowProof(coordRoot: string, runId: string): WorkflowProof {
@@ -321,13 +325,57 @@ export function readWorkflowProof(coordRoot: string, runId: string): WorkflowPro
321
325
  (proof.run.attempt_context !== undefined &&
322
326
  (!proof.run.work_item_id ||
323
327
  !proof.run.work_context ||
324
- !isCanonicalWorkflowAttemptContext(proof.run.attempt_context)))
328
+ !isCanonicalWorkflowAttemptContext(proof.run.attempt_context))) ||
329
+ (proof.execution !== undefined &&
330
+ !isWorkspaceExecutionEvidence(proof.execution, runId, proof.run.status))
325
331
  ) {
326
332
  throw new Error(`workflow proof at ${path} has an unsupported or mismatched schema`);
327
333
  }
334
+ const manifestPath = join(coordRoot, ".harnery", "workflows", runId, "run.json");
335
+ if (proof.execution === undefined && !existsSync(manifestPath)) return proof;
336
+ const manifest = readWorkflowRunManifest(coordRoot, runId);
337
+ const manifestBinding = manifest.execution.workspace_binding;
338
+ const manifestFallback = manifest.execution.workspace_fallback;
339
+ const proofExecution = proof.execution;
340
+ const executionMatches =
341
+ manifestBinding !== undefined
342
+ ? isWorkspaceBoundExecutionEvidence(proofExecution, runId) &&
343
+ stableDigest(proofExecution.binding) === stableDigest(manifestBinding)
344
+ : manifestFallback !== undefined
345
+ ? isWorkspaceUnsupportedExecutionEvidence(proofExecution, runId) &&
346
+ stableDigest(proofExecution) === stableDigest(manifestFallback)
347
+ : proofExecution === undefined;
348
+ if (!executionMatches) {
349
+ throw new Error(`workflow proof at ${path} does not match the frozen execution manifest`);
350
+ }
328
351
  return proof;
329
352
  }
330
353
 
354
+ function buildExecutionEvidence(
355
+ workflowStatus: "succeeded" | "failed",
356
+ binding: WorkspaceBinding,
357
+ terminalAttestation: WorkspaceAttestation,
358
+ ): WorkspaceExecutionEvidence {
359
+ return {
360
+ schema_version: 1,
361
+ binding,
362
+ terminal_attestation: terminalAttestation,
363
+ terminal_lifecycle_state: workspaceProofLifecycle(workflowStatus, terminalAttestation),
364
+ drift: [...terminalAttestation.provider_drift],
365
+ unsupported: [...terminalAttestation.unsupported],
366
+ unknowns: [...terminalAttestation.unknowns],
367
+ receipts: {
368
+ request: "workspace-request.json",
369
+ cancellation_outcome: "cancellation/outcome.json",
370
+ integration_plan: "integration/plan.json",
371
+ integration_authorization: "integration/authorization.json",
372
+ integration_apply: "integration/receipt.json",
373
+ cleanup_intent: "cleanup/intent.json",
374
+ cleanup_receipt: "cleanup/receipt.json",
375
+ },
376
+ };
377
+ }
378
+
331
379
  export function renderWorkflowProof(proof: WorkflowProof): string {
332
380
  const lines = [
333
381
  `run ${proof.run.id} (${proof.run.name}): ${proof.run.status}`,