harnery 0.22.1 → 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 (99) 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/workspaces/attestation-error.d.ts +7 -0
  27. package/dist/core/workflow/workspaces/attestation-error.d.ts.map +1 -0
  28. package/dist/core/workflow/workspaces/attestation-error.js +25 -0
  29. package/dist/core/workflow/workspaces/cancellation.d.ts +8 -0
  30. package/dist/core/workflow/workspaces/cancellation.d.ts.map +1 -0
  31. package/dist/core/workflow/workspaces/cancellation.js +95 -0
  32. package/dist/core/workflow/workspaces/cleanup.d.ts +9 -0
  33. package/dist/core/workflow/workspaces/cleanup.d.ts.map +1 -0
  34. package/dist/core/workflow/workspaces/cleanup.js +223 -0
  35. package/dist/core/workflow/workspaces/execution.d.ts +25 -0
  36. package/dist/core/workflow/workspaces/execution.d.ts.map +1 -0
  37. package/dist/core/workflow/workspaces/execution.js +316 -0
  38. package/dist/core/workflow/workspaces/git.d.ts +25 -0
  39. package/dist/core/workflow/workspaces/git.d.ts.map +1 -0
  40. package/dist/core/workflow/workspaces/git.js +102 -0
  41. package/dist/core/workflow/workspaces/index.d.ts +15 -0
  42. package/dist/core/workflow/workspaces/index.d.ts.map +1 -0
  43. package/dist/core/workflow/workspaces/index.js +7 -0
  44. package/dist/core/workflow/workspaces/inspect.d.ts +85 -0
  45. package/dist/core/workflow/workspaces/inspect.d.ts.map +1 -0
  46. package/dist/core/workflow/workspaces/inspect.js +255 -0
  47. package/dist/core/workflow/workspaces/integration.d.ts +24 -0
  48. package/dist/core/workflow/workspaces/integration.d.ts.map +1 -0
  49. package/dist/core/workflow/workspaces/integration.js +617 -0
  50. package/dist/core/workflow/workspaces/leases.d.ts +37 -0
  51. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -0
  52. package/dist/core/workflow/workspaces/leases.js +380 -0
  53. package/dist/core/workflow/workspaces/lifecycle.d.ts +43 -0
  54. package/dist/core/workflow/workspaces/lifecycle.d.ts.map +1 -0
  55. package/dist/core/workflow/workspaces/lifecycle.js +622 -0
  56. package/dist/core/workflow/workspaces/local-git.d.ts +8 -0
  57. package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -0
  58. package/dist/core/workflow/workspaces/local-git.js +1474 -0
  59. package/dist/core/workflow/workspaces/paths.d.ts +23 -0
  60. package/dist/core/workflow/workspaces/paths.d.ts.map +1 -0
  61. package/dist/core/workflow/workspaces/paths.js +191 -0
  62. package/dist/core/workflow/workspaces/recovery.d.ts +3 -0
  63. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -0
  64. package/dist/core/workflow/workspaces/recovery.js +50 -0
  65. package/dist/core/workflow/workspaces/state.d.ts +60 -0
  66. package/dist/core/workflow/workspaces/state.d.ts.map +1 -0
  67. package/dist/core/workflow/workspaces/state.js +296 -0
  68. package/dist/core/workflow/workspaces/types.d.ts +445 -0
  69. package/dist/core/workflow/workspaces/types.d.ts.map +1 -0
  70. package/dist/core/workflow/workspaces/types.js +2 -0
  71. package/dist/core/workflow/workspaces/validate.d.ts +14 -0
  72. package/dist/core/workflow/workspaces/validate.d.ts.map +1 -0
  73. package/dist/core/workflow/workspaces/validate.js +476 -0
  74. package/package.json +5 -3
  75. package/src/commands/workflow.ts +110 -4
  76. package/src/core/work/state.ts +156 -28
  77. package/src/core/workflow/child-env.ts +18 -7
  78. package/src/core/workflow/durable-record.ts +94 -0
  79. package/src/core/workflow/engine.ts +230 -30
  80. package/src/core/workflow/index.ts +73 -0
  81. package/src/core/workflow/proof.ts +70 -22
  82. package/src/core/workflow/run-state.ts +46 -11
  83. package/src/core/workflow/types.ts +14 -0
  84. package/src/core/workflow/workspaces/attestation-error.ts +30 -0
  85. package/src/core/workflow/workspaces/cancellation.ts +137 -0
  86. package/src/core/workflow/workspaces/cleanup.ts +308 -0
  87. package/src/core/workflow/workspaces/execution.ts +412 -0
  88. package/src/core/workflow/workspaces/git.ts +125 -0
  89. package/src/core/workflow/workspaces/index.ts +85 -0
  90. package/src/core/workflow/workspaces/inspect.ts +421 -0
  91. package/src/core/workflow/workspaces/integration.ts +829 -0
  92. package/src/core/workflow/workspaces/leases.ts +495 -0
  93. package/src/core/workflow/workspaces/lifecycle.ts +792 -0
  94. package/src/core/workflow/workspaces/local-git.ts +1892 -0
  95. package/src/core/workflow/workspaces/paths.ts +247 -0
  96. package/src/core/workflow/workspaces/recovery.ts +56 -0
  97. package/src/core/workflow/workspaces/state.ts +481 -0
  98. package/src/core/workflow/workspaces/types.ts +517 -0
  99. package/src/core/workflow/workspaces/validate.ts +595 -0
@@ -0,0 +1,617 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { evaluatePolicy, normalizePolicy, policyDigest, summarizePolicyRequest, } from "../../policy/index.js";
4
+ import { readWorkItem } from "../../work/state.js";
5
+ import { createWorkflowApproval, readWorkflowApproval } from "../approvals.js";
6
+ import { readWorkflowProof } from "../proof.js";
7
+ import { readWorkflowRunManifest } from "../run-state.js";
8
+ import { acquireNoClobberLease } from "./leases.js";
9
+ import { appendIntegrationAttempt, appendWorkflowJournalEvent, fileSha256, readIntegrationAttempts, readWorkflowSupplement, stableDigest, workflowRunDir, writeWorkflowSupplement, } from "./state.js";
10
+ import { isWorkspaceAllocationRequest, isWorkspaceAttestation, isWorkspaceBoundExecutionEvidence, } from "./validate.js";
11
+ const INTEGRATION_DECISION_ID = "p99999";
12
+ const TARGET_LEASE_STALE_MS = 5 * 60 * 1_000;
13
+ export async function prepareIntegration(input) {
14
+ const evidence = await readAndValidateEvidence(input.coordRoot, input.runId, input.provider);
15
+ const targetRoot = resolve(input.targetRoot ?? requiredIntegrationRoot(evidence.binding));
16
+ const release = acquireTargetLease(input.coordRoot, targetRoot, input.runId);
17
+ try {
18
+ const proofSha256 = fileSha256(join(workflowRunDir(input.coordRoot, input.runId), "proof.json"));
19
+ const acceptedUnknowns = [...new Set(input.acceptedUnknowns ?? [])].sort();
20
+ const unknownCodes = [
21
+ ...evidence.proof.unknowns.map((item) => item.code),
22
+ ...evidence.attestation.unknowns.map((item) => item.code),
23
+ ];
24
+ const unaccepted = unknownCodes.filter((code) => !acceptedUnknowns.includes(code));
25
+ if (unaccepted.length > 0) {
26
+ throw new Error(`integration has unaccepted unknown verification facts: ${unaccepted.join(", ")}`);
27
+ }
28
+ const terminalAttestationSha256 = stableDigest(evidence.proof.execution.terminal_attestation);
29
+ const review = resolveReview(input, evidence.binding, proofSha256, terminalAttestationSha256, acceptedUnknowns);
30
+ const reviewSha256 = stableDigest(review);
31
+ const preview = await input.provider.previewIntegration({
32
+ schema_version: 1,
33
+ run_id: input.runId,
34
+ binding: evidence.binding,
35
+ binding_sha256: stableDigest(evidence.binding),
36
+ proof_sha256: proofSha256,
37
+ terminal_attestation: evidence.proof.execution.terminal_attestation,
38
+ terminal_attestation_sha256: terminalAttestationSha256,
39
+ review_sha256: reviewSha256,
40
+ accepted_unknowns: acceptedUnknowns,
41
+ target: { root: targetRoot },
42
+ });
43
+ assertVerifiedSourceCommit(evidence.proof, preview.source_commit);
44
+ if (preview.blocked.length > 0) {
45
+ throw new Error(`integration preview is blocked: ${preview.blocked.map((item) => item.message).join("; ")}`);
46
+ }
47
+ const previewUnknowns = preview.unknowns
48
+ .map((item) => item.code)
49
+ .filter((code) => !acceptedUnknowns.includes(code));
50
+ if (previewUnknowns.length > 0) {
51
+ throw new Error(`integration preview has unaccepted unknowns: ${previewUnknowns.join(", ")}`);
52
+ }
53
+ const planAuthority = {
54
+ run: input.runId,
55
+ binding: stableDigest(evidence.binding),
56
+ proof: proofSha256,
57
+ terminalAttestation: stableDigest(evidence.proof.execution.terminal_attestation),
58
+ review: reviewSha256,
59
+ target: preview.target_root,
60
+ targetRef: preview.target_ref,
61
+ targetCommit: preview.target_commit,
62
+ sourceCommit: preview.source_commit,
63
+ operation: preview.operation,
64
+ };
65
+ const proposed = {
66
+ schema_version: 1,
67
+ plan_id: `integration-plan-${stableDigest(planAuthority).slice(0, 24)}`,
68
+ run_id: input.runId,
69
+ operation: "fast_forward",
70
+ binding: evidence.binding,
71
+ binding_sha256: stableDigest(evidence.binding),
72
+ proof_sha256: proofSha256,
73
+ terminal_attestation_sha256: terminalAttestationSha256,
74
+ review_sha256: reviewSha256,
75
+ accepted_unknowns: acceptedUnknowns,
76
+ provider_preview: preview,
77
+ target_identity_sha256: stableDigest({
78
+ root: preview.target_root,
79
+ ref: preview.target_ref,
80
+ commit: preview.target_commit,
81
+ tree: preview.target_tree,
82
+ }),
83
+ idempotency_sha256: stableDigest(planAuthority),
84
+ prepared_at: new Date().toISOString(),
85
+ };
86
+ const prior = readWorkflowSupplement(input.coordRoot, input.runId, "integration/plan.json");
87
+ const plan = prior ?? proposed;
88
+ if (prior && !samePlanAuthority(prior, proposed)) {
89
+ throw new Error("existing integration plan no longer matches current evidence");
90
+ }
91
+ if (!prior) {
92
+ writeWorkflowSupplement(input.coordRoot, input.runId, "integration/plan.json", plan);
93
+ appendWorkflowJournalEvent(input.coordRoot, input.runId, "integration.plan", {
94
+ plan_id: plan.plan_id,
95
+ plan_sha256: stableDigest(plan),
96
+ });
97
+ }
98
+ authorizePlan(input, plan);
99
+ return plan;
100
+ }
101
+ finally {
102
+ release();
103
+ }
104
+ }
105
+ export async function applyIntegration(input) {
106
+ const storedPlan = readWorkflowSupplement(input.coordRoot, input.runId, "integration/plan.json");
107
+ if (!storedPlan)
108
+ throw new Error("integration apply requires a durable plan");
109
+ if (input.plan && stableDigest(input.plan) !== stableDigest(storedPlan)) {
110
+ throw new Error("integration apply caller plan does not match the durable plan");
111
+ }
112
+ const release = acquireTargetLease(input.coordRoot, storedPlan.provider_preview.target_root, input.runId);
113
+ try {
114
+ const evidence = await readAndValidateEvidence(input.coordRoot, input.runId, input.provider);
115
+ const proofSha256 = fileSha256(join(workflowRunDir(input.coordRoot, input.runId), "proof.json"));
116
+ if (proofSha256 !== storedPlan.proof_sha256 ||
117
+ stableDigest(evidence.binding) !== storedPlan.binding_sha256 ||
118
+ stableDigest(storedPlan.binding) !== storedPlan.binding_sha256 ||
119
+ stableDigest(evidence.proof.execution.terminal_attestation) !==
120
+ storedPlan.terminal_attestation_sha256) {
121
+ throw new Error("integration proof or binding changed after planning");
122
+ }
123
+ const review = readReviewForPlan(input.coordRoot, storedPlan);
124
+ if (stableDigest(review) !== storedPlan.review_sha256) {
125
+ throw new Error("integration review authority changed or disappeared");
126
+ }
127
+ const authorization = readWorkflowSupplement(input.coordRoot, input.runId, "integration/authorization.json");
128
+ if (!authorization)
129
+ throw new Error("integration authorization disappeared");
130
+ const planSha256 = stableDigest(storedPlan);
131
+ if (authorization.plan_sha256 !== planSha256 || authorization.decision.verdict !== "allow") {
132
+ throw new Error("integration authorization does not permit the durable plan");
133
+ }
134
+ if (authorization.decision_sha256 !== stableDigest(authorization.decision) ||
135
+ authorization.journal_anchor.event !== "integration.plan" ||
136
+ authorization.journal_anchor.plan_sha256 !== planSha256) {
137
+ throw new Error("integration authorization digest or journal anchor is corrupt");
138
+ }
139
+ assertIntegrationPlanJournal(input.coordRoot, input.runId, planSha256);
140
+ if (authorization.approval_id) {
141
+ const approval = readWorkflowApproval(input.coordRoot, authorization.approval_id);
142
+ if (approval.status !== "approved" ||
143
+ approval.request.run_id !== input.runId ||
144
+ approval.decision?.actor !== authorization.approval_actor ||
145
+ authorization.approval_sha256 !== stableDigest(approval)) {
146
+ throw new Error("integration approval authority is missing or mismatched");
147
+ }
148
+ }
149
+ const authorized = {
150
+ schema_version: 1,
151
+ plan: storedPlan,
152
+ plan_sha256: planSha256,
153
+ authorization,
154
+ authorization_sha256: stableDigest(authorization),
155
+ };
156
+ const previewInput = {
157
+ schema_version: 1,
158
+ run_id: input.runId,
159
+ binding: evidence.binding,
160
+ binding_sha256: stableDigest(evidence.binding),
161
+ proof_sha256: proofSha256,
162
+ terminal_attestation: evidence.proof.execution.terminal_attestation,
163
+ terminal_attestation_sha256: storedPlan.terminal_attestation_sha256,
164
+ review_sha256: storedPlan.review_sha256,
165
+ accepted_unknowns: storedPlan.accepted_unknowns,
166
+ target: {
167
+ root: storedPlan.provider_preview.target_root,
168
+ ref: storedPlan.provider_preview.target_ref,
169
+ },
170
+ };
171
+ const currentPreview = await input.provider.previewIntegration(previewInput);
172
+ assertVerifiedSourceCommit(evidence.proof, currentPreview.source_commit);
173
+ if (currentPreview.provider_id !== storedPlan.provider_preview.provider_id ||
174
+ currentPreview.binding_id !== storedPlan.binding.binding_id ||
175
+ currentPreview.target_root !== storedPlan.provider_preview.target_root ||
176
+ currentPreview.target_ref !== storedPlan.provider_preview.target_ref ||
177
+ currentPreview.source_commit !== storedPlan.provider_preview.source_commit ||
178
+ currentPreview.source_tree !== storedPlan.provider_preview.source_tree) {
179
+ throw new Error("integration source, provider, or target identity drifted");
180
+ }
181
+ if (currentPreview.blocked.length > 0) {
182
+ throw new Error(`integration preview is blocked: ${currentPreview.blocked
183
+ .map((item) => item.message)
184
+ .join("; ")}`);
185
+ }
186
+ for (const attempt of readIntegrationAttempts(input.coordRoot, input.runId)) {
187
+ if (attempt.run_id !== input.runId ||
188
+ attempt.plan_sha256 !== planSha256 ||
189
+ attempt.authorization_sha256 !== authorized.authorization_sha256 ||
190
+ attempt.proof_sha256 !== proofSha256 ||
191
+ attempt.binding_sha256 !== stableDigest(evidence.binding)) {
192
+ throw new Error("integration attempt does not match current durable authority");
193
+ }
194
+ }
195
+ const prior = readWorkflowSupplement(input.coordRoot, input.runId, "integration/receipt.json");
196
+ if (prior) {
197
+ if (prior.plan_sha256 !== planSha256 ||
198
+ prior.authorization_sha256 !== authorized.authorization_sha256 ||
199
+ prior.proof_sha256 !== proofSha256 ||
200
+ prior.binding_sha256 !== stableDigest(evidence.binding) ||
201
+ prior.target_commit !== currentPreview.target_commit ||
202
+ prior.target_tree !== currentPreview.target_tree ||
203
+ prior.source_commit !== storedPlan.provider_preview.source_commit) {
204
+ throw new Error("existing integration receipt does not match current target authority");
205
+ }
206
+ return prior;
207
+ }
208
+ appendIntegrationAttempt(input.coordRoot, input.runId, {
209
+ schema_version: 1,
210
+ run_id: input.runId,
211
+ plan_sha256: planSha256,
212
+ authorization_sha256: authorized.authorization_sha256,
213
+ proof_sha256: proofSha256,
214
+ binding_sha256: stableDigest(evidence.binding),
215
+ status: "started",
216
+ recorded_at: new Date().toISOString(),
217
+ });
218
+ const providerResult = await input.provider.applyAuthorizedIntegration({
219
+ schema_version: 1,
220
+ run_id: input.runId,
221
+ binding: evidence.binding,
222
+ binding_sha256: stableDigest(evidence.binding),
223
+ plan_id: storedPlan.plan_id,
224
+ plan_sha256: planSha256,
225
+ authorization_sha256: authorized.authorization_sha256,
226
+ proof_sha256: proofSha256,
227
+ terminal_attestation_sha256: storedPlan.terminal_attestation_sha256,
228
+ review_sha256: storedPlan.review_sha256,
229
+ accepted_unknowns: storedPlan.accepted_unknowns,
230
+ preview: storedPlan.provider_preview,
231
+ });
232
+ validateProviderIntegrationResult(providerResult, storedPlan, currentPreview);
233
+ const postApplyAttestation = await input.provider.reattach(evidence.binding);
234
+ if (!isWorkspaceAttestation(postApplyAttestation, evidence.binding) ||
235
+ postApplyAttestation.status !== "ok" ||
236
+ stableDigest(postApplyAttestation.owner) !== stableDigest(evidence.binding.owner)) {
237
+ throw new Error("integration result could not reattach the frozen workspace");
238
+ }
239
+ const postApplyPreview = await input.provider.previewIntegration(previewInput);
240
+ validatePostApplyState(providerResult, storedPlan, postApplyPreview);
241
+ appendIntegrationAttempt(input.coordRoot, input.runId, {
242
+ schema_version: 1,
243
+ run_id: input.runId,
244
+ plan_sha256: planSha256,
245
+ authorization_sha256: authorized.authorization_sha256,
246
+ proof_sha256: proofSha256,
247
+ binding_sha256: stableDigest(evidence.binding),
248
+ status: providerResult.status,
249
+ target_commit: providerResult.target_commit,
250
+ target_tree: providerResult.target_tree,
251
+ recorded_at: providerResult.applied_at,
252
+ });
253
+ const receipt = {
254
+ schema_version: 1,
255
+ receipt_id: `integration-apply-${stableDigest({
256
+ plan: planSha256,
257
+ authorization: authorized.authorization_sha256,
258
+ target: providerResult.target_commit,
259
+ }).slice(0, 24)}`,
260
+ run_id: input.runId,
261
+ plan_id: storedPlan.plan_id,
262
+ plan_sha256: planSha256,
263
+ authorization_sha256: authorized.authorization_sha256,
264
+ proof_sha256: proofSha256,
265
+ binding_sha256: stableDigest(evidence.binding),
266
+ binding_id: storedPlan.binding.binding_id,
267
+ provider_id: storedPlan.binding.provider.id,
268
+ status: providerResult.status,
269
+ target_root: storedPlan.provider_preview.target_root,
270
+ target_ref: storedPlan.provider_preview.target_ref,
271
+ source_commit: storedPlan.provider_preview.source_commit,
272
+ target_commit: providerResult.target_commit,
273
+ target_tree: providerResult.target_tree,
274
+ applied_at: providerResult.applied_at,
275
+ };
276
+ writeWorkflowSupplement(input.coordRoot, input.runId, "integration/receipt.json", receipt);
277
+ appendWorkflowJournalEvent(input.coordRoot, input.runId, "integration.apply", {
278
+ receipt_id: receipt.receipt_id,
279
+ status: receipt.status,
280
+ target_commit: receipt.target_commit,
281
+ });
282
+ return receipt;
283
+ }
284
+ finally {
285
+ release();
286
+ }
287
+ }
288
+ function validateProviderIntegrationResult(result, plan, before) {
289
+ const expectedStatus = before.target_commit === plan.provider_preview.source_commit ? "already_applied" : "applied";
290
+ if (result.schema_version !== 1 ||
291
+ result.binding_id !== plan.binding.binding_id ||
292
+ result.plan_id !== plan.plan_id ||
293
+ result.status !== expectedStatus ||
294
+ result.target_commit !== plan.provider_preview.source_commit ||
295
+ result.target_tree !== plan.provider_preview.source_tree ||
296
+ !Number.isFinite(Date.parse(result.applied_at))) {
297
+ throw new Error("workspace provider returned an invalid integration result");
298
+ }
299
+ }
300
+ function validatePostApplyState(result, plan, current) {
301
+ const expected = plan.provider_preview;
302
+ if (current.schema_version !== 1 ||
303
+ current.provider_id !== expected.provider_id ||
304
+ current.binding_id !== plan.binding.binding_id ||
305
+ current.operation !== plan.operation ||
306
+ current.target_root !== expected.target_root ||
307
+ current.target_ref !== expected.target_ref ||
308
+ current.source_commit !== expected.source_commit ||
309
+ current.source_tree !== expected.source_tree ||
310
+ current.target_commit !== result.target_commit ||
311
+ current.target_tree !== result.target_tree ||
312
+ current.target_commit !== expected.source_commit ||
313
+ current.target_tree !== expected.source_tree ||
314
+ current.blocked.length > 0) {
315
+ throw new Error("integration result does not match the reattached target state");
316
+ }
317
+ }
318
+ async function readAndValidateEvidence(coordRoot, runId, provider) {
319
+ const manifest = readWorkflowRunManifest(coordRoot, runId);
320
+ const proof = readWorkflowProof(coordRoot, runId);
321
+ if (proof.run.status !== "succeeded")
322
+ throw new Error("integration requires succeeded proof");
323
+ if (proof.acceptance.summary.unsatisfied > 0 || proof.acceptance.summary.unknown > 0) {
324
+ throw new Error("integration requires fully satisfied acceptance");
325
+ }
326
+ const binding = manifest.execution.workspace_binding;
327
+ if (!binding || !isWorkspaceBoundExecutionEvidence(proof.execution, runId))
328
+ throw new Error("integration requires isolated execution evidence");
329
+ const boundProof = proof;
330
+ if (boundProof.execution.terminal_lifecycle_state !== "completed_unintegrated") {
331
+ throw new Error("integration requires a successful completed workspace proof");
332
+ }
333
+ if (stableDigest(binding) !== stableDigest(boundProof.execution.binding) ||
334
+ stableDigest(binding.owner) !== stableDigest(boundProof.execution.terminal_attestation.owner)) {
335
+ throw new Error("manifest, proof, binding, and terminal attestation do not agree");
336
+ }
337
+ const attestation = await provider.reattach(binding);
338
+ if (!isWorkspaceAttestation(attestation, binding) ||
339
+ attestation.status !== "ok" ||
340
+ stableDigest(attestation.owner) !== stableDigest(binding.owner)) {
341
+ throw new Error("workspace provider reattachment did not attest the frozen binding");
342
+ }
343
+ const request = readWorkflowSupplement(coordRoot, runId, "workspace-request.json");
344
+ if (!request ||
345
+ !isWorkspaceAllocationRequest(request) ||
346
+ stableDigest(request) !== binding.request_sha256) {
347
+ throw new Error("workspace request is missing or does not match the binding");
348
+ }
349
+ const capabilities = await provider.probe({
350
+ requested_cwd: request.requested_cwd,
351
+ writable_roots: request.writable_roots.map((root) => root.configured),
352
+ });
353
+ if (capabilities.capabilities.provider_id !== binding.provider.id ||
354
+ capabilities.capabilities.provider_version !== binding.provider.version ||
355
+ capabilities.capabilities.capability_digest !== binding.provider.capability_digest) {
356
+ throw new Error("workspace provider identity or capabilities drifted");
357
+ }
358
+ return { proof: boundProof, binding, attestation };
359
+ }
360
+ function resolveReview(input, binding, proofSha256, terminalAttestationSha256, acceptedUnknowns) {
361
+ if (binding.owner.kind === "work_attempt") {
362
+ const record = readWorkAcceptance(input.coordRoot, binding, proofSha256, terminalAttestationSha256, acceptedUnknowns);
363
+ if (!record)
364
+ throw new Error("work-linked integration requires exact durable work acceptance");
365
+ return record;
366
+ }
367
+ if (!input.review?.actor)
368
+ throw new Error("standalone integration requires explicit review");
369
+ const review = {
370
+ schema_version: 1,
371
+ run_id: input.runId,
372
+ owner: binding.owner,
373
+ proof_sha256: proofSha256,
374
+ binding_id: binding.binding_id,
375
+ terminal_attestation_sha256: terminalAttestationSha256,
376
+ accepted_unknowns: acceptedUnknowns,
377
+ actor: input.review.actor,
378
+ reason: input.review.reason,
379
+ reviewed_at: new Date().toISOString(),
380
+ };
381
+ const prior = readWorkflowSupplement(input.coordRoot, input.runId, "integration/review.json");
382
+ if (prior) {
383
+ if (prior.run_id !== review.run_id ||
384
+ prior.proof_sha256 !== review.proof_sha256 ||
385
+ prior.binding_id !== review.binding_id ||
386
+ prior.terminal_attestation_sha256 !== review.terminal_attestation_sha256 ||
387
+ stableDigest(prior.accepted_unknowns) !== stableDigest(review.accepted_unknowns) ||
388
+ prior.actor !== review.actor ||
389
+ prior.reason !== review.reason) {
390
+ throw new Error("existing integration review does not match current evidence");
391
+ }
392
+ return prior;
393
+ }
394
+ writeWorkflowSupplement(input.coordRoot, input.runId, "integration/review.json", review);
395
+ return review;
396
+ }
397
+ function readReviewForPlan(coordRoot, plan) {
398
+ if (plan.binding.owner.kind === "work_attempt") {
399
+ const review = readWorkAcceptance(coordRoot, plan.binding, plan.proof_sha256, plan.terminal_attestation_sha256, plan.accepted_unknowns);
400
+ if (!review)
401
+ throw new Error("durable work acceptance disappeared");
402
+ return review;
403
+ }
404
+ const review = readWorkflowSupplement(coordRoot, plan.run_id, "integration/review.json");
405
+ if (!review)
406
+ throw new Error("standalone integration review disappeared");
407
+ return review;
408
+ }
409
+ function readWorkAcceptance(coordRoot, binding, proofSha256, terminalAttestationSha256, acceptedUnknowns) {
410
+ if (binding.owner.kind !== "work_attempt")
411
+ return undefined;
412
+ const work = readWorkItem(coordRoot, binding.owner.work_item_id);
413
+ const accepted = [...work.events]
414
+ .reverse()
415
+ .find((event) => ["work.accepted", "work.cancelled", "work.reopened"].includes(event.event));
416
+ if (work.projection.state !== "succeeded" ||
417
+ !accepted ||
418
+ accepted.event !== "work.accepted" ||
419
+ accepted.run_id !== binding.run_id ||
420
+ accepted.attempt !== binding.owner.attempt ||
421
+ accepted.proof_sha256 !== proofSha256 ||
422
+ accepted.workspace_binding_id !== binding.binding_id ||
423
+ accepted.terminal_attestation_sha256 !== terminalAttestationSha256 ||
424
+ stableDigest(accepted.accepted_unknowns ?? []) !== stableDigest(acceptedUnknowns) ||
425
+ typeof accepted.actor !== "string" ||
426
+ typeof accepted.ts !== "string") {
427
+ return undefined;
428
+ }
429
+ return {
430
+ schema_version: 1,
431
+ run_id: binding.run_id,
432
+ owner: binding.owner,
433
+ proof_sha256: proofSha256,
434
+ binding_id: binding.binding_id,
435
+ terminal_attestation_sha256: terminalAttestationSha256,
436
+ accepted_unknowns: [...acceptedUnknowns],
437
+ actor: accepted.actor,
438
+ reason: typeof accepted.reason === "string" ? accepted.reason : undefined,
439
+ reviewed_at: accepted.ts,
440
+ };
441
+ }
442
+ function assertVerifiedSourceCommit(proof, sourceCommit) {
443
+ const verifiedCommit = proof.execution.terminal_attestation.repository?.head_commit;
444
+ if (!verifiedCommit || verifiedCommit !== sourceCommit) {
445
+ throw new Error("integration source commit does not match the terminal proof");
446
+ }
447
+ }
448
+ function authorizePlan(input, plan) {
449
+ const policy = normalizePolicy(input.policy, {
450
+ baseDir: plan.provider_preview.target_root,
451
+ });
452
+ const request = summarizePolicyRequest({
453
+ phase: "external_mutation",
454
+ action: `fast-forward ${plan.provider_preview.target_ref}`,
455
+ path: plan.provider_preview.target_root,
456
+ isolation: plan.binding.isolation,
457
+ network_access: "disabled",
458
+ });
459
+ const evaluation = evaluatePolicy(policy, request);
460
+ if (evaluation.verdict === "deny") {
461
+ throw new Error(`integration policy denied: ${evaluation.reason}`);
462
+ }
463
+ let decision;
464
+ let approvalId;
465
+ let approvalActor;
466
+ let approvalSha256;
467
+ if (evaluation.verdict === "ask") {
468
+ const created = createWorkflowApproval({
469
+ coordRoot: input.coordRoot,
470
+ runId: input.runId,
471
+ decisionId: INTEGRATION_DECISION_ID,
472
+ addressedTo: input.approvalAddressee,
473
+ policy: { name: policy.name, sha256: policyDigest(policy) },
474
+ request,
475
+ evaluation,
476
+ });
477
+ approvalId = created.approval.request.id;
478
+ if (created.approval.status === "pending") {
479
+ throw new Error(`integration approval ${approvalId} is pending`);
480
+ }
481
+ if (created.approval.status !== "approved" || !created.approval.decision) {
482
+ throw new Error(`integration approval ${approvalId} was denied`);
483
+ }
484
+ approvalActor = created.approval.decision.actor;
485
+ approvalSha256 = stableDigest(created.approval);
486
+ decision = {
487
+ id: INTEGRATION_DECISION_ID,
488
+ checked_at: created.approval.decision.decided_at,
489
+ policy: policy.name,
490
+ phase: "external_mutation",
491
+ initial_verdict: "ask",
492
+ verdict: "allow",
493
+ resolved_by: "approval",
494
+ reason: created.approval.decision.reason ?? evaluation.reason,
495
+ rule_codes: evaluation.rules.map((rule) => rule.code),
496
+ request,
497
+ };
498
+ }
499
+ else {
500
+ decision = {
501
+ id: INTEGRATION_DECISION_ID,
502
+ checked_at: new Date().toISOString(),
503
+ policy: policy.name,
504
+ phase: "external_mutation",
505
+ initial_verdict: "allow",
506
+ verdict: "allow",
507
+ resolved_by: "policy",
508
+ reason: evaluation.reason,
509
+ rule_codes: evaluation.rules.map((rule) => rule.code),
510
+ request,
511
+ };
512
+ }
513
+ const authorization = {
514
+ schema_version: 1,
515
+ run_id: input.runId,
516
+ plan_sha256: stableDigest(plan),
517
+ policy_sha256: policyDigest(policy),
518
+ decision,
519
+ decision_sha256: stableDigest(decision),
520
+ approval_id: approvalId,
521
+ approval_actor: approvalActor,
522
+ approval_sha256: approvalSha256,
523
+ journal_anchor: {
524
+ event: "integration.plan",
525
+ plan_sha256: stableDigest(plan),
526
+ },
527
+ authorized_at: new Date().toISOString(),
528
+ };
529
+ const prior = readWorkflowSupplement(input.coordRoot, input.runId, "integration/authorization.json");
530
+ if (prior) {
531
+ if (prior.plan_sha256 !== authorization.plan_sha256 ||
532
+ prior.policy_sha256 !== authorization.policy_sha256 ||
533
+ prior.decision.verdict !== "allow" ||
534
+ prior.decision_sha256 !== stableDigest(prior.decision) ||
535
+ prior.approval_id !== authorization.approval_id ||
536
+ prior.approval_sha256 !== authorization.approval_sha256 ||
537
+ stableDigest(prior.journal_anchor) !== stableDigest(authorization.journal_anchor)) {
538
+ throw new Error("existing integration authorization does not match the exact plan");
539
+ }
540
+ return;
541
+ }
542
+ writeWorkflowSupplement(input.coordRoot, input.runId, "integration/authorization.json", authorization);
543
+ appendWorkflowJournalEvent(input.coordRoot, input.runId, "integration.authorized", {
544
+ plan_id: plan.plan_id,
545
+ authorization_sha256: stableDigest(authorization),
546
+ policy_sha256: authorization.policy_sha256,
547
+ approval_id: authorization.approval_id ?? null,
548
+ });
549
+ }
550
+ function samePlanAuthority(left, right) {
551
+ return (left.plan_id === right.plan_id &&
552
+ left.operation === right.operation &&
553
+ left.binding_sha256 === right.binding_sha256 &&
554
+ left.proof_sha256 === right.proof_sha256 &&
555
+ left.terminal_attestation_sha256 === right.terminal_attestation_sha256 &&
556
+ left.review_sha256 === right.review_sha256 &&
557
+ stableDigest(left.binding) === stableDigest(right.binding) &&
558
+ left.provider_preview.provider_id === right.provider_preview.provider_id &&
559
+ left.provider_preview.target_root === right.provider_preview.target_root &&
560
+ left.provider_preview.target_ref === right.provider_preview.target_ref &&
561
+ left.provider_preview.target_commit === right.provider_preview.target_commit &&
562
+ left.provider_preview.target_tree === right.provider_preview.target_tree &&
563
+ left.provider_preview.source_commit === right.provider_preview.source_commit &&
564
+ left.provider_preview.source_tree === right.provider_preview.source_tree &&
565
+ left.target_identity_sha256 === right.target_identity_sha256 &&
566
+ left.idempotency_sha256 === right.idempotency_sha256 &&
567
+ stableDigest(left.accepted_unknowns) === stableDigest(right.accepted_unknowns));
568
+ }
569
+ function requiredIntegrationRoot(binding) {
570
+ if (!binding.integration_root)
571
+ throw new Error("workspace binding has no integration root");
572
+ return binding.integration_root;
573
+ }
574
+ function assertIntegrationPlanJournal(coordRoot, runId, planSha256) {
575
+ const path = join(workflowRunDir(coordRoot, runId), "journal.jsonl");
576
+ let records;
577
+ try {
578
+ records = readFileSync(path, "utf8")
579
+ .split("\n")
580
+ .filter(Boolean)
581
+ .map((line) => JSON.parse(line));
582
+ }
583
+ catch (error) {
584
+ throw new Error(`workflow journal is corrupt: ${error.message}`);
585
+ }
586
+ if (records.some((record) => record.run_id !== runId || typeof record.event !== "string") ||
587
+ !records.some((record) => record.event === "integration.plan" && record.plan_sha256 === planSha256)) {
588
+ throw new Error("integration plan journal anchor is missing or mismatched");
589
+ }
590
+ }
591
+ function acquireTargetLease(coordRoot, targetRoot, runId) {
592
+ const resolvedTarget = resolve(targetRoot);
593
+ const authoritySha256 = stableDigest({
594
+ run_id: runId,
595
+ target_root: resolvedTarget,
596
+ operation: "integration",
597
+ });
598
+ const lease = acquireNoClobberLease({
599
+ path: join(resolve(coordRoot), ".harnery", "workspaces", ".integration-leases", `${stableDigest(resolvedTarget)}.lease`),
600
+ scope: "integration",
601
+ authoritySha256,
602
+ staleAfterMs: TARGET_LEASE_STALE_MS,
603
+ metadata: { run_id: runId, target_root: resolvedTarget },
604
+ validateStaleOwner: (owner) => {
605
+ const ownerRunId = owner.metadata?.run_id;
606
+ return (ownerRunId !== undefined &&
607
+ owner.metadata?.target_root === resolvedTarget &&
608
+ owner.authority_sha256 ===
609
+ stableDigest({
610
+ run_id: ownerRunId,
611
+ target_root: resolvedTarget,
612
+ operation: "integration",
613
+ }));
614
+ },
615
+ });
616
+ return () => lease.release();
617
+ }
@@ -0,0 +1,37 @@
1
+ export interface NoClobberLeaseOwner {
2
+ schema_version: 1;
3
+ owner_id: string;
4
+ owner_file: string;
5
+ scope: string;
6
+ authority_sha256: string;
7
+ metadata?: Record<string, string>;
8
+ pid: number;
9
+ host: string;
10
+ created_at: string;
11
+ expires_at: string;
12
+ }
13
+ export type LeaseRecoveryStep = "claim_created" | "stale_pinned" | "current_removed" | "new_current_linked";
14
+ export interface AcquireNoClobberLeaseInput {
15
+ path: string;
16
+ scope: string;
17
+ authoritySha256: string;
18
+ metadata?: Readonly<Record<string, string>>;
19
+ staleAfterMs: number;
20
+ validateStaleOwner?: (owner: NoClobberLeaseOwner) => boolean;
21
+ now?: () => number;
22
+ pid?: number;
23
+ host?: string;
24
+ onRecoveryStep?: (step: LeaseRecoveryStep) => void;
25
+ }
26
+ export interface NoClobberLease {
27
+ owner: NoClobberLeaseOwner;
28
+ recovered_owner?: NoClobberLeaseOwner;
29
+ release(): void;
30
+ }
31
+ /**
32
+ * Acquires a crash-safe lease whose active name is always a hard link to one
33
+ * immutable owner file. Stale takeover is serialized by an atomic recovery
34
+ * directory; only its claimant may pin and replace the observed `current`.
35
+ */
36
+ export declare function acquireNoClobberLease(input: AcquireNoClobberLeaseInput): NoClobberLease;
37
+ //# sourceMappingURL=leases.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/leases.ts"],"names":[],"mappings":"AAsBA,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,OAAO,IAAI,IAAI,CAAC;CACjB;AAUD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,cAAc,CA6FvF"}