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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +674 -0
- package/NOTICE.md +137 -0
- package/README.md +44 -0
- package/SOURCE.md +10 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
- package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
- package/package.json +24 -0
- package/packages/attest/LICENSE +674 -0
- package/packages/attest/README.md +63 -0
- package/packages/attest/cli/owlcoda-attest.mjs +155 -0
- package/packages/attest/consumer-surface-v1.json +255 -0
- package/packages/attest/distribution-manifest-v1.json +104 -0
- package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
- package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
- package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
- package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
- package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
- package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
- package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
- package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
- package/packages/attest/fixtures/index.json +90 -0
- package/packages/attest/sbom.spdx.json +33 -0
- package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
- package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
- package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
- package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
- package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
- package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
- package/packages/attest/src/canonical.mjs +12 -0
- package/packages/attest/src/formal.mjs +762 -0
- package/packages/attest/src/index.mjs +113 -0
- package/packages/attest/src/offline-bundle.mjs +108 -0
- package/packages/attest/src/quick-receipt-contract.mjs +156 -0
- package/packages/attest/src/quick.mjs +274 -0
- package/packages/attest/src/reference-contract.mjs +64 -0
- package/packages/attest/src/reference.mjs +131 -0
- package/packages/attest/src/workspace-snapshot.mjs +171 -0
- package/registry-release-policy-v1.json +34 -0
- package/sbom.spdx.json +39 -0
- package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
- package/scripts/runkit-contract/core-contract.mjs +392 -0
- package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
- package/scripts/runkit-contract/delivery-create.mjs +252 -0
- package/scripts/runkit-contract/delivery-selection.mjs +31 -0
- package/scripts/runkit-contract/finalize.mjs +790 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
- package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
- package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
- package/scripts/runkit-contract/offline-store.mjs +248 -0
- package/scripts/runkit-contract/profile-impact.mjs +223 -0
- package/scripts/runkit-contract/provenance-common.mjs +153 -0
- package/scripts/runkit-contract/quick-attest.mjs +4 -0
- package/scripts/runkit-contract/quick-canonical.mjs +41 -0
- package/scripts/runkit-contract/quick-metrics.mjs +86 -0
- package/scripts/runkit-contract/quick-receipt.mjs +139 -0
- package/scripts/runkit-contract/quick-verify.mjs +201 -0
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
- package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
- package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
- package/scripts/runkit-contract/repair-execution.mjs +997 -0
- package/scripts/runkit-contract/resource-preflight.mjs +829 -0
- package/scripts/runkit-contract/resume-execution.mjs +569 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
- package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
- package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
- package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
- package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
- package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
- package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
- package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
- package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
- package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
- package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
- package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
- package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
- package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
- package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
- package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
- package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
- package/scripts/runkit-contract/snapshot.mjs +340 -0
- package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
- package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
- package/scripts/runkit-contract/templates/finalize-request.json +18 -0
- package/scripts/runkit-contract/templates/goal-contract.json +11 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
- package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
- package/scripts/runkit-contract/templates/resume-request.json +10 -0
- package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
- package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
- package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
- package/scripts/runkit-contract/templates/worker-lease.json +7 -0
- package/scripts/runkit-contract/verification-context.mjs +141 -0
- package/scripts/runkit-contract/verification-plan.mjs +608 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
- package/scripts/runkit-contract/visual-smoke.mjs +410 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendFileSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
RUNTIME_ROOT,
|
|
18
|
+
currentCoreIdentity,
|
|
19
|
+
validateCoreArtifact,
|
|
20
|
+
validateExecutionPin,
|
|
21
|
+
validateLeaseOwnedPaths,
|
|
22
|
+
} from "./core-contract.mjs";
|
|
23
|
+
import { buildCoverageIndex } from "./coverage-adoption.mjs";
|
|
24
|
+
import { validateVerificationReceiptGate } from "./verification-receipt-gate.mjs";
|
|
25
|
+
import {
|
|
26
|
+
assertAllowedKeys,
|
|
27
|
+
readJson,
|
|
28
|
+
relativeToWorkspace,
|
|
29
|
+
safeIdentifier,
|
|
30
|
+
sha256,
|
|
31
|
+
writeJsonExclusive,
|
|
32
|
+
} from "./provenance-common.mjs";
|
|
33
|
+
|
|
34
|
+
const REQUEST_KEYS = [
|
|
35
|
+
"schemaVersion",
|
|
36
|
+
"resumeId",
|
|
37
|
+
"continuationRunId",
|
|
38
|
+
"reason",
|
|
39
|
+
"coverage",
|
|
40
|
+
];
|
|
41
|
+
const COVERAGE_KEYS = ["coverageId", "sources"];
|
|
42
|
+
|
|
43
|
+
function validateRequest(request) {
|
|
44
|
+
assertAllowedKeys(request, "Resume request", REQUEST_KEYS);
|
|
45
|
+
if (request.schemaVersion !== "OwlCodaRunKitResumeRequestV1") {
|
|
46
|
+
throw new Error("Unsupported resume request schemaVersion.");
|
|
47
|
+
}
|
|
48
|
+
safeIdentifier(request.resumeId, "resumeId");
|
|
49
|
+
if (request.continuationRunId !== null) {
|
|
50
|
+
safeIdentifier(request.continuationRunId, "continuationRunId");
|
|
51
|
+
}
|
|
52
|
+
if (typeof request.reason !== "string" || request.reason.length === 0) {
|
|
53
|
+
throw new Error("Resume request requires a non-empty reason.");
|
|
54
|
+
}
|
|
55
|
+
assertAllowedKeys(request.coverage, "Resume coverage", COVERAGE_KEYS);
|
|
56
|
+
safeIdentifier(request.coverage.coverageId, "coverageId");
|
|
57
|
+
if (!Array.isArray(request.coverage.sources)) {
|
|
58
|
+
throw new Error("Resume coverage requires sources.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function executionRoot(workspaceRoot, runId) {
|
|
63
|
+
return path.join(workspaceRoot, RUNTIME_ROOT, "executions", runId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isWithin(root, candidate) {
|
|
67
|
+
const remainder = path.relative(root, candidate);
|
|
68
|
+
return remainder === ""
|
|
69
|
+
|| (remainder !== ".." && !remainder.startsWith(`..${path.sep}`) && !path.isAbsolute(remainder));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertSafeDirectory(root, candidate, label, { create = false } = {}) {
|
|
73
|
+
const resolvedRoot = realpathSync(root);
|
|
74
|
+
if (!existsSync(candidate)) {
|
|
75
|
+
if (!create) throw new Error(`${label} does not exist.`);
|
|
76
|
+
const parent = path.dirname(candidate);
|
|
77
|
+
assertSafeDirectory(root, parent, `${label} parent`);
|
|
78
|
+
mkdirSync(candidate);
|
|
79
|
+
}
|
|
80
|
+
const stat = lstatSync(candidate);
|
|
81
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not be a symlink.`);
|
|
82
|
+
if (!stat.isDirectory()) throw new Error(`${label} must be a directory.`);
|
|
83
|
+
const resolved = realpathSync(candidate);
|
|
84
|
+
if (resolved !== path.resolve(candidate)) {
|
|
85
|
+
throw new Error(`${label} must not use symlink ancestors.`);
|
|
86
|
+
}
|
|
87
|
+
if (!isWithin(resolvedRoot, resolved)) throw new Error(`${label} escapes its artifact root.`);
|
|
88
|
+
return resolved;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function assertRegularArtifact(filePath, label) {
|
|
92
|
+
const stat = lstatSync(filePath);
|
|
93
|
+
if (stat.isSymbolicLink()) throw new Error(`${label} must not be a symlink.`);
|
|
94
|
+
if (!stat.isFile()) throw new Error(`${label} must be a regular file.`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function acquireResumeLock(workspaceRoot) {
|
|
98
|
+
const runtimeRoot = path.join(workspaceRoot, RUNTIME_ROOT);
|
|
99
|
+
assertSafeDirectory(workspaceRoot, runtimeRoot, "RunKit runtime root");
|
|
100
|
+
assertSafeDirectory(runtimeRoot, path.join(runtimeRoot, "executions"), "Executions directory");
|
|
101
|
+
const lockPath = path.join(runtimeRoot, "control.lock");
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(lockPath);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error?.code === "EEXIST") throw new Error("Another RunKit control transaction is already active.");
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
return () => rmSync(lockPath, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function activeRunIds(workspaceRoot) {
|
|
112
|
+
const root = path.join(workspaceRoot, RUNTIME_ROOT, "executions");
|
|
113
|
+
return readdirSync(root, { withFileTypes: true })
|
|
114
|
+
.filter(entry => entry.isDirectory())
|
|
115
|
+
.map(entry => entry.name)
|
|
116
|
+
.filter(runId => !existsSync(path.join(root, runId, "closeout-receipt.json")))
|
|
117
|
+
.sort();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertSoleActiveRun(workspaceRoot, sourceRunId) {
|
|
121
|
+
const active = activeRunIds(workspaceRoot);
|
|
122
|
+
if (active.length !== 1 || active[0] !== sourceRunId) {
|
|
123
|
+
throw new Error("Same-execution resume requires the source to be the only active execution.");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function assertNoOtherActiveRun(workspaceRoot) {
|
|
128
|
+
const active = activeRunIds(workspaceRoot);
|
|
129
|
+
if (active.length > 0) {
|
|
130
|
+
throw new Error(`Closed-run continuation requires no active execution: ${active.join(", ")}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertNoActiveWriterLease(sourceRoot) {
|
|
135
|
+
const leasesRoot = path.join(sourceRoot, "leases");
|
|
136
|
+
if (!existsSync(leasesRoot)) return;
|
|
137
|
+
for (const entry of readdirSync(leasesRoot, { withFileTypes: true })) {
|
|
138
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
139
|
+
if (entry.isSymbolicLink()) throw new Error(`Resume lease must not be a symlink: ${entry.name}`);
|
|
140
|
+
if (!entry.isFile()) throw new Error(`Resume lease must be a regular file: ${entry.name}`);
|
|
141
|
+
const leasePath = path.join(leasesRoot, entry.name);
|
|
142
|
+
assertRegularArtifact(leasePath, `Resume lease ${entry.name}`);
|
|
143
|
+
const lease = readJson(leasePath);
|
|
144
|
+
validateLeaseArtifact(lease, `Resume lease ${entry.name}`);
|
|
145
|
+
if (lease.state === "active") throw new Error(`Resume is not allowed while an active writer lease remains: ${lease.workItemId ?? entry.name}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateLeaseArtifact(lease, label) {
|
|
150
|
+
try {
|
|
151
|
+
if (lease?.schemaVersion !== "OwlCodaRunKitWorkerLeaseV1") {
|
|
152
|
+
throw new Error("schemaVersion is invalid");
|
|
153
|
+
}
|
|
154
|
+
if (typeof lease.workItemId !== "string" || lease.workItemId.length === 0) {
|
|
155
|
+
throw new Error("workItemId is required");
|
|
156
|
+
}
|
|
157
|
+
if (!Number.isInteger(lease.attempt) || lease.attempt < 1) {
|
|
158
|
+
throw new Error("attempt must be a positive integer");
|
|
159
|
+
}
|
|
160
|
+
validateLeaseOwnedPaths(lease.ownedPaths);
|
|
161
|
+
if (!new Set(["active", "released"]).has(lease.state)) {
|
|
162
|
+
throw new Error("state is invalid");
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(`${label} is not a valid lease: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function findNamedArtifacts(sourceRoot, searchRoot, name, label) {
|
|
170
|
+
if (!existsSync(searchRoot)) return [];
|
|
171
|
+
const found = [];
|
|
172
|
+
function visit(directory) {
|
|
173
|
+
assertSafeDirectory(sourceRoot, directory, label);
|
|
174
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
175
|
+
const candidate = path.join(directory, entry.name);
|
|
176
|
+
if (entry.isSymbolicLink()) throw new Error(`${label} must not contain symlinks.`);
|
|
177
|
+
if (entry.isDirectory()) visit(candidate);
|
|
178
|
+
else if (entry.isFile() && entry.name === name) {
|
|
179
|
+
assertRegularArtifact(candidate, `${label} artifact`);
|
|
180
|
+
found.push(candidate);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
visit(searchRoot);
|
|
185
|
+
return found.sort();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resumeIdOwners(workspaceRoot, resumeId) {
|
|
189
|
+
const executionsRoot = path.join(workspaceRoot, RUNTIME_ROOT, "executions");
|
|
190
|
+
const owners = [];
|
|
191
|
+
for (const entry of readdirSync(executionsRoot, { withFileTypes: true })) {
|
|
192
|
+
if (!entry.isDirectory()) continue;
|
|
193
|
+
const attemptPath = path.join(executionsRoot, entry.name, "resume-attempts", `${resumeId}.json`);
|
|
194
|
+
if (!existsSync(attemptPath)) continue;
|
|
195
|
+
assertRegularArtifact(attemptPath, `Resume attempt ${entry.name}/${resumeId}`);
|
|
196
|
+
owners.push(readJson(attemptPath));
|
|
197
|
+
}
|
|
198
|
+
return owners;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function assertResumeIdUnused(workspaceRoot, sourceRunId, resumeId) {
|
|
202
|
+
if (resumeIdOwners(workspaceRoot, resumeId).some(attempt => attempt.sourceRunId === sourceRunId)) {
|
|
203
|
+
throw new Error(`Resume id already exists for source execution: ${resumeId}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function acceptedVerificationIsComplete(verification) {
|
|
208
|
+
const selectedProfileIds = verification?.selectedProfileIds;
|
|
209
|
+
const releasedLeaseIds = verification?.releasedLeaseIds;
|
|
210
|
+
return verification?.contractVersion === "0.2"
|
|
211
|
+
&& verification.gateDecision === "accepted_passed"
|
|
212
|
+
&& /^[a-f0-9]{64}$/.test(verification.gateInputSha256 ?? "")
|
|
213
|
+
&& /^[a-f0-9]{64}$/.test(verification.activeReceiptSha256 ?? "")
|
|
214
|
+
&& /^[a-f0-9]{64}$/.test(verification.sourceFingerprint ?? "")
|
|
215
|
+
&& /^[a-f0-9]{64}$/.test(verification.verificationContextFingerprint ?? "")
|
|
216
|
+
&& verification.leaseState === "released"
|
|
217
|
+
&& Array.isArray(selectedProfileIds)
|
|
218
|
+
&& selectedProfileIds.length > 0
|
|
219
|
+
&& selectedProfileIds.every(value => typeof value === "string" && value.length > 0)
|
|
220
|
+
&& new Set(selectedProfileIds).size === selectedProfileIds.length
|
|
221
|
+
&& Array.isArray(releasedLeaseIds)
|
|
222
|
+
&& releasedLeaseIds.length > 0
|
|
223
|
+
&& releasedLeaseIds.every(value => typeof value === "string" && value.length > 0)
|
|
224
|
+
&& new Set(releasedLeaseIds).size === releasedLeaseIds.length;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function matchingGateInputs(sourceRoot, expectedSha256) {
|
|
228
|
+
const root = path.join(sourceRoot, "verification-receipts");
|
|
229
|
+
const matches = [];
|
|
230
|
+
for (const candidate of findNamedArtifacts(
|
|
231
|
+
sourceRoot,
|
|
232
|
+
root,
|
|
233
|
+
"verification-gate-input.json",
|
|
234
|
+
"Parent verification receipts directory",
|
|
235
|
+
)) {
|
|
236
|
+
const bytes = readFileSync(candidate);
|
|
237
|
+
if (sha256(bytes) === expectedSha256) matches.push({ path: candidate, bytes });
|
|
238
|
+
}
|
|
239
|
+
return matches;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function validateAcceptedParentEvidence(sourceRoot, sourceRunId, verification) {
|
|
243
|
+
if (!acceptedVerificationIsComplete(verification)) {
|
|
244
|
+
throw new Error("Parent accepted closeout lacks complete verification evidence.");
|
|
245
|
+
}
|
|
246
|
+
const gateMatches = matchingGateInputs(sourceRoot, verification.gateInputSha256);
|
|
247
|
+
if (gateMatches.length !== 1) {
|
|
248
|
+
throw new Error("Parent accepted closeout must bind exactly one preserved verification gate input.");
|
|
249
|
+
}
|
|
250
|
+
const gateInput = JSON.parse(gateMatches[0].bytes.toString("utf8"));
|
|
251
|
+
const gate = validateVerificationReceiptGate(gateInput);
|
|
252
|
+
if (!gate.accepted
|
|
253
|
+
|| gate.contractVersion !== "0.2"
|
|
254
|
+
|| gate.lineage.active.receipt.runId !== sourceRunId
|
|
255
|
+
|| gate.activeReceiptSha256 !== verification.activeReceiptSha256
|
|
256
|
+
|| gate.sourceFingerprint !== verification.sourceFingerprint
|
|
257
|
+
|| gate.verificationContextFingerprint !== verification.verificationContextFingerprint
|
|
258
|
+
|| JSON.stringify([...gate.selectedProfileIds].sort()) !== JSON.stringify([...verification.selectedProfileIds].sort())) {
|
|
259
|
+
throw new Error("Parent accepted closeout does not match its preserved verification gate.");
|
|
260
|
+
}
|
|
261
|
+
const leasesRoot = path.join(sourceRoot, "leases");
|
|
262
|
+
assertSafeDirectory(sourceRoot, leasesRoot, "Parent leases directory");
|
|
263
|
+
const leaseIds = readdirSync(leasesRoot, { withFileTypes: true })
|
|
264
|
+
.filter(entry => entry.name.endsWith(".json"))
|
|
265
|
+
.map(entry => {
|
|
266
|
+
if (entry.isSymbolicLink()) throw new Error(`Parent lease must not be a symlink: ${entry.name}`);
|
|
267
|
+
if (!entry.isFile()) throw new Error(`Parent lease must be a regular file: ${entry.name}`);
|
|
268
|
+
const leasePath = path.join(leasesRoot, entry.name);
|
|
269
|
+
assertRegularArtifact(leasePath, `Parent lease ${entry.name}`);
|
|
270
|
+
const lease = readJson(leasePath);
|
|
271
|
+
validateLeaseArtifact(lease, `Parent lease ${entry.name}`);
|
|
272
|
+
if (lease.state !== "released") {
|
|
273
|
+
throw new Error(`Parent lease is not a valid released lease: ${entry.name}`);
|
|
274
|
+
}
|
|
275
|
+
return lease.workItemId;
|
|
276
|
+
})
|
|
277
|
+
.sort();
|
|
278
|
+
if (new Set(leaseIds).size !== leaseIds.length
|
|
279
|
+
|| JSON.stringify(leaseIds) !== JSON.stringify([...verification.releasedLeaseIds].sort())) {
|
|
280
|
+
throw new Error("Parent accepted closeout released leases do not match preserved lease artifacts.");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function validatedParentCloseout(workspaceRoot, sourceRoot, sourceRunId) {
|
|
285
|
+
const closeoutPath = path.join(sourceRoot, "closeout-receipt.json");
|
|
286
|
+
assertRegularArtifact(closeoutPath, "Parent closeout");
|
|
287
|
+
const closeoutBytes = readFileSync(closeoutPath);
|
|
288
|
+
const closeout = JSON.parse(closeoutBytes.toString("utf8"));
|
|
289
|
+
const artifactGate = validateCoreArtifact(closeout.artifact);
|
|
290
|
+
if (!artifactGate.valid) throw new Error(`Parent closeout is invalid: ${artifactGate.issues.join("; ")}`);
|
|
291
|
+
if (closeout.acceptanceSha256 !== artifactGate.acceptanceSha256
|
|
292
|
+
|| closeout.artifactSha256 !== artifactGate.artifactSha256) {
|
|
293
|
+
throw new Error("Parent closeout hashes do not match its artifact.");
|
|
294
|
+
}
|
|
295
|
+
const payload = closeout.artifact.payload;
|
|
296
|
+
if (payload.runId !== sourceRunId
|
|
297
|
+
|| !new Set(["accepted", "rejected", "blocked"]).has(payload.decision)
|
|
298
|
+
|| payload.authorizationGranted !== false) {
|
|
299
|
+
throw new Error("Parent closeout payload is invalid.");
|
|
300
|
+
}
|
|
301
|
+
if (payload.decision === "accepted") validateAcceptedParentEvidence(sourceRoot, sourceRunId, payload.verification);
|
|
302
|
+
const pin = readJson(path.join(sourceRoot, "engine-pin.json"));
|
|
303
|
+
const pinGate = validateExecutionPin({ expected: pin, actual: closeout.artifact.core });
|
|
304
|
+
if (pinGate.status !== "valid") throw new Error(`Parent closeout does not match its engine pin: ${pinGate.issues.join("; ")}`);
|
|
305
|
+
return {
|
|
306
|
+
decision: payload.decision,
|
|
307
|
+
path: relativeToWorkspace(workspaceRoot, closeoutPath),
|
|
308
|
+
sha256: sha256(closeoutBytes),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function sourceGoal(workspaceRoot, sourceRoot) {
|
|
313
|
+
const goalPath = path.join(sourceRoot, "goal-contract.json");
|
|
314
|
+
assertRegularArtifact(goalPath, "Source goal contract");
|
|
315
|
+
const bytes = readFileSync(goalPath);
|
|
316
|
+
return {
|
|
317
|
+
bytes,
|
|
318
|
+
descriptor: {
|
|
319
|
+
path: relativeToWorkspace(workspaceRoot, goalPath),
|
|
320
|
+
sha256: sha256(bytes),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function coverageRequest(request) {
|
|
326
|
+
return {
|
|
327
|
+
schemaVersion: "OwlCodaRunKitCoverageAdoptRequestV1",
|
|
328
|
+
coverageId: request.coverage.coverageId,
|
|
329
|
+
sources: request.coverage.sources,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function coverageDescriptor(workspaceRoot, targetRoot, targetRunId, request, coverageIndex) {
|
|
334
|
+
const outputPath = path.join(targetRoot, "coverage-indexes", `${request.coverage.coverageId}.json`);
|
|
335
|
+
const bytes = Buffer.from(`${JSON.stringify(coverageIndex, null, 2)}\n`);
|
|
336
|
+
return {
|
|
337
|
+
outputPath,
|
|
338
|
+
relativePath: `.owlcoda/runkit/executions/${targetRunId}/coverage-indexes/${request.coverage.coverageId}.json`,
|
|
339
|
+
sha256: sha256(bytes),
|
|
340
|
+
bytes,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function attemptArtifact({
|
|
345
|
+
request,
|
|
346
|
+
mode,
|
|
347
|
+
sourceRunId,
|
|
348
|
+
targetRunId,
|
|
349
|
+
parentGoal,
|
|
350
|
+
parentCloseout,
|
|
351
|
+
coverage,
|
|
352
|
+
coverageIndex,
|
|
353
|
+
}) {
|
|
354
|
+
return {
|
|
355
|
+
schemaVersion: "OwlCodaRunKitResumeAttemptV1",
|
|
356
|
+
resumeId: request.resumeId,
|
|
357
|
+
mode,
|
|
358
|
+
runId: targetRunId,
|
|
359
|
+
sourceRunId,
|
|
360
|
+
reason: request.reason,
|
|
361
|
+
parentGoal,
|
|
362
|
+
parentCloseout,
|
|
363
|
+
inheritedEvidence: {
|
|
364
|
+
coverageIndexPath: coverage.relativePath,
|
|
365
|
+
coverageIndexSha256: coverage.sha256,
|
|
366
|
+
reusableReceiptIds: coverageIndex.entries.map(entry => entry.receiptId).sort(),
|
|
367
|
+
},
|
|
368
|
+
nextAllowedAction: "acquire_writer_lease",
|
|
369
|
+
requiredWorkflow: [
|
|
370
|
+
"acquire_writer_lease",
|
|
371
|
+
"prepare_or_replace_delivery_packet",
|
|
372
|
+
"verify_plan",
|
|
373
|
+
],
|
|
374
|
+
authorizationGranted: false,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function appendResumeEvent(targetRoot, event) {
|
|
379
|
+
const eventsPath = path.join(targetRoot, "events.jsonl");
|
|
380
|
+
assertRegularArtifact(eventsPath, "Execution events artifact");
|
|
381
|
+
const existing = readFileSync(eventsPath, "utf8").trim().split("\n").filter(Boolean);
|
|
382
|
+
appendFileSync(eventsPath, `${JSON.stringify({
|
|
383
|
+
sequence: existing.length + 1,
|
|
384
|
+
type: "execution_resumed",
|
|
385
|
+
...event,
|
|
386
|
+
authorizationGranted: false,
|
|
387
|
+
})}\n`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function appendSameExecution({ workspaceRoot, sourceRunId, sourceRoot, request, goal }) {
|
|
391
|
+
if (request.continuationRunId !== null) {
|
|
392
|
+
throw new Error("Unclosed execution resume requires continuationRunId=null.");
|
|
393
|
+
}
|
|
394
|
+
assertSoleActiveRun(workspaceRoot, sourceRunId);
|
|
395
|
+
const pin = readJson(path.join(sourceRoot, "engine-pin.json"));
|
|
396
|
+
const pinGate = validateExecutionPin({ expected: pin, actual: currentCoreIdentity() });
|
|
397
|
+
if (pinGate.status !== "valid") throw new Error(`Resume engine pin is stale: ${pinGate.issues.join("; ")}`);
|
|
398
|
+
assertNoActiveWriterLease(sourceRoot);
|
|
399
|
+
if (findNamedArtifacts(
|
|
400
|
+
sourceRoot,
|
|
401
|
+
path.join(sourceRoot, "verification-receipts"),
|
|
402
|
+
"receipt-lineage.json",
|
|
403
|
+
"Resume verification receipts directory",
|
|
404
|
+
).length > 0) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
"An execution with a finalized receipt lineage must close before resuming as a continuation.",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const eventsPath = path.join(sourceRoot, "events.jsonl");
|
|
410
|
+
assertRegularArtifact(eventsPath, "Execution events artifact");
|
|
411
|
+
const originalEvents = readFileSync(eventsPath);
|
|
412
|
+
const coverageRoot = path.join(sourceRoot, "coverage-indexes");
|
|
413
|
+
const attemptsRoot = path.join(sourceRoot, "resume-attempts");
|
|
414
|
+
assertSafeDirectory(sourceRoot, coverageRoot, "Resume coverage directory", { create: true });
|
|
415
|
+
assertSafeDirectory(sourceRoot, attemptsRoot, "Resume attempts directory", { create: true });
|
|
416
|
+
const attemptPath = path.join(sourceRoot, "resume-attempts", `${request.resumeId}.json`);
|
|
417
|
+
const coverageIndex = buildCoverageIndex({
|
|
418
|
+
workspaceRoot,
|
|
419
|
+
runId: sourceRunId,
|
|
420
|
+
request: coverageRequest(request),
|
|
421
|
+
});
|
|
422
|
+
const coverage = coverageDescriptor(workspaceRoot, sourceRoot, sourceRunId, request, coverageIndex);
|
|
423
|
+
if (existsSync(attemptPath)) throw new Error(`Resume attempt already exists: ${request.resumeId}`);
|
|
424
|
+
if (existsSync(coverage.outputPath)) throw new Error(`Resume coverage index already exists: ${request.coverage.coverageId}`);
|
|
425
|
+
const attempt = attemptArtifact({
|
|
426
|
+
request,
|
|
427
|
+
mode: "same_execution",
|
|
428
|
+
sourceRunId,
|
|
429
|
+
targetRunId: sourceRunId,
|
|
430
|
+
parentGoal: goal.descriptor,
|
|
431
|
+
parentCloseout: null,
|
|
432
|
+
coverage,
|
|
433
|
+
coverageIndex,
|
|
434
|
+
});
|
|
435
|
+
let coverageWritten = false;
|
|
436
|
+
let attemptWritten = false;
|
|
437
|
+
try {
|
|
438
|
+
writeJsonExclusive(coverage.outputPath, coverageIndex);
|
|
439
|
+
coverageWritten = true;
|
|
440
|
+
writeJsonExclusive(attemptPath, attempt);
|
|
441
|
+
attemptWritten = true;
|
|
442
|
+
assertNoActiveWriterLease(sourceRoot);
|
|
443
|
+
appendResumeEvent(sourceRoot, { runId: sourceRunId, sourceRunId, resumeId: request.resumeId });
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (attemptWritten) rmSync(attemptPath, { force: true });
|
|
446
|
+
if (coverageWritten) rmSync(coverage.outputPath, { force: true });
|
|
447
|
+
if (existsSync(eventsPath) && lstatSync(eventsPath).isFile()) {
|
|
448
|
+
writeFileSync(eventsPath, originalEvents);
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`Resume transaction failed while updating execution events or artifacts: ${error instanceof Error ? error.message : String(error)}`);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
status: "resume_attempt_appended",
|
|
454
|
+
exitCode: 0,
|
|
455
|
+
runId: sourceRunId,
|
|
456
|
+
sourceRunId,
|
|
457
|
+
attemptPath: relativeToWorkspace(workspaceRoot, attemptPath),
|
|
458
|
+
coverageIndexPath: coverage.relativePath,
|
|
459
|
+
coverageIndexSha256: coverage.sha256,
|
|
460
|
+
authorizationGranted: false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function createContinuation({ workspaceRoot, sourceRunId, sourceRoot, request, goal }) {
|
|
465
|
+
if (request.continuationRunId === null) {
|
|
466
|
+
throw new Error("Closed execution resume requires continuationRunId.");
|
|
467
|
+
}
|
|
468
|
+
const targetRunId = request.continuationRunId;
|
|
469
|
+
if (targetRunId === sourceRunId) throw new Error("Continuation run id must differ from the source run id.");
|
|
470
|
+
assertNoOtherActiveRun(workspaceRoot);
|
|
471
|
+
const targetRoot = executionRoot(workspaceRoot, targetRunId);
|
|
472
|
+
if (existsSync(targetRoot)) throw new Error(`Continuation run id already exists: ${targetRunId}`);
|
|
473
|
+
const parentCloseout = validatedParentCloseout(workspaceRoot, sourceRoot, sourceRunId);
|
|
474
|
+
const coverageIndex = buildCoverageIndex({
|
|
475
|
+
workspaceRoot,
|
|
476
|
+
runId: targetRunId,
|
|
477
|
+
request: coverageRequest(request),
|
|
478
|
+
});
|
|
479
|
+
const coverage = coverageDescriptor(workspaceRoot, targetRoot, targetRunId, request, coverageIndex);
|
|
480
|
+
const attemptRelativePath = `.owlcoda/runkit/executions/${targetRunId}/resume-attempts/${request.resumeId}.json`;
|
|
481
|
+
const attempt = attemptArtifact({
|
|
482
|
+
request,
|
|
483
|
+
mode: "continuation",
|
|
484
|
+
sourceRunId,
|
|
485
|
+
targetRunId,
|
|
486
|
+
parentGoal: goal.descriptor,
|
|
487
|
+
parentCloseout,
|
|
488
|
+
coverage,
|
|
489
|
+
coverageIndex,
|
|
490
|
+
});
|
|
491
|
+
const enginePin = currentCoreIdentity();
|
|
492
|
+
const runtimeRoot = path.join(workspaceRoot, RUNTIME_ROOT);
|
|
493
|
+
const executionsRoot = path.join(runtimeRoot, "executions");
|
|
494
|
+
assertSafeDirectory(runtimeRoot, executionsRoot, "RunKit executions directory");
|
|
495
|
+
const stagingParent = path.join(runtimeRoot, "resume-staging");
|
|
496
|
+
assertSafeDirectory(runtimeRoot, stagingParent, "Resume staging directory", { create: true });
|
|
497
|
+
const stagingRoot = mkdtempSync(path.join(stagingParent, `${targetRunId}-`));
|
|
498
|
+
let stagingPresent = true;
|
|
499
|
+
try {
|
|
500
|
+
mkdirSync(path.join(stagingRoot, "leases"));
|
|
501
|
+
mkdirSync(path.join(stagingRoot, "delivery-packets"));
|
|
502
|
+
mkdirSync(path.join(stagingRoot, "verification-receipts"));
|
|
503
|
+
writeJsonExclusive(path.join(stagingRoot, "engine-pin.json"), enginePin);
|
|
504
|
+
writeFileSync(path.join(stagingRoot, "goal-contract.json"), goal.bytes, { flag: "wx" });
|
|
505
|
+
writeJsonExclusive(path.join(stagingRoot, "execution-plan.json"), {
|
|
506
|
+
schemaVersion: "OwlCodaRunKitExecutionPlanV1",
|
|
507
|
+
runId: targetRunId,
|
|
508
|
+
state: "planned",
|
|
509
|
+
enginePin,
|
|
510
|
+
continuation: {
|
|
511
|
+
parentRunId: sourceRunId,
|
|
512
|
+
resumeId: request.resumeId,
|
|
513
|
+
attemptPath: attemptRelativePath,
|
|
514
|
+
},
|
|
515
|
+
authorizationGranted: false,
|
|
516
|
+
});
|
|
517
|
+
writeJsonExclusive(
|
|
518
|
+
path.join(stagingRoot, "coverage-indexes", `${request.coverage.coverageId}.json`),
|
|
519
|
+
coverageIndex,
|
|
520
|
+
);
|
|
521
|
+
writeJsonExclusive(path.join(stagingRoot, "resume-attempts", `${request.resumeId}.json`), attempt);
|
|
522
|
+
writeFileSync(path.join(stagingRoot, "events.jsonl"), [
|
|
523
|
+
JSON.stringify({ sequence: 1, type: "execution_planned", runId: targetRunId, authorizationGranted: false }),
|
|
524
|
+
JSON.stringify({
|
|
525
|
+
sequence: 2,
|
|
526
|
+
type: "execution_resumed",
|
|
527
|
+
runId: targetRunId,
|
|
528
|
+
sourceRunId,
|
|
529
|
+
resumeId: request.resumeId,
|
|
530
|
+
authorizationGranted: false,
|
|
531
|
+
}),
|
|
532
|
+
"",
|
|
533
|
+
].join("\n"), { flag: "wx" });
|
|
534
|
+
renameSync(stagingRoot, targetRoot);
|
|
535
|
+
stagingPresent = false;
|
|
536
|
+
} finally {
|
|
537
|
+
if (stagingPresent) rmSync(stagingRoot, { recursive: true, force: true });
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
status: "continuation_created",
|
|
541
|
+
exitCode: 0,
|
|
542
|
+
runId: targetRunId,
|
|
543
|
+
sourceRunId,
|
|
544
|
+
attemptPath: attemptRelativePath,
|
|
545
|
+
coverageIndexPath: coverage.relativePath,
|
|
546
|
+
coverageIndexSha256: coverage.sha256,
|
|
547
|
+
authorizationGranted: false,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function runResumeExecution({ workspaceRoot, sourceRunId, request }) {
|
|
552
|
+
safeIdentifier(sourceRunId, "sourceRunId");
|
|
553
|
+
validateRequest(request);
|
|
554
|
+
const releaseLock = acquireResumeLock(workspaceRoot);
|
|
555
|
+
try {
|
|
556
|
+
const executionsRoot = path.join(workspaceRoot, RUNTIME_ROOT, "executions");
|
|
557
|
+
assertSafeDirectory(path.join(workspaceRoot, RUNTIME_ROOT), executionsRoot, "RunKit executions directory");
|
|
558
|
+
const sourceRoot = executionRoot(workspaceRoot, sourceRunId);
|
|
559
|
+
if (!existsSync(sourceRoot)) throw new Error(`Source execution does not exist: ${sourceRunId}`);
|
|
560
|
+
assertSafeDirectory(executionsRoot, sourceRoot, "Source execution directory");
|
|
561
|
+
assertResumeIdUnused(workspaceRoot, sourceRunId, request.resumeId);
|
|
562
|
+
const goal = sourceGoal(workspaceRoot, sourceRoot);
|
|
563
|
+
return existsSync(path.join(sourceRoot, "closeout-receipt.json"))
|
|
564
|
+
? createContinuation({ workspaceRoot, sourceRunId, sourceRoot, request, goal })
|
|
565
|
+
: appendSameExecution({ workspaceRoot, sourceRunId, sourceRoot, request, goal });
|
|
566
|
+
} finally {
|
|
567
|
+
releaseLock();
|
|
568
|
+
}
|
|
569
|
+
}
|