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
@@ -0,0 +1,223 @@
1
+ import { join } from "node:path";
2
+ import { readWorkflowProof } from "../proof.js";
3
+ import { readWorkflowRunManifest } from "../run-state.js";
4
+ import { acquireNoClobberLease } from "./leases.js";
5
+ import { containsPath } from "./paths.js";
6
+ import { appendCleanupAttempt, appendWorkflowJournalEvent, readCleanupAttempts, readWorkflowSupplement, stableDigest, workflowRunDir, writeWorkflowSupplement, } from "./state.js";
7
+ import { isWorkspaceAttestation, isWorkspaceBoundExecutionEvidence } from "./validate.js";
8
+ const CLEANUP_LEASE_STALE_MS = 5 * 60 * 1_000;
9
+ export async function cleanupWorkspace(input) {
10
+ const release = acquireCleanupLease(input.coordRoot, input.runId);
11
+ try {
12
+ return await cleanupWorkspaceUnderLease(input);
13
+ }
14
+ finally {
15
+ release();
16
+ }
17
+ }
18
+ async function cleanupWorkspaceUnderLease(input) {
19
+ const manifest = readWorkflowRunManifest(input.coordRoot, input.runId);
20
+ const proof = readWorkflowProof(input.coordRoot, input.runId);
21
+ const binding = manifest.execution.workspace_binding;
22
+ if (!binding ||
23
+ !isWorkspaceBoundExecutionEvidence(proof.execution, input.runId) ||
24
+ stableDigest(binding) !== stableDigest(proof.execution.binding)) {
25
+ throw new Error("workspace cleanup requires matching terminal execution evidence");
26
+ }
27
+ assertCleanupPathAuthority(binding, manifest.execution.policy?.allowed_paths ?? []);
28
+ const mode = input.mode ?? "normal";
29
+ const current = await input.provider.attest(binding);
30
+ if (!isWorkspaceAttestation(current, binding)) {
31
+ throw new Error("workspace cleanup provider returned an invalid current attestation");
32
+ }
33
+ const intent = resolveCleanupIntent(input, binding, current, mode);
34
+ for (const attempt of readCleanupAttempts(input.coordRoot, input.runId)) {
35
+ if (attempt.operation_id !== intent.operation_id ||
36
+ attempt.binding_id !== binding.binding_id ||
37
+ attempt.binding_sha256 !== intent.binding_sha256 ||
38
+ attempt.mode !== intent.mode ||
39
+ (attempt.attestation !== undefined && !isWorkspaceAttestation(attempt.attestation, binding))) {
40
+ throw new Error("workspace cleanup attempt does not match the immutable intent");
41
+ }
42
+ }
43
+ const receipt = readWorkflowSupplement(input.coordRoot, input.runId, "cleanup/receipt.json");
44
+ if (receipt) {
45
+ validateCleanupReceipt(receipt, binding, intent);
46
+ if (!isReleasedAttestation(current)) {
47
+ throw new Error("existing cleanup receipt no longer reattests exact resource absence");
48
+ }
49
+ return receipt;
50
+ }
51
+ appendCleanupAttempt(input.coordRoot, input.runId, {
52
+ schema_version: 1,
53
+ operation_id: intent.operation_id,
54
+ binding_id: binding.binding_id,
55
+ binding_sha256: intent.binding_sha256,
56
+ mode,
57
+ status: "started",
58
+ recorded_at: new Date().toISOString(),
59
+ });
60
+ const result = await input.provider.cleanup(binding, intent);
61
+ validateCleanupResult(result, binding);
62
+ const attempt = appendCleanupAttempt(input.coordRoot, input.runId, {
63
+ schema_version: 1,
64
+ operation_id: intent.operation_id,
65
+ binding_id: binding.binding_id,
66
+ binding_sha256: intent.binding_sha256,
67
+ mode,
68
+ status: result.status,
69
+ branch_deleted: result.branch_deleted,
70
+ attestation: result.attestation,
71
+ reason: result.reason,
72
+ recorded_at: result.recorded_at,
73
+ });
74
+ appendWorkflowJournalEvent(input.coordRoot, input.runId, "workspace.cleanup", {
75
+ binding_id: binding.binding_id,
76
+ provider_id: binding.provider.id,
77
+ operation_id: intent.operation_id,
78
+ attempt: attempt.seq,
79
+ status: attempt.status,
80
+ });
81
+ if (result.status !== "released" && result.status !== "already_released") {
82
+ return attempt;
83
+ }
84
+ if (!result.branch_deleted || !isReleasedAttestation(result.attestation)) {
85
+ throw new Error("workspace cleanup cannot receipt an incomplete resource release");
86
+ }
87
+ const terminal = {
88
+ schema_version: 1,
89
+ receipt_id: `cleanup-${stableDigest({
90
+ operation: intent.operation_id,
91
+ binding: intent.binding_sha256,
92
+ }).slice(0, 24)}`,
93
+ operation_id: intent.operation_id,
94
+ binding_id: binding.binding_id,
95
+ binding_sha256: intent.binding_sha256,
96
+ mode,
97
+ status: result.status,
98
+ branch_deleted: true,
99
+ attestation: result.attestation,
100
+ recorded_at: result.recorded_at,
101
+ };
102
+ writeWorkflowSupplement(input.coordRoot, input.runId, "cleanup/receipt.json", terminal);
103
+ return terminal;
104
+ }
105
+ function assertCleanupPathAuthority(binding, allowedPaths) {
106
+ if (allowedPaths.length > 0 &&
107
+ (!allowedPaths.some((root) => containsPath(root, binding.workspace_root)) ||
108
+ !allowedPaths.some((root) => containsPath(root, binding.active_root)) ||
109
+ (binding.repository !== undefined &&
110
+ (!allowedPaths.some((root) => containsPath(root, binding.repository.source_root.realpath)) ||
111
+ !allowedPaths.some((root) => containsPath(root, binding.repository.common_dir.realpath)))))) {
112
+ throw new Error("workspace cleanup path is outside frozen policy path authority");
113
+ }
114
+ if (!containsPath(binding.writable_root.realpath, binding.workspace_root) ||
115
+ !containsPath(binding.writable_root.realpath, binding.active_root)) {
116
+ throw new Error("workspace cleanup path is outside frozen writable-root authority");
117
+ }
118
+ }
119
+ function acquireCleanupLease(coordRoot, runId) {
120
+ const authoritySha256 = stableDigest({ run_id: runId, operation: "cleanup" });
121
+ const lease = acquireNoClobberLease({
122
+ path: join(workflowRunDir(coordRoot, runId), "cleanup", "operation.lease"),
123
+ scope: "cleanup",
124
+ authoritySha256,
125
+ staleAfterMs: CLEANUP_LEASE_STALE_MS,
126
+ metadata: { run_id: runId },
127
+ validateStaleOwner: (owner) => owner.authority_sha256 === authoritySha256 && owner.metadata?.run_id === runId,
128
+ });
129
+ return () => lease.release();
130
+ }
131
+ function resolveCleanupIntent(input, binding, attestation, mode) {
132
+ if (!binding.repository)
133
+ throw new Error("workspace cleanup requires repository identity");
134
+ const bindingSha256 = stableDigest(binding);
135
+ const operationId = `cleanup-${stableDigest({ binding: bindingSha256, mode }).slice(0, 24)}`;
136
+ const prior = readWorkflowSupplement(input.coordRoot, input.runId, "cleanup/intent.json");
137
+ if (prior) {
138
+ if (prior.schema_version !== 1 ||
139
+ prior.run_id !== input.runId ||
140
+ prior.binding_id !== binding.binding_id ||
141
+ prior.binding_sha256 !== bindingSha256 ||
142
+ prior.mode !== mode ||
143
+ prior.operation_id !== operationId) {
144
+ throw new Error("existing cleanup intent does not match current workspace authority");
145
+ }
146
+ validateCleanupIntentExpected(prior, binding);
147
+ return prior;
148
+ }
149
+ const workspaceRefOid = attestation.repository?.workspace_ref_oid;
150
+ const targetRefOid = attestation.repository?.target_ref_oid;
151
+ if (!workspaceRefOid || !targetRefOid) {
152
+ throw new Error("workspace cleanup cannot freeze exact source and target ref identities");
153
+ }
154
+ const proposed = {
155
+ schema_version: 1,
156
+ run_id: input.runId,
157
+ operation_id: operationId,
158
+ binding_id: binding.binding_id,
159
+ binding_sha256: bindingSha256,
160
+ mode,
161
+ expected: {
162
+ worktree_path: binding.workspace_root,
163
+ gitdir: binding.repository.gitdir,
164
+ workspace_ref: binding.repository.workspace_ref,
165
+ workspace_ref_oid: workspaceRefOid,
166
+ target_ref: binding.repository.target_ref,
167
+ target_ref_oid: targetRefOid,
168
+ },
169
+ created_at: new Date().toISOString(),
170
+ };
171
+ writeWorkflowSupplement(input.coordRoot, input.runId, "cleanup/intent.json", proposed);
172
+ return proposed;
173
+ }
174
+ function validateCleanupIntentExpected(intent, binding) {
175
+ const repository = binding.repository;
176
+ if (!repository ||
177
+ intent.expected.worktree_path !== binding.workspace_root ||
178
+ stableDigest(intent.expected.gitdir) !== stableDigest(repository.gitdir) ||
179
+ intent.expected.workspace_ref !== repository.workspace_ref ||
180
+ intent.expected.target_ref !== repository.target_ref ||
181
+ !/^[0-9a-f]{40,64}$/.test(intent.expected.workspace_ref_oid) ||
182
+ !/^[0-9a-f]{40,64}$/.test(intent.expected.target_ref_oid)) {
183
+ throw new Error("cleanup intent expected identities do not match the workspace binding");
184
+ }
185
+ }
186
+ function validateCleanupResult(result, binding) {
187
+ if (result.schema_version !== 1 ||
188
+ result.binding_id !== binding.binding_id ||
189
+ ![
190
+ "released",
191
+ "already_released",
192
+ "preserved_dirty",
193
+ "blocked",
194
+ "unsupported",
195
+ "partial",
196
+ ].includes(result.status) ||
197
+ !Number.isFinite(Date.parse(result.recorded_at)) ||
198
+ typeof result.branch_deleted !== "boolean" ||
199
+ !isWorkspaceAttestation(result.attestation, binding) ||
200
+ ((result.status === "released" || result.status === "already_released") &&
201
+ !isReleasedAttestation(result.attestation))) {
202
+ throw new Error("workspace cleanup provider returned an invalid result");
203
+ }
204
+ }
205
+ function validateCleanupReceipt(receipt, binding, intent) {
206
+ if (receipt.schema_version !== 1 ||
207
+ receipt.operation_id !== intent.operation_id ||
208
+ receipt.binding_id !== binding.binding_id ||
209
+ receipt.binding_sha256 !== intent.binding_sha256 ||
210
+ receipt.mode !== intent.mode ||
211
+ (receipt.status !== "released" && receipt.status !== "already_released") ||
212
+ receipt.branch_deleted !== true ||
213
+ !isWorkspaceAttestation(receipt.attestation, binding) ||
214
+ !isReleasedAttestation(receipt.attestation)) {
215
+ throw new Error("workspace cleanup receipt is invalid or mismatched");
216
+ }
217
+ }
218
+ function isReleasedAttestation(attestation) {
219
+ return (attestation.status === "ok" &&
220
+ attestation.provider_drift.length === 0 &&
221
+ attestation.resource_state === "released" &&
222
+ !attestation.workspace_exists);
223
+ }
@@ -0,0 +1,25 @@
1
+ import { type NormalizedPolicy } from "../../policy/index.js";
2
+ import type { WorkflowRunManifest } from "../run-state.js";
3
+ import type { EngineOpts, WorkflowAttemptContext, WorkflowWorkContext } from "../types.js";
4
+ import type { WorkspaceAttestation, WorkspaceBinding, WorkspaceProvider, WorkspaceUnsupportedExecutionEvidence } from "./types.js";
5
+ interface WorkspaceResumeState {
6
+ manifest: WorkflowRunManifest;
7
+ }
8
+ export declare function resolveWorkspaceBinding(input: {
9
+ opts: EngineOpts;
10
+ resumeState: WorkspaceResumeState | undefined;
11
+ runId: string;
12
+ cwd: string;
13
+ isolation: string;
14
+ absScript: string;
15
+ scriptSha256: string;
16
+ provider: WorkspaceProvider | undefined;
17
+ workContext: Readonly<WorkflowWorkContext> | undefined;
18
+ attemptContext: Readonly<WorkflowAttemptContext> | undefined;
19
+ policy: NormalizedPolicy | undefined;
20
+ onUnsupportedFallback?: (evidence: WorkspaceUnsupportedExecutionEvidence) => void;
21
+ }): Promise<WorkspaceBinding | undefined>;
22
+ export declare function attestTerminal(provider: WorkspaceProvider | undefined, binding: WorkspaceBinding): Promise<WorkspaceAttestation>;
23
+ export declare function attestWorkspaceFailure(provider: WorkspaceProvider | undefined, binding: WorkspaceBinding, error: unknown): Promise<WorkspaceAttestation>;
24
+ export {};
25
+ //# sourceMappingURL=execution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execution.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/execution.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAU3F,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EAEhB,iBAAiB,EACjB,qCAAqC,EACtC,MAAM,YAAY,CAAC;AAGpB,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,oBAAoB,GAAG,SAAS,CAAC;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACxC,WAAW,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAAG,SAAS,CAAC;IACvD,cAAc,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,SAAS,CAAC;IAC7D,MAAM,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrC,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,qCAAqC,KAAK,IAAI,CAAC;CACnF,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAuOxC;AAgDD,wBAAsB,cAAc,CAClC,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAa/B;AAED,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,OAAO,GACb,OAAO,CAAC,oBAAoB,CAAC,CAW/B"}
@@ -0,0 +1,316 @@
1
+ import { existsSync } from "node:fs";
2
+ import { policyDigest } from "../../policy/index.js";
3
+ import { WorkspaceAttestationError, workspaceAttestationFromError } from "./attestation-error.js";
4
+ import { assertContainedExisting, containsPath, filesystemIdentity, sameFilesystemIdentity, validateConfiguredRoot, } from "./paths.js";
5
+ import { appendWorkflowJournalEvent, stableDigest, writeWorkspaceRequest } from "./state.js";
6
+ import { isWorkspaceAttestation, isWorkspaceBinding } from "./validate.js";
7
+ export async function resolveWorkspaceBinding(input) {
8
+ if (input.isolation === "shared")
9
+ return undefined;
10
+ if (input.isolation !== "worktree" &&
11
+ input.isolation !== "sandbox" &&
12
+ input.isolation !== "remote") {
13
+ throw new Error(`unsupported workflow isolation ${JSON.stringify(input.isolation)}`);
14
+ }
15
+ if (input.resumeState && !input.resumeState.manifest.execution.workspace_binding) {
16
+ return undefined;
17
+ }
18
+ const capability = input.isolation;
19
+ const provider = input.provider;
20
+ if (!provider) {
21
+ if (input.resumeState?.manifest.execution.workspace_binding) {
22
+ throw new Error("isolated workflow resume requires the frozen workspace provider");
23
+ }
24
+ const fallback = {
25
+ schema_version: 1,
26
+ run_id: input.runId,
27
+ requested_isolation: capability,
28
+ effective_isolation: "shared",
29
+ selection_reason: "provider_not_configured",
30
+ terminal_lifecycle_state: "shared",
31
+ drift: [],
32
+ unsupported: [
33
+ {
34
+ code: "provider_not_configured",
35
+ message: "no workspace provider is configured; shared execution was selected",
36
+ },
37
+ ],
38
+ unknowns: [],
39
+ receipts: {},
40
+ };
41
+ appendWorkflowJournalEvent(input.opts.coordRoot, input.runId, "workspace.compatibility", {
42
+ requested_isolation: fallback.requested_isolation,
43
+ effective_isolation: fallback.effective_isolation,
44
+ selection_reason: fallback.selection_reason,
45
+ unsupported: fallback.unsupported,
46
+ });
47
+ input.onUnsupportedFallback?.(fallback);
48
+ return undefined;
49
+ }
50
+ const frozenBinding = input.resumeState?.manifest.execution.workspace_binding;
51
+ if (frozenBinding) {
52
+ const roots = input.opts.workspace?.writableRoots;
53
+ if (!roots || roots.length === 0) {
54
+ throw new Error("isolated workflow resume requires explicit writable roots");
55
+ }
56
+ assertWorkspaceRootAuthority(frozenBinding, roots, input.policy?.allowed_paths ?? []);
57
+ const probe = await provider.probe({
58
+ requested_cwd: frozenBinding.repository?.source_root.configured ?? input.cwd,
59
+ writable_roots: roots,
60
+ });
61
+ if (probe.capabilities.provider_id !== frozenBinding.provider.id ||
62
+ probe.capabilities.provider_version !== frozenBinding.provider.version ||
63
+ probe.capabilities.capability_digest !== frozenBinding.provider.capability_digest) {
64
+ throw new Error("parked workflow provider identity or capabilities drifted");
65
+ }
66
+ const providerBinding = await provider.readBinding(frozenBinding);
67
+ if (JSON.stringify(providerBinding) !== JSON.stringify(frozenBinding)) {
68
+ throw new Error("parked workflow binding differs from the immutable provider record");
69
+ }
70
+ const reattached = await provider.reattach(frozenBinding);
71
+ if (!isWorkspaceAttestation(reattached, frozenBinding) || reattached.status !== "ok") {
72
+ if (isWorkspaceAttestation(reattached, frozenBinding)) {
73
+ throw new WorkspaceAttestationError(`workflow run ${input.runId} could not reattach its frozen workspace`, reattached);
74
+ }
75
+ throw new Error(`workflow run ${input.runId} could not reattach its frozen workspace`);
76
+ }
77
+ appendWorkflowJournalEvent(input.opts.coordRoot, input.runId, "workspace.reattach", {
78
+ binding_id: frozenBinding.binding_id,
79
+ provider_id: frozenBinding.provider.id,
80
+ attestation_sha256: stableDigest(reattached),
81
+ });
82
+ return frozenBinding;
83
+ }
84
+ const roots = input.opts.workspace?.writableRoots;
85
+ if (roots?.length !== 1) {
86
+ throw new Error("isolated workflow allocation requires one explicitly selected writable root");
87
+ }
88
+ const selectedRoot = validateConfiguredRoot(roots[0]);
89
+ const sourceRoot = validateConfiguredRoot(input.cwd);
90
+ const allowedRoots = (input.policy?.allowed_paths ?? []).map((path) => validateConfiguredRoot(path));
91
+ if (sourceRoot.configured !== input.cwd ||
92
+ (allowedRoots.length > 0 &&
93
+ !allowedRoots.some((path) => containsPath(path.realpath, sourceRoot.realpath)))) {
94
+ throw new Error("workflow source root is outside frozen path authority");
95
+ }
96
+ if (allowedRoots.length > 0 &&
97
+ !allowedRoots.some((path) => containsPath(path.realpath, selectedRoot.realpath))) {
98
+ throw new Error("workspace writable root is outside frozen path authority");
99
+ }
100
+ const probe = await provider.probe({
101
+ requested_cwd: input.cwd,
102
+ writable_roots: [selectedRoot.configured],
103
+ });
104
+ if (!probe.supported ||
105
+ probe.unsupported.length > 0 ||
106
+ !probe.capabilities.isolation.includes(capability)) {
107
+ if (input.policy?.allowed_isolation?.includes("shared")) {
108
+ const unsupported = probe.unsupported.length > 0
109
+ ? [...probe.unsupported]
110
+ : [
111
+ {
112
+ code: "isolation_unsupported",
113
+ message: `provider does not support ${capability} isolation`,
114
+ },
115
+ ];
116
+ const fallback = {
117
+ schema_version: 1,
118
+ run_id: input.runId,
119
+ requested_isolation: capability,
120
+ effective_isolation: "shared",
121
+ selection_reason: "provider_unsupported",
122
+ provider: {
123
+ id: probe.capabilities.provider_id,
124
+ version: probe.capabilities.provider_version,
125
+ capability_digest: probe.capabilities.capability_digest,
126
+ },
127
+ terminal_lifecycle_state: "shared",
128
+ drift: [],
129
+ unsupported,
130
+ unknowns: [...probe.unknowns],
131
+ receipts: {},
132
+ };
133
+ appendWorkflowJournalEvent(input.opts.coordRoot, input.runId, "workspace.fallback", {
134
+ requested_isolation: fallback.requested_isolation,
135
+ effective_isolation: fallback.effective_isolation,
136
+ selection_reason: fallback.selection_reason,
137
+ provider_id: fallback.provider?.id,
138
+ unsupported: fallback.unsupported,
139
+ });
140
+ input.onUnsupportedFallback?.(fallback);
141
+ return undefined;
142
+ }
143
+ throw new Error(`workspace provider ${probe.capabilities.provider_id} does not support ${capability}: ${probe.unsupported.map((item) => item.message).join("; ") || "unsupported capability"}`);
144
+ }
145
+ if (input.opts.workItemId && !input.attemptContext) {
146
+ throw new Error("isolated work-linked execution requires a frozen attempt identity");
147
+ }
148
+ const requestWithoutKey = {
149
+ schema_version: 1,
150
+ run_id: input.runId,
151
+ owner: input.opts.workItemId && input.attemptContext
152
+ ? {
153
+ kind: "work_attempt",
154
+ work_item_id: input.opts.workItemId,
155
+ attempt: input.attemptContext.number,
156
+ }
157
+ : { kind: "standalone", work_item_id: null, attempt: null },
158
+ requested_cwd: input.cwd,
159
+ requested_isolation: capability,
160
+ network_access: input.opts.networkAccess ?? "unknown",
161
+ script: { path: input.absScript, sha256: input.scriptSha256 },
162
+ policy_sha256: input.policy ? policyDigest(input.policy) : null,
163
+ allowed_paths: allowedRoots,
164
+ writable_roots: [selectedRoot],
165
+ selected_writable_root: selectedRoot,
166
+ provider_id: probe.capabilities.provider_id,
167
+ capability_digest: probe.capabilities.capability_digest,
168
+ };
169
+ const request = {
170
+ ...requestWithoutKey,
171
+ idempotency_key: stableDigest(requestWithoutKey),
172
+ };
173
+ writeWorkspaceRequest(input.opts.coordRoot, request);
174
+ appendWorkflowJournalEvent(input.opts.coordRoot, input.runId, "execution.allocate.start", {
175
+ provider_id: probe.capabilities.provider_id,
176
+ request_sha256: stableDigest(request),
177
+ });
178
+ const binding = await provider.allocate(request);
179
+ if (!isWorkspaceBinding(binding, input.runId) ||
180
+ binding.request_sha256 !== stableDigest(request) ||
181
+ stableDigest(binding.owner) !== stableDigest(request.owner) ||
182
+ binding.provider.id !== probe.capabilities.provider_id ||
183
+ binding.provider.version !== probe.capabilities.provider_version ||
184
+ binding.provider.capability_digest !== probe.capabilities.capability_digest ||
185
+ binding.isolation !== capability) {
186
+ throw new Error("workspace provider returned a binding that does not match the frozen request");
187
+ }
188
+ const providerBinding = await provider.readBinding(binding);
189
+ if (JSON.stringify(providerBinding) !== JSON.stringify(binding)) {
190
+ throw new Error("workspace provider returned bytes that differ from its immutable binding");
191
+ }
192
+ assertWorkspaceRootAuthority(binding, [selectedRoot.configured], input.policy?.allowed_paths ?? []);
193
+ const attestation = await provider.reattach(binding);
194
+ if (!isWorkspaceAttestation(attestation, binding) || attestation.status !== "ok") {
195
+ if (isWorkspaceAttestation(attestation, binding)) {
196
+ throw new WorkspaceAttestationError("workspace provider returned a binding that failed initial attestation", attestation);
197
+ }
198
+ throw new Error("workspace provider returned a binding that failed initial attestation");
199
+ }
200
+ appendWorkflowJournalEvent(input.opts.coordRoot, input.runId, "execution.allocate.end", {
201
+ binding_id: binding.binding_id,
202
+ provider_id: binding.provider.id,
203
+ active_root: binding.active_root,
204
+ attestation_sha256: stableDigest(attestation),
205
+ });
206
+ return binding;
207
+ }
208
+ function assertWorkspaceRootAuthority(binding, configuredRoots, allowedPaths) {
209
+ const roots = configuredRoots.map((root) => validateConfiguredRoot(root));
210
+ const authority = roots.find((root) => root.configured === binding.writable_root.configured &&
211
+ root.realpath === binding.writable_root.realpath &&
212
+ sameFilesystemIdentity(root.identity, binding.writable_root.identity));
213
+ if (!authority) {
214
+ throw new Error("workspace provider binding is outside the explicit writable-root authority");
215
+ }
216
+ if (allowedPaths.length > 0 &&
217
+ (!allowedPaths.some((root) => containsPath(root, binding.workspace_root)) ||
218
+ !allowedPaths.some((root) => containsPath(root, binding.active_root)) ||
219
+ (binding.repository !== undefined &&
220
+ (!allowedPaths.some((root) => containsPath(root, binding.repository.source_root.realpath)) ||
221
+ !allowedPaths.some((root) => containsPath(root, binding.repository.common_dir.realpath)))))) {
222
+ throw new Error("workspace provider binding is outside frozen policy path authority");
223
+ }
224
+ assertContainedExisting(authority, binding.workspace_root, "workspace root");
225
+ assertContainedExisting(authority, binding.active_root, "active workspace root");
226
+ if (!sameFilesystemIdentity(filesystemIdentity(binding.workspace_root), binding.workspace_root_identity)) {
227
+ throw new Error("workspace root identity differs from the frozen binding");
228
+ }
229
+ if (!sameFilesystemIdentity(filesystemIdentity(binding.active_root), binding.active_root_identity)) {
230
+ throw new Error("active workspace root identity differs from the frozen binding");
231
+ }
232
+ }
233
+ export async function attestTerminal(provider, binding) {
234
+ if (!provider)
235
+ return failedWorkspaceAttestation(binding, "workspace provider is unavailable");
236
+ try {
237
+ const attestation = await provider.attest(binding);
238
+ return isWorkspaceAttestation(attestation, binding)
239
+ ? attestation
240
+ : failedWorkspaceAttestation(binding, "workspace provider returned invalid attestation");
241
+ }
242
+ catch (error) {
243
+ return (workspaceAttestationFromError(error, binding) ??
244
+ failedWorkspaceAttestation(binding, errorMessage(error)));
245
+ }
246
+ }
247
+ export async function attestWorkspaceFailure(provider, binding, error) {
248
+ const carried = workspaceAttestationFromError(error, binding);
249
+ if (carried)
250
+ return carried;
251
+ const observed = await attestTerminal(provider, binding);
252
+ if (observed.status !== "ok")
253
+ return observed;
254
+ return {
255
+ ...observed,
256
+ provider_drift: [...observed.provider_drift, errorMessage(error)],
257
+ resource_state: observed.workspace_exists ? "blocked" : "lost",
258
+ status: observed.workspace_exists ? "blocked" : "lost",
259
+ };
260
+ }
261
+ function failedWorkspaceAttestation(binding, message) {
262
+ const workspaceExists = existsSync(binding.workspace_root);
263
+ let rootMatch = false;
264
+ let workspaceContained = false;
265
+ let activeContained = false;
266
+ let workspaceIdentity;
267
+ let activeIdentity;
268
+ try {
269
+ const root = validateConfiguredRoot(binding.writable_root.configured);
270
+ rootMatch =
271
+ root.realpath === binding.writable_root.realpath &&
272
+ sameFilesystemIdentity(root.identity, binding.writable_root.identity);
273
+ if (workspaceExists) {
274
+ const workspaceReal = assertContainedExisting(root, binding.workspace_root, "workspace root");
275
+ workspaceContained = workspaceReal === binding.workspace_root;
276
+ workspaceIdentity = filesystemIdentity(workspaceReal);
277
+ }
278
+ if (existsSync(binding.active_root)) {
279
+ const activeReal = assertContainedExisting(root, binding.active_root, "active root");
280
+ activeContained = activeReal === binding.active_root;
281
+ activeIdentity = filesystemIdentity(activeReal);
282
+ }
283
+ }
284
+ catch {
285
+ // Preserve the facts that could be observed before path validation failed.
286
+ }
287
+ return {
288
+ schema_version: 1,
289
+ binding_id: binding.binding_id,
290
+ workspace_id: binding.workspace_id,
291
+ run_id: binding.run_id,
292
+ owner: binding.owner,
293
+ provider: binding.provider,
294
+ recorded_at: new Date().toISOString(),
295
+ containment: {
296
+ writable_root: rootMatch,
297
+ workspace_root: workspaceContained,
298
+ active_root: activeContained,
299
+ integration_root: null,
300
+ },
301
+ filesystem: {
302
+ root_identity_match: rootMatch,
303
+ workspace_identity: workspaceIdentity,
304
+ active_identity: activeIdentity,
305
+ },
306
+ provider_drift: [message],
307
+ workspace_exists: workspaceExists,
308
+ resource_state: workspaceExists ? "blocked" : "lost",
309
+ unsupported: [],
310
+ unknowns: [],
311
+ status: workspaceExists ? "blocked" : "lost",
312
+ };
313
+ }
314
+ function errorMessage(error) {
315
+ return error instanceof Error ? error.message : String(error);
316
+ }
@@ -0,0 +1,25 @@
1
+ export type GitResult = {
2
+ ok: true;
3
+ out: string;
4
+ } | {
5
+ ok: false;
6
+ err: string;
7
+ };
8
+ export declare function git(cwd: string, args: readonly string[]): string;
9
+ export declare function gitMaybe(cwd: string, args: readonly string[]): GitResult;
10
+ export declare function gitMaybeWithDirectoryDescriptor(cwd: string, args: readonly string[], fd: number): GitResult;
11
+ export declare function worktreeInventory(cwd: string): Array<{
12
+ path: string;
13
+ ref?: string;
14
+ head?: string;
15
+ }>;
16
+ export declare function parseWorktreeInventory(body: string): Array<{
17
+ path: string;
18
+ ref?: string;
19
+ head?: string;
20
+ }>;
21
+ export declare function gitOperations(cwd: string): string[];
22
+ export declare function overwriteRiskPaths(cwd: string, changed: readonly string[]): string[];
23
+ export declare function isAncestor(cwd: string, ancestor: string, descendant: string): boolean;
24
+ export declare function nulEntries(value: string): string[];
25
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../../../src/core/workflow/workspaces/git.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,SAAS,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/E,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAIhE;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAExE;AAED,wBAAgB,+BAA+B,CAC7C,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,SAAS,CAKX;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,GACV,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAEtD;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,GACX,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBtD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAenD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOpF;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAErF;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAElD"}