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,414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
lstatSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
realpathSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
import { RUNTIME_ROOT } from "./core-contract.mjs";
|
|
11
|
+
import {
|
|
12
|
+
assertAllowedKeys,
|
|
13
|
+
loadActiveExecution,
|
|
14
|
+
readJson,
|
|
15
|
+
relativeToWorkspace,
|
|
16
|
+
resolveExistingArtifact,
|
|
17
|
+
safeIdentifier,
|
|
18
|
+
safeRelativePath,
|
|
19
|
+
sha256,
|
|
20
|
+
writeJsonExclusive,
|
|
21
|
+
} from "./provenance-common.mjs";
|
|
22
|
+
import { validateVerificationReceiptGate } from "./verification-receipt-gate.mjs";
|
|
23
|
+
|
|
24
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
25
|
+
const REQUEST_KEYS = ["schemaVersion", "coverageId", "sources"];
|
|
26
|
+
const SOURCE_KEYS = [
|
|
27
|
+
"gateInputPath",
|
|
28
|
+
"gateInputSha256",
|
|
29
|
+
"commandBindings",
|
|
30
|
+
"dependencyBindings",
|
|
31
|
+
];
|
|
32
|
+
const COMMAND_BINDING_KEYS = ["receiptCommandId", "profileId", "commandId"];
|
|
33
|
+
const DEPENDENCY_BINDING_KEYS = ["dependencyId", "source"];
|
|
34
|
+
const DEPENDENCY_SOURCE_KEYS = ["kind", "identity"];
|
|
35
|
+
|
|
36
|
+
function ensureCoverageDirectory(executionRoot) {
|
|
37
|
+
const executionStat = lstatSync(executionRoot);
|
|
38
|
+
if (executionStat.isSymbolicLink() || !executionStat.isDirectory()) {
|
|
39
|
+
throw new Error("Coverage execution root must be a regular directory, not a symlink.");
|
|
40
|
+
}
|
|
41
|
+
const resolvedExecution = realpathSync(executionRoot);
|
|
42
|
+
const coverageRoot = path.join(executionRoot, "coverage-indexes");
|
|
43
|
+
if (!existsSync(coverageRoot)) mkdirSync(coverageRoot);
|
|
44
|
+
const coverageStat = lstatSync(coverageRoot);
|
|
45
|
+
if (coverageStat.isSymbolicLink() || !coverageStat.isDirectory()) {
|
|
46
|
+
throw new Error("Coverage output directory must be a regular directory, not a symlink.");
|
|
47
|
+
}
|
|
48
|
+
const resolvedCoverage = realpathSync(coverageRoot);
|
|
49
|
+
const relative = path.relative(resolvedExecution, resolvedCoverage);
|
|
50
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
51
|
+
throw new Error("Coverage output directory escapes its execution root.");
|
|
52
|
+
}
|
|
53
|
+
return coverageRoot;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function expectedSha256(value, label) {
|
|
61
|
+
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
62
|
+
throw new Error(`${label} must be a lowercase SHA-256.`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sortedUniqueStrings(values) {
|
|
68
|
+
return [...new Set(values)].sort();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateProfileRule(rule) {
|
|
72
|
+
if (typeof rule !== "string" || rule.length === 0) {
|
|
73
|
+
throw new Error("Profile path rules must be non-empty strings.");
|
|
74
|
+
}
|
|
75
|
+
const wildcardIndex = rule.indexOf("*");
|
|
76
|
+
const prefix = wildcardIndex === -1
|
|
77
|
+
? rule
|
|
78
|
+
: rule.endsWith("/**") && !rule.slice(0, -3).includes("*")
|
|
79
|
+
? rule.slice(0, -3)
|
|
80
|
+
: null;
|
|
81
|
+
if (prefix === null) throw new Error(`Unsupported profile path rule: ${rule}`);
|
|
82
|
+
const safePrefix = safeRelativePath(prefix, "Profile path rule");
|
|
83
|
+
if (safePrefix === RUNTIME_ROOT || safePrefix.startsWith(`${RUNTIME_ROOT}/`)) {
|
|
84
|
+
throw new Error("Profile path rule uses the reserved RunKit runtime.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateProfileDocument(value) {
|
|
89
|
+
if (!isRecord(value)
|
|
90
|
+
|| value.schemaVersion !== "OwlCodaRunKitProfilesV1"
|
|
91
|
+
|| !Array.isArray(value.profiles)) {
|
|
92
|
+
throw new Error("Project profiles artifact is invalid.");
|
|
93
|
+
}
|
|
94
|
+
const profiles = new Map();
|
|
95
|
+
for (const profile of value.profiles) {
|
|
96
|
+
if (!isRecord(profile)
|
|
97
|
+
|| typeof profile.id !== "string"
|
|
98
|
+
|| profile.id.length === 0
|
|
99
|
+
|| !Array.isArray(profile.paths)
|
|
100
|
+
|| profile.paths.length === 0) {
|
|
101
|
+
throw new Error("Each project profile requires id and paths.");
|
|
102
|
+
}
|
|
103
|
+
if (profiles.has(profile.id)) throw new Error(`Project profile ids must be unique: ${profile.id}`);
|
|
104
|
+
for (const rule of profile.paths) validateProfileRule(rule);
|
|
105
|
+
if (profile.commands !== undefined) {
|
|
106
|
+
if (!Array.isArray(profile.commands)) throw new Error(`Profile commands must be an array: ${profile.id}`);
|
|
107
|
+
const commandIds = new Set();
|
|
108
|
+
for (const command of profile.commands) {
|
|
109
|
+
if (!isRecord(command)
|
|
110
|
+
|| typeof command.id !== "string"
|
|
111
|
+
|| command.id.length === 0
|
|
112
|
+
|| typeof command.executable !== "string"
|
|
113
|
+
|| command.executable.length === 0
|
|
114
|
+
|| !Array.isArray(command.argv)
|
|
115
|
+
|| command.argv.some(argument => typeof argument !== "string" || argument.length === 0)) {
|
|
116
|
+
throw new Error(`Profile command is invalid: ${profile.id}`);
|
|
117
|
+
}
|
|
118
|
+
safeRelativePath(command.cwd, "Profile command cwd", { allowDot: true });
|
|
119
|
+
if (commandIds.has(command.id)) {
|
|
120
|
+
throw new Error(`Profile command ids must be unique: ${profile.id}/${command.id}`);
|
|
121
|
+
}
|
|
122
|
+
commandIds.add(command.id);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
profiles.set(profile.id, profile);
|
|
126
|
+
}
|
|
127
|
+
return profiles;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matchesRule(filePath, rule) {
|
|
131
|
+
if (rule.endsWith("/**")) return filePath.startsWith(`${rule.slice(0, -3)}/`);
|
|
132
|
+
return filePath === rule;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function launcherMatches(expected, actual) {
|
|
136
|
+
if (expected === actual) return true;
|
|
137
|
+
if (expected.includes("/") || expected.includes("\\")) return false;
|
|
138
|
+
return path.basename(actual) === expected || path.win32.basename(actual) === expected;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function commandForBinding({ binding, receipt, profiles }) {
|
|
142
|
+
const profile = profiles.get(binding.profileId);
|
|
143
|
+
if (!profile) throw new Error(`Coverage profile does not exist: ${binding.profileId}`);
|
|
144
|
+
if (!receipt.selectedProfileIds.includes(binding.profileId)) {
|
|
145
|
+
throw new Error(`Coverage profile was not selected by the accepted receipt: ${binding.profileId}`);
|
|
146
|
+
}
|
|
147
|
+
const commands = Array.isArray(profile.commands) ? profile.commands : [];
|
|
148
|
+
const command = commands.find(candidate => candidate?.id === binding.commandId);
|
|
149
|
+
if (!command) {
|
|
150
|
+
throw new Error(`Coverage command is not owned by profile ${binding.profileId}: ${binding.commandId}`);
|
|
151
|
+
}
|
|
152
|
+
const commandReceipt = receipt.commandReceipts.find(candidate => candidate?.id === binding.receiptCommandId);
|
|
153
|
+
if (!commandReceipt) throw new Error(`Receipt command does not exist: ${binding.receiptCommandId}`);
|
|
154
|
+
if (commandReceipt.exitCode !== 0) {
|
|
155
|
+
throw new Error(`Receipt command did not pass: ${binding.receiptCommandId}`);
|
|
156
|
+
}
|
|
157
|
+
const evidence = commandReceipt.evidence;
|
|
158
|
+
const exactArgv = [evidence?.launcher?.executable, ...(command.argv ?? [])];
|
|
159
|
+
if (evidence?.cwd !== command.cwd
|
|
160
|
+
|| !launcherMatches(command.executable, evidence?.launcher?.executable ?? "")
|
|
161
|
+
|| JSON.stringify(evidence?.argv) !== JSON.stringify(exactArgv)) {
|
|
162
|
+
throw new Error(`Exact command evidence mismatch: ${binding.receiptCommandId}`);
|
|
163
|
+
}
|
|
164
|
+
return { profile, command, evidence };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function materialInputs(boundCommands) {
|
|
168
|
+
const inputs = new Map();
|
|
169
|
+
for (const { evidence } of boundCommands) {
|
|
170
|
+
for (const material of evidence.materialInputs ?? []) {
|
|
171
|
+
const previous = inputs.get(material.id);
|
|
172
|
+
if (previous && previous !== material.sha256) {
|
|
173
|
+
throw new Error(`Material input has conflicting hashes: ${material.id}`);
|
|
174
|
+
}
|
|
175
|
+
inputs.set(material.id, material.sha256);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return inputs;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sourceFilesForBindings(boundCommands, inputs) {
|
|
182
|
+
const sourceFiles = {};
|
|
183
|
+
for (const [materialId, hash] of [...inputs.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
184
|
+
const covered = boundCommands.some(({ profile }) => profile.paths.some(rule => matchesRule(materialId, rule)));
|
|
185
|
+
if (!covered) continue;
|
|
186
|
+
const sourcePath = safeRelativePath(materialId, "Receipt material source path");
|
|
187
|
+
if (sourcePath === RUNTIME_ROOT || sourcePath.startsWith(`${RUNTIME_ROOT}/`)) {
|
|
188
|
+
throw new Error("Receipt material source path uses the reserved RunKit runtime.");
|
|
189
|
+
}
|
|
190
|
+
sourceFiles[sourcePath] = expectedSha256(hash, "Receipt material source hash");
|
|
191
|
+
}
|
|
192
|
+
if (Object.keys(sourceFiles).length === 0) {
|
|
193
|
+
throw new Error("Coverage adoption found no profile-owned source material inputs.");
|
|
194
|
+
}
|
|
195
|
+
return sourceFiles;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dependencySha256ForBindings(bindings, context, inputs) {
|
|
199
|
+
const result = {};
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
for (const binding of bindings) {
|
|
202
|
+
assertAllowedKeys(binding, "Dependency binding", DEPENDENCY_BINDING_KEYS);
|
|
203
|
+
safeIdentifier(binding.dependencyId, "dependencyId");
|
|
204
|
+
if (seen.has(binding.dependencyId)) {
|
|
205
|
+
throw new Error(`Dependency bindings must be unique: ${binding.dependencyId}`);
|
|
206
|
+
}
|
|
207
|
+
seen.add(binding.dependencyId);
|
|
208
|
+
if (!isRecord(binding.source)) throw new Error("Dependency binding source must be an object.");
|
|
209
|
+
assertAllowedKeys(binding.source, "Dependency binding source", DEPENDENCY_SOURCE_KEYS);
|
|
210
|
+
if (typeof binding.source.identity !== "string" || binding.source.identity.length === 0) {
|
|
211
|
+
throw new Error("Dependency binding identity is required.");
|
|
212
|
+
}
|
|
213
|
+
let hash = null;
|
|
214
|
+
if (binding.source.kind === "lockfile") {
|
|
215
|
+
hash = context.lockfiles.find(candidate => candidate.path === binding.source.identity)?.sha256 ?? null;
|
|
216
|
+
} else if (binding.source.kind === "fixture") {
|
|
217
|
+
hash = context.fixtures.find(candidate => candidate.id === binding.source.identity)?.sha256 ?? null;
|
|
218
|
+
} else if (binding.source.kind === "material_input") {
|
|
219
|
+
hash = inputs.get(binding.source.identity) ?? null;
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(`Unsupported dependency source kind: ${binding.source.kind}`);
|
|
222
|
+
}
|
|
223
|
+
if (hash === null) {
|
|
224
|
+
throw new Error(`Declared dependency evidence is missing: ${binding.dependencyId}`);
|
|
225
|
+
}
|
|
226
|
+
result[binding.dependencyId] = expectedSha256(hash, "Declared dependency evidence hash");
|
|
227
|
+
}
|
|
228
|
+
return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left.localeCompare(right)));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateSourceRequest(value) {
|
|
232
|
+
assertAllowedKeys(value, "Coverage source", SOURCE_KEYS);
|
|
233
|
+
if (typeof value.gateInputPath !== "string"
|
|
234
|
+
|| value.gateInputPath.length === 0
|
|
235
|
+
|| !value.gateInputPath.startsWith(`${RUNTIME_ROOT}/`)) {
|
|
236
|
+
throw new Error("Coverage gateInputPath must reference a project RunKit artifact.");
|
|
237
|
+
}
|
|
238
|
+
expectedSha256(value.gateInputSha256, "gateInputSha256");
|
|
239
|
+
if (!Array.isArray(value.commandBindings) || value.commandBindings.length === 0) {
|
|
240
|
+
throw new Error("Coverage source requires commandBindings.");
|
|
241
|
+
}
|
|
242
|
+
if (!Array.isArray(value.dependencyBindings)) {
|
|
243
|
+
throw new Error("Coverage source requires dependencyBindings.");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function adoptSource({ workspaceRoot, source, profiles }) {
|
|
248
|
+
validateSourceRequest(source);
|
|
249
|
+
const gatePath = resolveExistingArtifact(workspaceRoot, source.gateInputPath, "gateInputPath");
|
|
250
|
+
const normalizedGatePath = relativeToWorkspace(workspaceRoot, gatePath);
|
|
251
|
+
const gateBytes = readFileSync(gatePath);
|
|
252
|
+
const actualGateSha256 = sha256(gateBytes);
|
|
253
|
+
if (actualGateSha256 !== source.gateInputSha256) throw new Error("Gate input hash mismatch.");
|
|
254
|
+
const gateInput = JSON.parse(gateBytes.toString("utf8"));
|
|
255
|
+
const gate = validateVerificationReceiptGate(gateInput);
|
|
256
|
+
if (!gate.accepted || gate.contractVersion !== "0.2" || gate.decision !== "accepted_passed") {
|
|
257
|
+
const details = gate.issues.map(issue => issue.message).join("; ");
|
|
258
|
+
throw new Error(`Coverage source gate is not an accepted Contract v0.2 gate: ${details}`);
|
|
259
|
+
}
|
|
260
|
+
const active = gate.lineage.active;
|
|
261
|
+
const receipt = active.receipt;
|
|
262
|
+
if (receipt.status !== "passed") throw new Error("Coverage source active receipt did not pass.");
|
|
263
|
+
const receiptCommandIds = receipt.commandReceipts.map(command => command.id);
|
|
264
|
+
if (receiptCommandIds.some(id => typeof id !== "string" || id.length === 0)
|
|
265
|
+
|| new Set(receiptCommandIds).size !== receiptCommandIds.length) {
|
|
266
|
+
throw new Error("Accepted receipt command ids must be unique non-empty strings.");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const commandBindingIds = new Set();
|
|
270
|
+
const commandIds = new Set();
|
|
271
|
+
const profileIds = new Set();
|
|
272
|
+
const normalizedCommandBindings = [];
|
|
273
|
+
const boundCommands = source.commandBindings.map(binding => {
|
|
274
|
+
assertAllowedKeys(binding, "Command binding", COMMAND_BINDING_KEYS);
|
|
275
|
+
for (const [field, label] of [
|
|
276
|
+
["receiptCommandId", "receiptCommandId"],
|
|
277
|
+
["profileId", "profileId"],
|
|
278
|
+
["commandId", "commandId"],
|
|
279
|
+
]) safeIdentifier(binding[field], label);
|
|
280
|
+
if (commandBindingIds.has(binding.receiptCommandId)) {
|
|
281
|
+
throw new Error(`Receipt command bindings must be unique: ${binding.receiptCommandId}`);
|
|
282
|
+
}
|
|
283
|
+
if (commandIds.has(binding.commandId)) {
|
|
284
|
+
throw new Error(`Coverage command ids must be unique per receipt: ${binding.commandId}`);
|
|
285
|
+
}
|
|
286
|
+
commandBindingIds.add(binding.receiptCommandId);
|
|
287
|
+
commandIds.add(binding.commandId);
|
|
288
|
+
profileIds.add(binding.profileId);
|
|
289
|
+
normalizedCommandBindings.push({
|
|
290
|
+
receiptCommandId: binding.receiptCommandId,
|
|
291
|
+
profileId: binding.profileId,
|
|
292
|
+
commandId: binding.commandId,
|
|
293
|
+
});
|
|
294
|
+
return commandForBinding({ binding, receipt, profiles });
|
|
295
|
+
});
|
|
296
|
+
const inputs = materialInputs(boundCommands);
|
|
297
|
+
const dependencySha256 = dependencySha256ForBindings(
|
|
298
|
+
source.dependencyBindings,
|
|
299
|
+
gateInput.verificationContext,
|
|
300
|
+
inputs,
|
|
301
|
+
);
|
|
302
|
+
const normalizedDependencyBindings = source.dependencyBindings.map(binding => ({
|
|
303
|
+
dependencyId: binding.dependencyId,
|
|
304
|
+
source: {
|
|
305
|
+
kind: binding.source.kind,
|
|
306
|
+
identity: binding.source.identity,
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
return {
|
|
310
|
+
generatedFrom: {
|
|
311
|
+
gateInputPath: normalizedGatePath,
|
|
312
|
+
gateInputSha256: actualGateSha256,
|
|
313
|
+
commandBindings: normalizedCommandBindings,
|
|
314
|
+
dependencyBindings: normalizedDependencyBindings,
|
|
315
|
+
activeReceiptSha256: active.receiptSha256,
|
|
316
|
+
sourceRunId: receipt.runId,
|
|
317
|
+
},
|
|
318
|
+
entry: {
|
|
319
|
+
receiptId: receipt.receiptId,
|
|
320
|
+
receiptSha256: active.receiptSha256,
|
|
321
|
+
status: "passed",
|
|
322
|
+
sourceFiles: sourceFilesForBindings(boundCommands, inputs),
|
|
323
|
+
dependencySha256,
|
|
324
|
+
verificationContextFingerprint: receipt.verificationContextFingerprint,
|
|
325
|
+
profileIds: sortedUniqueStrings(profileIds),
|
|
326
|
+
commandIds: sortedUniqueStrings(commandIds),
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function validateRequest(request) {
|
|
332
|
+
assertAllowedKeys(request, "Coverage-adopt request", REQUEST_KEYS);
|
|
333
|
+
if (request.schemaVersion !== "OwlCodaRunKitCoverageAdoptRequestV1") {
|
|
334
|
+
throw new Error("Unsupported coverage-adopt request schemaVersion.");
|
|
335
|
+
}
|
|
336
|
+
safeIdentifier(request.coverageId, "coverageId");
|
|
337
|
+
if (!Array.isArray(request.sources)) {
|
|
338
|
+
throw new Error("Coverage-adopt request requires sources.");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function deriveCoverageIndex({ workspaceRoot, runId, coverageId, sources }) {
|
|
343
|
+
const profiles = validateProfileDocument(readJson(path.join(workspaceRoot, RUNTIME_ROOT, "profiles.json")));
|
|
344
|
+
const adopted = sources.map(source => adoptSource({ workspaceRoot, source, profiles }));
|
|
345
|
+
const receiptIds = adopted.map(item => item.entry.receiptId);
|
|
346
|
+
if (new Set(receiptIds).size !== receiptIds.length) {
|
|
347
|
+
throw new Error("Adopted receipt ids must be unique.");
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
schemaVersion: "OwlCodaRunKitEvidenceCoverageIndexV1",
|
|
351
|
+
coverageId,
|
|
352
|
+
runId,
|
|
353
|
+
generatedFrom: adopted.map(item => item.generatedFrom)
|
|
354
|
+
.sort((left, right) => left.activeReceiptSha256.localeCompare(right.activeReceiptSha256)),
|
|
355
|
+
entries: adopted.map(item => item.entry)
|
|
356
|
+
.sort((left, right) => left.receiptId.localeCompare(right.receiptId)),
|
|
357
|
+
authorizationGranted: false,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function buildCoverageIndex({ workspaceRoot, runId, request }) {
|
|
362
|
+
validateRequest(request);
|
|
363
|
+
return deriveCoverageIndex({
|
|
364
|
+
workspaceRoot,
|
|
365
|
+
runId,
|
|
366
|
+
coverageId: request.coverageId,
|
|
367
|
+
sources: request.sources,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function validateCoverageIndexArtifact({ workspaceRoot, runId, coverageIndex }) {
|
|
372
|
+
if (!isRecord(coverageIndex)
|
|
373
|
+
|| coverageIndex.schemaVersion !== "OwlCodaRunKitEvidenceCoverageIndexV1"
|
|
374
|
+
|| coverageIndex.runId !== runId
|
|
375
|
+
|| typeof coverageIndex.coverageId !== "string"
|
|
376
|
+
|| !Array.isArray(coverageIndex.generatedFrom)) {
|
|
377
|
+
throw new Error("Coverage index cannot be revalidated.");
|
|
378
|
+
}
|
|
379
|
+
const sources = coverageIndex.generatedFrom.map(source => ({
|
|
380
|
+
gateInputPath: source.gateInputPath,
|
|
381
|
+
gateInputSha256: source.gateInputSha256,
|
|
382
|
+
commandBindings: source.commandBindings,
|
|
383
|
+
dependencyBindings: source.dependencyBindings,
|
|
384
|
+
}));
|
|
385
|
+
const derived = deriveCoverageIndex({
|
|
386
|
+
workspaceRoot,
|
|
387
|
+
runId,
|
|
388
|
+
coverageId: coverageIndex.coverageId,
|
|
389
|
+
sources,
|
|
390
|
+
});
|
|
391
|
+
if (JSON.stringify(derived) !== JSON.stringify(coverageIndex)) {
|
|
392
|
+
throw new Error("Coverage index no longer matches its receipt-backed source evidence.");
|
|
393
|
+
}
|
|
394
|
+
return derived;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function runCoverageAdoption({ workspaceRoot, runId, request }) {
|
|
398
|
+
const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
|
|
399
|
+
if (pinGate.status !== "valid") return pinGate;
|
|
400
|
+
if (existsSync(path.join(executionRoot, "closeout-receipt.json"))) {
|
|
401
|
+
throw new Error(`Cannot adopt coverage into closed execution: ${runId}`);
|
|
402
|
+
}
|
|
403
|
+
const coverageIndex = buildCoverageIndex({ workspaceRoot, runId, request });
|
|
404
|
+
const outputPath = path.join(ensureCoverageDirectory(executionRoot), `${request.coverageId}.json`);
|
|
405
|
+
writeJsonExclusive(outputPath, coverageIndex);
|
|
406
|
+
return {
|
|
407
|
+
status: "coverage_index_written",
|
|
408
|
+
exitCode: 0,
|
|
409
|
+
runId,
|
|
410
|
+
coverageIndexPath: relativeToWorkspace(workspaceRoot, outputPath),
|
|
411
|
+
coverageIndexSha256: sha256(readFileSync(outputPath)),
|
|
412
|
+
authorizationGranted: false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
RUNTIME_ROOT,
|
|
13
|
+
currentCoreIdentity,
|
|
14
|
+
} from "./core-contract.mjs";
|
|
15
|
+
import {
|
|
16
|
+
loadActiveExecution,
|
|
17
|
+
relativeToWorkspace,
|
|
18
|
+
safeIdentifier,
|
|
19
|
+
writeJsonExclusiveAtomically,
|
|
20
|
+
} from "./provenance-common.mjs";
|
|
21
|
+
import {
|
|
22
|
+
listLeaseArtifacts,
|
|
23
|
+
withControlTransaction,
|
|
24
|
+
} from "./lease-lifecycle.mjs";
|
|
25
|
+
import {
|
|
26
|
+
canonicalSourceFingerprint,
|
|
27
|
+
verifyDeliveryPacket,
|
|
28
|
+
} from "./source-fingerprint.mjs";
|
|
29
|
+
|
|
30
|
+
function git(workspaceRoot, args, label, { trim = true } = {}) {
|
|
31
|
+
const completed = spawnSync("git", ["-C", workspaceRoot, ...args], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
34
|
+
});
|
|
35
|
+
if (completed.error) throw completed.error;
|
|
36
|
+
if (completed.status !== 0) throw new Error(`${label} failed: ${completed.stderr.trim()}`);
|
|
37
|
+
return trim ? completed.stdout.trim() : completed.stdout;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseStatus(workspaceRoot) {
|
|
41
|
+
const raw = git(workspaceRoot, [
|
|
42
|
+
"status",
|
|
43
|
+
"--porcelain=v1",
|
|
44
|
+
"-z",
|
|
45
|
+
"--untracked-files=all",
|
|
46
|
+
"--",
|
|
47
|
+
".",
|
|
48
|
+
":(exclude).owlcoda/runkit/**",
|
|
49
|
+
], "Git status", { trim: false });
|
|
50
|
+
if (raw.length === 0) return [];
|
|
51
|
+
const entries = raw.split("\0");
|
|
52
|
+
const records = [];
|
|
53
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
54
|
+
const entry = entries[index];
|
|
55
|
+
if (!entry) continue;
|
|
56
|
+
if (entry.length < 4 || entry[2] !== " ") throw new Error("Git status emitted a malformed entry.");
|
|
57
|
+
const status = entry.slice(0, 2);
|
|
58
|
+
const currentPath = entry.slice(3);
|
|
59
|
+
const paths = [currentPath];
|
|
60
|
+
if (status.includes("R") || status.includes("C")) {
|
|
61
|
+
const priorPath = entries[index + 1];
|
|
62
|
+
if (!priorPath) throw new Error("Git status rename entry is incomplete.");
|
|
63
|
+
paths.push(priorPath);
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
records.push({ status, currentPath, paths });
|
|
67
|
+
}
|
|
68
|
+
return records;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pathMatches(filePath, ownedPath) {
|
|
72
|
+
if (ownedPath.endsWith("/**")) {
|
|
73
|
+
const prefix = ownedPath.slice(0, -3);
|
|
74
|
+
return filePath === prefix || filePath.startsWith(`${prefix}/`);
|
|
75
|
+
}
|
|
76
|
+
return filePath === ownedPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isOwned(filePath, ownedPaths) {
|
|
80
|
+
return ownedPaths.some(ownedPath => pathMatches(filePath, ownedPath));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ownedStatusSignature(records, ownedPaths) {
|
|
84
|
+
return JSON.stringify(ownedStatusRecords(records, ownedPaths));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ownedStatusRecords(records, ownedPaths) {
|
|
88
|
+
return records
|
|
89
|
+
.filter(record => record.paths.some(filePath => isOwned(filePath, ownedPaths)))
|
|
90
|
+
.map(record => ({ status: record.status, paths: [...record.paths] }))
|
|
91
|
+
.sort((left, right) => JSON.stringify(left.paths).localeCompare(JSON.stringify(right.paths)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function within(root, candidate) {
|
|
95
|
+
const relative = path.relative(root, candidate);
|
|
96
|
+
return relative === ""
|
|
97
|
+
|| (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function safeOutputDirectory(executionRoot) {
|
|
101
|
+
const rootStat = lstatSync(executionRoot);
|
|
102
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
103
|
+
throw new Error("Delivery execution root must be a regular directory, not a symlink.");
|
|
104
|
+
}
|
|
105
|
+
const outputRoot = path.join(executionRoot, "delivery-packets");
|
|
106
|
+
const outputStat = lstatSync(outputRoot);
|
|
107
|
+
if (outputStat.isSymbolicLink() || !outputStat.isDirectory()) {
|
|
108
|
+
throw new Error("Delivery packet directory must be a regular directory, not a symlink.");
|
|
109
|
+
}
|
|
110
|
+
if (!within(realpathSync(executionRoot), realpathSync(outputRoot))) {
|
|
111
|
+
throw new Error("Delivery packet directory escapes its execution root.");
|
|
112
|
+
}
|
|
113
|
+
return outputRoot;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hashRegularFile(workspaceRoot, filePath) {
|
|
117
|
+
if (filePath === RUNTIME_ROOT || filePath.startsWith(`${RUNTIME_ROOT}/`)) {
|
|
118
|
+
throw new Error(`Delivery candidate uses reserved runtime path: ${filePath}`);
|
|
119
|
+
}
|
|
120
|
+
let candidate = workspaceRoot;
|
|
121
|
+
for (const segment of filePath.split("/")) {
|
|
122
|
+
if (!segment || segment === "." || segment === "..") {
|
|
123
|
+
throw new Error(`Delivery candidate path is unsafe: ${filePath}`);
|
|
124
|
+
}
|
|
125
|
+
candidate = path.resolve(candidate, segment);
|
|
126
|
+
if (!within(workspaceRoot, candidate)) throw new Error(`Delivery candidate escapes workspace: ${filePath}`);
|
|
127
|
+
const stat = lstatSync(candidate);
|
|
128
|
+
if (stat.isSymbolicLink()) throw new Error(`Delivery candidate is a symlink: ${filePath}`);
|
|
129
|
+
}
|
|
130
|
+
const stat = lstatSync(candidate);
|
|
131
|
+
if (!stat.isFile()) throw new Error(`Delivery candidate is not a regular file: ${filePath}`);
|
|
132
|
+
const realCandidate = realpathSync(candidate);
|
|
133
|
+
if (!within(workspaceRoot, realCandidate)) throw new Error(`Delivery candidate escapes workspace: ${filePath}`);
|
|
134
|
+
return createHash("sha256").update(readFileSync(realCandidate)).digest("hex");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function repositoryActionsFalse() {
|
|
138
|
+
return {
|
|
139
|
+
worktreeCreated: false,
|
|
140
|
+
branchCreated: false,
|
|
141
|
+
staged: false,
|
|
142
|
+
committed: false,
|
|
143
|
+
pushed: false,
|
|
144
|
+
tagged: false,
|
|
145
|
+
published: false,
|
|
146
|
+
deployed: false,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function createDeliveryFromLeaseWithinControlTransaction({ workspaceRoot, runId, workItemId, packetId }) {
|
|
151
|
+
safeIdentifier(workItemId, "from-lease");
|
|
152
|
+
safeIdentifier(packetId, "packet-id");
|
|
153
|
+
const { executionRoot, pinGate } = loadActiveExecution(workspaceRoot, runId);
|
|
154
|
+
if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
|
|
155
|
+
if (existsSync(path.join(executionRoot, "closeout-receipt.json"))) {
|
|
156
|
+
throw new Error(`Cannot create delivery in closed execution: ${runId}`);
|
|
157
|
+
}
|
|
158
|
+
const matches = listLeaseArtifacts({ workspaceRoot, executionRoot })
|
|
159
|
+
.filter(lease => lease.workItemId === workItemId);
|
|
160
|
+
if (matches.length !== 1 || matches[0].state !== "active") {
|
|
161
|
+
throw new Error(`Delivery creation requires one active lease: ${workItemId}`);
|
|
162
|
+
}
|
|
163
|
+
const [lease] = matches;
|
|
164
|
+
const outputPath = path.join(safeOutputDirectory(executionRoot), `${packetId}.json`);
|
|
165
|
+
if (existsSync(outputPath)) throw new Error(`Delivery packet already exists: ${packetId}`);
|
|
166
|
+
|
|
167
|
+
const candidatePaths = new Set();
|
|
168
|
+
const unrelatedDirtyPaths = new Set();
|
|
169
|
+
const deletedOwnedPaths = new Set();
|
|
170
|
+
const renamedOwnedPaths = new Set();
|
|
171
|
+
const statusBefore = parseStatus(workspaceRoot);
|
|
172
|
+
for (const record of statusBefore) {
|
|
173
|
+
const ownedRecordPaths = record.paths.filter(filePath => isOwned(filePath, lease.ownedPaths));
|
|
174
|
+
for (const filePath of record.paths) {
|
|
175
|
+
if (!isOwned(filePath, lease.ownedPaths)) unrelatedDirtyPaths.add(filePath);
|
|
176
|
+
}
|
|
177
|
+
if (ownedRecordPaths.length === 0) continue;
|
|
178
|
+
if (record.status.includes("R") || record.status.includes("C")) {
|
|
179
|
+
for (const filePath of ownedRecordPaths) renamedOwnedPaths.add(filePath);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (record.status.includes("D")) {
|
|
183
|
+
for (const filePath of ownedRecordPaths) deletedOwnedPaths.add(filePath);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (isOwned(record.currentPath, lease.ownedPaths)) candidatePaths.add(record.currentPath);
|
|
187
|
+
}
|
|
188
|
+
const deleted = [...deletedOwnedPaths].sort();
|
|
189
|
+
const renamed = [...renamedOwnedPaths].sort();
|
|
190
|
+
if (deleted.length > 0) throw new Error(`Deleted owned paths require an explicit packet protocol: ${deleted.join(", ")}`);
|
|
191
|
+
if (renamed.length > 0) throw new Error(`Renamed owned paths require an explicit packet protocol: ${renamed.join(", ")}`);
|
|
192
|
+
if (candidatePaths.size === 0) throw new Error("No changed regular files are owned by the active lease.");
|
|
193
|
+
|
|
194
|
+
const files = {};
|
|
195
|
+
for (const filePath of [...candidatePaths].sort()) {
|
|
196
|
+
files[filePath] = hashRegularFile(workspaceRoot, filePath);
|
|
197
|
+
}
|
|
198
|
+
const statusAfter = parseStatus(workspaceRoot);
|
|
199
|
+
if (ownedStatusSignature(statusBefore, lease.ownedPaths)
|
|
200
|
+
!== ownedStatusSignature(statusAfter, lease.ownedPaths)) {
|
|
201
|
+
throw new Error("Owned workspace status changed during DeliveryPacket creation.");
|
|
202
|
+
}
|
|
203
|
+
const packet = {
|
|
204
|
+
schemaVersion: "ExecutionDeliveryPacketV1",
|
|
205
|
+
status: "ready_for_stage_verification",
|
|
206
|
+
runId,
|
|
207
|
+
revision: 1,
|
|
208
|
+
baseline: {
|
|
209
|
+
branch: git(workspaceRoot, ["rev-parse", "--abbrev-ref", "HEAD"], "Git branch"),
|
|
210
|
+
head: git(workspaceRoot, ["rev-parse", "HEAD"], "Git HEAD"),
|
|
211
|
+
},
|
|
212
|
+
changedFiles: { wholeFileSha256: files },
|
|
213
|
+
sourceFingerprint: { sha256: canonicalSourceFingerprint(files) },
|
|
214
|
+
core: currentCoreIdentity(),
|
|
215
|
+
discovery: {
|
|
216
|
+
fromLease: workItemId,
|
|
217
|
+
leasePath: lease.leaseRelativePath,
|
|
218
|
+
ownedPathState: {
|
|
219
|
+
schemaVersion: "OwlCodaRunKitOwnedPathStateV1",
|
|
220
|
+
statusMode: "porcelain-v1-z-untracked-all-runkit-excluded",
|
|
221
|
+
ownedPaths: [...lease.ownedPaths],
|
|
222
|
+
records: ownedStatusRecords(statusBefore, lease.ownedPaths),
|
|
223
|
+
},
|
|
224
|
+
unrelatedDirtyPaths: [...unrelatedDirtyPaths].sort(),
|
|
225
|
+
deletedOwnedPaths: [],
|
|
226
|
+
renamedOwnedPaths: [],
|
|
227
|
+
},
|
|
228
|
+
repositoryActions: repositoryActionsFalse(),
|
|
229
|
+
authorizationGranted: false,
|
|
230
|
+
};
|
|
231
|
+
const sourceGate = verifyDeliveryPacket({ workspaceRoot, packet });
|
|
232
|
+
if (sourceGate.status !== "valid") {
|
|
233
|
+
throw new Error(`Delivery source changed during packet creation: ${sourceGate.issues.map(issue => issue.message ?? issue).join("; ")}`);
|
|
234
|
+
}
|
|
235
|
+
writeJsonExclusiveAtomically(outputPath, packet);
|
|
236
|
+
return {
|
|
237
|
+
status: "delivery_packet_created",
|
|
238
|
+
exitCode: 0,
|
|
239
|
+
runId,
|
|
240
|
+
deliveryPacketPath: relativeToWorkspace(workspaceRoot, outputPath),
|
|
241
|
+
sourceFingerprint: packet.sourceFingerprint.sha256,
|
|
242
|
+
unrelatedDirtyPaths: packet.discovery.unrelatedDirtyPaths,
|
|
243
|
+
authorizationGranted: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function createDeliveryFromLease(args) {
|
|
248
|
+
return withControlTransaction(
|
|
249
|
+
args.workspaceRoot,
|
|
250
|
+
() => createDeliveryFromLeaseWithinControlTransaction(args),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
function candidatePath(candidate) {
|
|
5
|
+
const value = candidate?.packetPath ?? candidate?.path;
|
|
6
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
7
|
+
throw new Error("Delivery candidate requires packetPath or path.");
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function collapseByteIdenticalDeliveryCandidates(candidates) {
|
|
13
|
+
const groups = new Map();
|
|
14
|
+
for (const candidate of [...candidates].sort((left, right) =>
|
|
15
|
+
candidatePath(left).localeCompare(candidatePath(right)))) {
|
|
16
|
+
const digest = createHash("sha256")
|
|
17
|
+
.update(readFileSync(candidatePath(candidate)))
|
|
18
|
+
.digest("hex");
|
|
19
|
+
const existing = groups.get(digest);
|
|
20
|
+
if (existing) {
|
|
21
|
+
existing.duplicatePacketPaths.push(candidatePath(candidate));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
groups.set(digest, {
|
|
25
|
+
...candidate,
|
|
26
|
+
packetFileSha256: digest,
|
|
27
|
+
duplicatePacketPaths: [],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return [...groups.values()];
|
|
31
|
+
}
|