owlrunkit 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +674 -0
  3. package/NOTICE.md +137 -0
  4. package/README.md +44 -0
  5. package/SOURCE.md +10 -0
  6. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
  7. package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
  8. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
  9. package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
  10. package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
  11. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
  12. package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
  13. package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
  14. package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
  15. package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
  16. package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
  17. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
  18. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
  19. package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
  20. package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
  21. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
  22. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
  23. package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
  24. package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
  25. package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
  26. package/package.json +24 -0
  27. package/packages/attest/LICENSE +674 -0
  28. package/packages/attest/README.md +63 -0
  29. package/packages/attest/cli/owlcoda-attest.mjs +155 -0
  30. package/packages/attest/consumer-surface-v1.json +255 -0
  31. package/packages/attest/distribution-manifest-v1.json +104 -0
  32. package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
  33. package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
  34. package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
  35. package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
  36. package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
  37. package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
  38. package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
  39. package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
  40. package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
  41. package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
  42. package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
  43. package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
  44. package/packages/attest/fixtures/index.json +90 -0
  45. package/packages/attest/sbom.spdx.json +33 -0
  46. package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
  47. package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
  48. package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
  49. package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
  50. package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
  51. package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
  52. package/packages/attest/src/canonical.mjs +12 -0
  53. package/packages/attest/src/formal.mjs +762 -0
  54. package/packages/attest/src/index.mjs +113 -0
  55. package/packages/attest/src/offline-bundle.mjs +108 -0
  56. package/packages/attest/src/quick-receipt-contract.mjs +156 -0
  57. package/packages/attest/src/quick.mjs +274 -0
  58. package/packages/attest/src/reference-contract.mjs +64 -0
  59. package/packages/attest/src/reference.mjs +131 -0
  60. package/packages/attest/src/workspace-snapshot.mjs +171 -0
  61. package/registry-release-policy-v1.json +34 -0
  62. package/sbom.spdx.json +39 -0
  63. package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
  64. package/scripts/runkit-contract/core-contract.mjs +392 -0
  65. package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
  66. package/scripts/runkit-contract/delivery-create.mjs +252 -0
  67. package/scripts/runkit-contract/delivery-selection.mjs +31 -0
  68. package/scripts/runkit-contract/finalize.mjs +790 -0
  69. package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
  70. package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
  71. package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
  72. package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
  73. package/scripts/runkit-contract/offline-store.mjs +248 -0
  74. package/scripts/runkit-contract/profile-impact.mjs +223 -0
  75. package/scripts/runkit-contract/provenance-common.mjs +153 -0
  76. package/scripts/runkit-contract/quick-attest.mjs +4 -0
  77. package/scripts/runkit-contract/quick-canonical.mjs +41 -0
  78. package/scripts/runkit-contract/quick-metrics.mjs +86 -0
  79. package/scripts/runkit-contract/quick-receipt.mjs +139 -0
  80. package/scripts/runkit-contract/quick-verify.mjs +201 -0
  81. package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
  82. package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
  83. package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
  84. package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
  85. package/scripts/runkit-contract/repair-execution.mjs +997 -0
  86. package/scripts/runkit-contract/resource-preflight.mjs +829 -0
  87. package/scripts/runkit-contract/resume-execution.mjs +569 -0
  88. package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
  89. package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
  90. package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
  91. package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
  92. package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
  93. package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
  94. package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
  95. package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
  96. package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
  97. package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
  98. package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
  99. package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
  100. package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
  101. package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
  102. package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
  103. package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
  104. package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
  105. package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
  106. package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
  107. package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
  108. package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
  109. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
  110. package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
  111. package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
  112. package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
  113. package/scripts/runkit-contract/snapshot.mjs +340 -0
  114. package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
  115. package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
  116. package/scripts/runkit-contract/templates/finalize-request.json +18 -0
  117. package/scripts/runkit-contract/templates/goal-contract.json +11 -0
  118. package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
  119. package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
  120. package/scripts/runkit-contract/templates/resume-request.json +10 -0
  121. package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
  122. package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
  123. package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
  124. package/scripts/runkit-contract/templates/worker-lease.json +7 -0
  125. package/scripts/runkit-contract/verification-context.mjs +141 -0
  126. package/scripts/runkit-contract/verification-plan.mjs +608 -0
  127. package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
  128. package/scripts/runkit-contract/visual-smoke.mjs +410 -0
@@ -0,0 +1,324 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ realpathSync,
8
+ rmSync,
9
+ } from "node:fs";
10
+ import path from "node:path";
11
+
12
+ import {
13
+ RUNTIME_ROOT,
14
+ acceptedCloseoutVerificationIssues,
15
+ validateCoreArtifact,
16
+ validateExecutionPin,
17
+ validateLeaseOwnedPaths,
18
+ } from "./core-contract.mjs";
19
+ import {
20
+ assertAllowedKeys,
21
+ loadActiveExecution,
22
+ readJson,
23
+ relativeToWorkspace,
24
+ safeIdentifier,
25
+ writeJsonAtomically,
26
+ writeJsonExclusiveAtomically,
27
+ } from "./provenance-common.mjs";
28
+
29
+ const LEASE_KEYS = ["schemaVersion", "workItemId", "attempt", "ownedPaths", "state"];
30
+
31
+ function validateOwnedPatterns(paths) {
32
+ for (const ownedPath of paths) {
33
+ if (ownedPath.includes("*")
34
+ && (!ownedPath.endsWith("/**") || ownedPath.slice(0, -3).includes("*"))) {
35
+ throw new Error(`Lease owned path uses an unsupported wildcard: ${ownedPath}`);
36
+ }
37
+ }
38
+ return paths;
39
+ }
40
+
41
+ function within(root, candidate) {
42
+ const relative = path.relative(root, candidate);
43
+ return relative === ""
44
+ || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
45
+ }
46
+
47
+ function safeDirectory(root, candidate, label) {
48
+ const rootStat = lstatSync(root);
49
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
50
+ throw new Error(`${label} root must be a regular directory, not a symlink.`);
51
+ }
52
+ const candidateStat = lstatSync(candidate);
53
+ if (candidateStat.isSymbolicLink() || !candidateStat.isDirectory()) {
54
+ throw new Error(`${label} must be a regular directory, not a symlink.`);
55
+ }
56
+ const realRoot = realpathSync(root);
57
+ const realCandidate = realpathSync(candidate);
58
+ if (realCandidate !== path.resolve(candidate)) {
59
+ throw new Error(`${label} must be a real directory without symlink ancestors.`);
60
+ }
61
+ if (!within(realRoot, realCandidate)) {
62
+ throw new Error(`${label} escapes its artifact root.`);
63
+ }
64
+ return candidate;
65
+ }
66
+
67
+ function validateLease(value, label) {
68
+ assertAllowedKeys(value, label, LEASE_KEYS);
69
+ if (value.schemaVersion !== "OwlCodaRunKitWorkerLeaseV1") {
70
+ throw new Error(`${label} schemaVersion is invalid.`);
71
+ }
72
+ safeIdentifier(value.workItemId, `${label} workItemId`);
73
+ if (!Number.isInteger(value.attempt) || value.attempt < 1) {
74
+ throw new Error(`${label} attempt must be a positive integer.`);
75
+ }
76
+ const ownedPaths = validateOwnedPatterns(validateLeaseOwnedPaths(value.ownedPaths));
77
+ if (!new Set(["active", "released"]).has(value.state)) {
78
+ throw new Error(`${label} state must be active or released.`);
79
+ }
80
+ return { ...value, ownedPaths };
81
+ }
82
+
83
+ export function listLeaseArtifacts({ workspaceRoot, executionRoot }) {
84
+ const leasesRoot = safeDirectory(executionRoot, path.join(executionRoot, "leases"), "Lease directory");
85
+ const leases = [];
86
+ for (const entry of readdirSync(leasesRoot, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
87
+ if (!entry.name.endsWith(".json")) continue;
88
+ if (entry.isSymbolicLink()) throw new Error(`Lease artifact must not be a symlink: ${entry.name}`);
89
+ if (!entry.isFile()) throw new Error(`Lease artifact must be a regular file: ${entry.name}`);
90
+ const leasePath = path.join(leasesRoot, entry.name);
91
+ const stat = lstatSync(leasePath);
92
+ if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`Lease artifact must be a regular file: ${entry.name}`);
93
+ leases.push({
94
+ ...validateLease(JSON.parse(readFileSync(realpathSync(leasePath), "utf8")), `Lease ${entry.name}`),
95
+ leasePath,
96
+ leaseRelativePath: relativeToWorkspace(workspaceRoot, leasePath),
97
+ });
98
+ }
99
+ const workItems = leases.map(lease => lease.workItemId);
100
+ if (new Set(workItems).size !== workItems.length) {
101
+ throw new Error("Lease workItemId values must be unique inside an execution.");
102
+ }
103
+ return leases;
104
+ }
105
+
106
+ function executionRoots(workspaceRoot) {
107
+ const executionsRoot = path.join(workspaceRoot, RUNTIME_ROOT, "executions");
108
+ safeDirectory(path.join(workspaceRoot, RUNTIME_ROOT), executionsRoot, "Executions directory");
109
+ const executions = [];
110
+ for (const entry of readdirSync(executionsRoot, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
111
+ if (entry.isSymbolicLink()) throw new Error(`Execution directory must not be a symlink: ${entry.name}`);
112
+ if (!entry.isDirectory()) continue;
113
+ const executionRoot = path.join(executionsRoot, entry.name);
114
+ executions.push({ runId: entry.name, executionRoot });
115
+ }
116
+ return executions;
117
+ }
118
+
119
+ function hasTrustedCloseout(executionRoot, runId) {
120
+ const closeoutPath = path.join(executionRoot, "closeout-receipt.json");
121
+ if (!existsSync(closeoutPath)) return false;
122
+ try {
123
+ const closeoutStat = lstatSync(closeoutPath);
124
+ if (closeoutStat.isSymbolicLink() || !closeoutStat.isFile()) return false;
125
+ if (!within(realpathSync(executionRoot), realpathSync(closeoutPath))) return false;
126
+ const closeout = readJson(closeoutPath);
127
+ const artifactGate = validateCoreArtifact(closeout.artifact);
128
+ if (!artifactGate.valid
129
+ || closeout.acceptanceSha256 !== artifactGate.acceptanceSha256
130
+ || closeout.artifactSha256 !== artifactGate.artifactSha256) {
131
+ return false;
132
+ }
133
+ const payload = closeout.artifact.payload;
134
+ if (payload.runId !== runId
135
+ || !new Set(["accepted", "rejected", "blocked"]).has(payload.decision)
136
+ || payload.authorizationGranted !== false) {
137
+ return false;
138
+ }
139
+ if (acceptedCloseoutVerificationIssues(closeout.artifact).length > 0) return false;
140
+ const pinPath = path.join(executionRoot, "engine-pin.json");
141
+ const pinStat = lstatSync(pinPath);
142
+ if (pinStat.isSymbolicLink() || !pinStat.isFile()) return false;
143
+ return validateExecutionPin({
144
+ expected: readJson(pinPath),
145
+ actual: closeout.artifact.core,
146
+ }).status === "valid";
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function patternPrefix(value) {
153
+ return value.endsWith("/**") ? value.slice(0, -3) : null;
154
+ }
155
+
156
+ function covers(owner, candidate) {
157
+ if (owner === candidate) return true;
158
+ const prefix = patternPrefix(owner);
159
+ if (prefix === null) return false;
160
+ const candidatePrefix = patternPrefix(candidate) ?? candidate;
161
+ return candidatePrefix === prefix || candidatePrefix.startsWith(`${prefix}/`);
162
+ }
163
+
164
+ function overlappingPaths(left, right) {
165
+ const overlaps = [];
166
+ for (const leftPath of left) {
167
+ for (const rightPath of right) {
168
+ if (covers(leftPath, rightPath) || covers(rightPath, leftPath)) {
169
+ overlaps.push(`${leftPath} <> ${rightPath}`);
170
+ }
171
+ }
172
+ }
173
+ return overlaps.sort();
174
+ }
175
+
176
+ export function assertExecutionUnclosed(executionRoot, runId) {
177
+ if (existsSync(path.join(executionRoot, "closeout-receipt.json"))) {
178
+ throw new Error(`Cannot manage leases in closed execution: ${runId}`);
179
+ }
180
+ }
181
+
182
+ function acquireControlLock(workspaceRoot) {
183
+ const runtimeRoot = path.join(workspaceRoot, RUNTIME_ROOT);
184
+ safeDirectory(workspaceRoot, runtimeRoot, "RunKit runtime directory");
185
+ safeDirectory(runtimeRoot, path.join(runtimeRoot, "executions"), "Executions directory");
186
+ const lockPath = path.join(runtimeRoot, "control.lock");
187
+ try {
188
+ mkdirSync(lockPath);
189
+ } catch (error) {
190
+ if (error?.code === "EEXIST") throw new Error("Another RunKit control transaction is already active.");
191
+ throw error;
192
+ }
193
+ return () => rmSync(lockPath, { recursive: true, force: true });
194
+ }
195
+
196
+ export function withControlTransaction(workspaceRoot, operation) {
197
+ const release = acquireControlLock(workspaceRoot);
198
+ try {
199
+ return operation();
200
+ } finally {
201
+ release();
202
+ }
203
+ }
204
+
205
+ export function acquireLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId, ownedPaths }) {
206
+ safeIdentifier(workItemId, "work-item");
207
+ const normalizedPaths = validateOwnedPatterns(validateLeaseOwnedPaths(ownedPaths));
208
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
209
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
210
+ assertExecutionUnclosed(executionRoot, runId);
211
+ safeDirectory(path.join(workspaceRoot, RUNTIME_ROOT, "executions"), executionRoot, "Execution directory");
212
+ const currentLeases = listLeaseArtifacts({ workspaceRoot, executionRoot });
213
+ if (currentLeases.some(lease => lease.workItemId === workItemId)) {
214
+ throw new Error(`Lease work item already exists in execution: ${workItemId}`);
215
+ }
216
+ for (const execution of executionRoots(workspaceRoot)) {
217
+ if (hasTrustedCloseout(execution.executionRoot, execution.runId)) continue;
218
+ for (const lease of listLeaseArtifacts({ workspaceRoot, executionRoot: execution.executionRoot })) {
219
+ if (lease.state !== "active") continue;
220
+ const overlaps = overlappingPaths(normalizedPaths, lease.ownedPaths);
221
+ if (overlaps.length > 0) {
222
+ throw new Error(
223
+ `Owned paths overlap active lease ${execution.runId}/${lease.workItemId}: ${overlaps.join(", ")}`,
224
+ );
225
+ }
226
+ }
227
+ }
228
+ const lease = {
229
+ schemaVersion: "OwlCodaRunKitWorkerLeaseV1",
230
+ workItemId,
231
+ attempt: 1,
232
+ ownedPaths: normalizedPaths,
233
+ state: "active",
234
+ };
235
+ const leasePath = path.join(executionRoot, "leases", `${workItemId}-attempt-001.json`);
236
+ writeJsonExclusiveAtomically(leasePath, lease);
237
+ return {
238
+ status: "lease_acquired",
239
+ exitCode: 0,
240
+ runId,
241
+ leasePath: relativeToWorkspace(workspaceRoot, leasePath),
242
+ lease,
243
+ authorizationGranted: false,
244
+ };
245
+ }
246
+
247
+ export function acquireLease(args) {
248
+ return withControlTransaction(args.workspaceRoot, () => acquireLeaseWithinControlTransaction(args));
249
+ }
250
+
251
+ export function inspectLeases({ workspaceRoot, runId }) {
252
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
253
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
254
+ const leases = listLeaseArtifacts({ workspaceRoot, executionRoot }).map(lease => ({
255
+ workItemId: lease.workItemId,
256
+ attempt: lease.attempt,
257
+ ownedPaths: lease.ownedPaths,
258
+ state: lease.state,
259
+ leasePath: lease.leaseRelativePath,
260
+ }));
261
+ return {
262
+ status: "leases_inspected",
263
+ exitCode: 0,
264
+ runId,
265
+ leases,
266
+ authorizationGranted: false,
267
+ };
268
+ }
269
+
270
+ export function releaseLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId }) {
271
+ safeIdentifier(workItemId, "work-item");
272
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
273
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
274
+ assertExecutionUnclosed(executionRoot, runId);
275
+ const matches = listLeaseArtifacts({ workspaceRoot, executionRoot })
276
+ .filter(lease => lease.workItemId === workItemId);
277
+ if (matches.length !== 1) throw new Error(`Lease work item was not found: ${workItemId}`);
278
+ const [lease] = matches;
279
+ if (lease.state === "released") throw new Error(`Lease is already released: ${workItemId}`);
280
+ writeJsonAtomically(lease.leasePath, {
281
+ schemaVersion: lease.schemaVersion,
282
+ workItemId: lease.workItemId,
283
+ attempt: lease.attempt,
284
+ ownedPaths: lease.ownedPaths,
285
+ state: "released",
286
+ });
287
+ return {
288
+ status: "lease_released",
289
+ exitCode: 0,
290
+ runId,
291
+ leasePath: lease.leaseRelativePath,
292
+ authorizationGranted: false,
293
+ };
294
+ }
295
+
296
+ export function releaseLease(args) {
297
+ return withControlTransaction(args.workspaceRoot, () => releaseLeaseWithinControlTransaction(args));
298
+ }
299
+
300
+ export function restoreLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId }) {
301
+ safeIdentifier(workItemId, "work-item");
302
+ const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
303
+ if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
304
+ assertExecutionUnclosed(executionRoot, runId);
305
+ const matches = listLeaseArtifacts({ workspaceRoot, executionRoot })
306
+ .filter(lease => lease.workItemId === workItemId);
307
+ if (matches.length !== 1) throw new Error(`Lease work item was not found: ${workItemId}`);
308
+ const [lease] = matches;
309
+ if (lease.state !== "released") throw new Error(`Lease is not released: ${workItemId}`);
310
+ writeJsonAtomically(lease.leasePath, {
311
+ schemaVersion: lease.schemaVersion,
312
+ workItemId: lease.workItemId,
313
+ attempt: lease.attempt,
314
+ ownedPaths: lease.ownedPaths,
315
+ state: "active",
316
+ });
317
+ return {
318
+ status: "lease_restored",
319
+ exitCode: 0,
320
+ runId,
321
+ leasePath: lease.leaseRelativePath,
322
+ authorizationGranted: false,
323
+ };
324
+ }