owlrunkit 0.15.1 → 0.16.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 +91 -0
- package/README.md +274 -66
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +222 -116
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
- package/package.json +2 -2
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +5 -0
- package/registry-release-policy-v1.json +8 -2
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/assurance-router.mjs +119 -0
- package/scripts/runkit-contract/core-contract.mjs +203 -206
- package/scripts/runkit-contract/core-successor.mjs +2043 -0
- package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
- package/scripts/runkit-contract/finalize.mjs +242 -21
- package/scripts/runkit-contract/fleet-discovery.mjs +456 -0
- package/scripts/runkit-contract/formal-workflow.mjs +850 -0
- package/scripts/runkit-contract/human-status.mjs +553 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
- package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
- package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
- package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
- package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
- package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
- package/scripts/runkit-contract/project-cli-resolver.mjs +701 -0
- package/scripts/runkit-contract/project-control-parser.mjs +830 -0
- package/scripts/runkit-contract/project-control-state.mjs +28 -0
- package/scripts/runkit-contract/provenance-common.mjs +19 -2
- package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
- package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
- package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
- package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1097 -21
- package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
- package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +155 -0
- package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
- package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
- package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
- package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
- package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +31 -0
- package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
- package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
- package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
- package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
- package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
- package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
- package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
- package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
- package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
- package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
- package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
- package/scripts/runkit-contract/snapshot.mjs +1 -1
- package/scripts/runkit-contract/source-candidate.mjs +1382 -0
- package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
- package/scripts/runkit-contract/sync-core016-release-identity.mjs +161 -0
- package/scripts/runkit-contract/templates/assurance-request.json +13 -0
- package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
- package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
- package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
- package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
- package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
- package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
- package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
- package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
mkdir,
|
|
7
|
+
readdir,
|
|
8
|
+
readFile,
|
|
9
|
+
realpath,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
coreIdentityFromSourceRoot,
|
|
18
|
+
currentCoreIdentity,
|
|
19
|
+
initializeProjectRunKit,
|
|
20
|
+
validateExecutionPin,
|
|
21
|
+
validateProjectConfigV2,
|
|
22
|
+
} from "./core-contract.mjs";
|
|
23
|
+
import { discoverFleet } from "./fleet-discovery.mjs";
|
|
24
|
+
import { withControlTransaction } from "./lease-lifecycle.mjs";
|
|
25
|
+
import { inspectProjectControlState } from "./project-control-state.mjs";
|
|
26
|
+
import {
|
|
27
|
+
freezeSourceCandidateV2,
|
|
28
|
+
materializeSourceCandidateV2,
|
|
29
|
+
verifySourceCandidatePathClosureV2,
|
|
30
|
+
verifySourceCandidateV2,
|
|
31
|
+
} from "./source-candidate.mjs";
|
|
32
|
+
import {
|
|
33
|
+
ownerAuthorityArtifactSha256V1,
|
|
34
|
+
verifyTrustedOwnerAuthorityV1,
|
|
35
|
+
} from "./owner-authority-trust.mjs";
|
|
36
|
+
|
|
37
|
+
const SHA256_REF = /^sha256:[a-f0-9]{64}$/u;
|
|
38
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
39
|
+
const MAX_ARTIFACT_BYTES = 1_048_576;
|
|
40
|
+
|
|
41
|
+
const REPOSITORY_ACTIONS = Object.freeze({
|
|
42
|
+
stage: false,
|
|
43
|
+
commit: false,
|
|
44
|
+
push: false,
|
|
45
|
+
publish: false,
|
|
46
|
+
deploy: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
function canonicalJson(value) {
|
|
50
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
return `{${Object.keys(value)
|
|
53
|
+
.sort()
|
|
54
|
+
.map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
55
|
+
.join(",")}}`;
|
|
56
|
+
}
|
|
57
|
+
const encoded = JSON.stringify(value);
|
|
58
|
+
if (encoded === undefined) {
|
|
59
|
+
throw new Error("core_successor_noncanonical_value");
|
|
60
|
+
}
|
|
61
|
+
return encoded;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function coreSuccessorArtifactSha256V1(value) {
|
|
65
|
+
return `sha256:${createHash("sha256")
|
|
66
|
+
.update(canonicalJson(value))
|
|
67
|
+
.digest("hex")}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cloneJson(value, label) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(JSON.stringify(value));
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error(`core_successor_${label}_not_json`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assertIdentifier(value, label) {
|
|
79
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value)) {
|
|
80
|
+
throw new Error(`core_successor_${label}_invalid`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function canonicalWorkspaceRoot(workspaceRoot) {
|
|
85
|
+
if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
|
|
86
|
+
throw new Error("core_successor_controller_workspace_required");
|
|
87
|
+
}
|
|
88
|
+
const requested = path.resolve(workspaceRoot);
|
|
89
|
+
const stat = await lstat(requested);
|
|
90
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
91
|
+
throw new Error("core_successor_controller_workspace_invalid");
|
|
92
|
+
}
|
|
93
|
+
return realpath(requested);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function safeRelativePath(value, label) {
|
|
97
|
+
if (
|
|
98
|
+
typeof value !== "string"
|
|
99
|
+
|| value.length === 0
|
|
100
|
+
|| path.isAbsolute(value)
|
|
101
|
+
|| value.includes("\\")
|
|
102
|
+
|| value.split("/").some(segment => (
|
|
103
|
+
segment.length === 0 || segment === "." || segment === ".."
|
|
104
|
+
))
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(`core_successor_${label}_invalid`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function within(root, candidate) {
|
|
112
|
+
const relative = path.relative(root, candidate);
|
|
113
|
+
return relative === ""
|
|
114
|
+
|| (
|
|
115
|
+
relative !== ".."
|
|
116
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
117
|
+
&& !path.isAbsolute(relative)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function prepareCreateOnlyPath(root, relativePath, existsCode) {
|
|
122
|
+
const safePath = safeRelativePath(relativePath, "artifact_path");
|
|
123
|
+
const absolutePath = path.resolve(root, safePath);
|
|
124
|
+
if (!within(root, absolutePath)) {
|
|
125
|
+
throw new Error("core_successor_artifact_path_escapes_workspace");
|
|
126
|
+
}
|
|
127
|
+
const parent = path.dirname(absolutePath);
|
|
128
|
+
await mkdir(parent, { recursive: true });
|
|
129
|
+
if (await realpath(parent) !== path.resolve(parent)) {
|
|
130
|
+
throw new Error("core_successor_artifact_parent_symlink_rejected");
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await lstat(absolutePath);
|
|
134
|
+
throw new Error(existsCode);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error?.code !== "ENOENT") throw error;
|
|
137
|
+
}
|
|
138
|
+
return { absolutePath, relativePath: safePath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function readBoundedJson(root, relativePath, label) {
|
|
142
|
+
const safePath = safeRelativePath(relativePath, label);
|
|
143
|
+
const absolutePath = path.resolve(root, safePath);
|
|
144
|
+
if (!within(root, absolutePath)) {
|
|
145
|
+
throw new Error(`core_successor_${label}_escapes_workspace`);
|
|
146
|
+
}
|
|
147
|
+
const stat = await lstat(absolutePath);
|
|
148
|
+
if (
|
|
149
|
+
stat.isSymbolicLink()
|
|
150
|
+
|| !stat.isFile()
|
|
151
|
+
|| stat.size > MAX_ARTIFACT_BYTES
|
|
152
|
+
|| await realpath(absolutePath) !== path.resolve(absolutePath)
|
|
153
|
+
) {
|
|
154
|
+
throw new Error(`core_successor_${label}_invalid`);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(await readFile(absolutePath, "utf8"));
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error(`core_successor_${label}_invalid_json`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function writeCreateOnlyJson(root, relativePath, value, existsCode) {
|
|
164
|
+
const output = await prepareCreateOnlyPath(root, relativePath, existsCode);
|
|
165
|
+
await writeFile(
|
|
166
|
+
output.absolutePath,
|
|
167
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
168
|
+
{ flag: "wx" },
|
|
169
|
+
);
|
|
170
|
+
return output.relativePath;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertCoreIdentity(core, label) {
|
|
174
|
+
const gate = validateProjectConfigV2({
|
|
175
|
+
schemaVersion: "OwlCodaRunKitConfigV2",
|
|
176
|
+
core,
|
|
177
|
+
authorizationPolicy: "external_explicit_authority_required",
|
|
178
|
+
});
|
|
179
|
+
if (!gate.valid) throw new Error(`core_successor_${label}_invalid`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sameCore(left, right) {
|
|
183
|
+
return validateExecutionPin({ expected: left, actual: right }).status === "valid";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sha256Bytes(bytes) {
|
|
187
|
+
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function readWorkspaceArtifact(workspaceRoot, relativePath, label) {
|
|
191
|
+
const root = await canonicalWorkspaceRoot(workspaceRoot);
|
|
192
|
+
const safePath = safeRelativePath(relativePath, label);
|
|
193
|
+
const absolutePath = path.resolve(root, safePath);
|
|
194
|
+
if (!within(root, absolutePath)) {
|
|
195
|
+
throw new Error(`core_successor_${label}_escapes_workspace`);
|
|
196
|
+
}
|
|
197
|
+
const stat = await lstat(absolutePath);
|
|
198
|
+
if (
|
|
199
|
+
stat.isSymbolicLink()
|
|
200
|
+
|| !stat.isFile()
|
|
201
|
+
|| stat.size > MAX_ARTIFACT_BYTES
|
|
202
|
+
|| await realpath(absolutePath) !== path.resolve(absolutePath)
|
|
203
|
+
) {
|
|
204
|
+
throw new Error(`core_successor_${label}_invalid`);
|
|
205
|
+
}
|
|
206
|
+
const bytes = await readFile(absolutePath);
|
|
207
|
+
let value;
|
|
208
|
+
try {
|
|
209
|
+
value = JSON.parse(bytes.toString("utf8"));
|
|
210
|
+
} catch {
|
|
211
|
+
throw new Error(`core_successor_${label}_invalid_json`);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
relativePath: safePath,
|
|
215
|
+
bytes,
|
|
216
|
+
sha256: sha256Bytes(bytes),
|
|
217
|
+
value,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function receiptAttemptPath(basePath, attemptNumber) {
|
|
222
|
+
const extension = path.extname(basePath);
|
|
223
|
+
const stem = extension.length > 0
|
|
224
|
+
? basePath.slice(0, -extension.length)
|
|
225
|
+
: basePath;
|
|
226
|
+
return attemptNumber === 1
|
|
227
|
+
? basePath
|
|
228
|
+
: `${stem}-attempt-${String(attemptNumber).padStart(3, "0")}${extension}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function projectSuccessReceiptPath(basePath, receiptId, workspaceRoot) {
|
|
232
|
+
const extension = path.extname(basePath);
|
|
233
|
+
const stem = path.basename(
|
|
234
|
+
extension.length > 0
|
|
235
|
+
? basePath.slice(0, -extension.length)
|
|
236
|
+
: basePath,
|
|
237
|
+
);
|
|
238
|
+
const workspaceSha256 = createHash("sha256")
|
|
239
|
+
.update(workspaceRoot)
|
|
240
|
+
.digest("hex");
|
|
241
|
+
return path.join(
|
|
242
|
+
path.dirname(basePath),
|
|
243
|
+
`${stem}-project-success`,
|
|
244
|
+
`${receiptId}-${workspaceSha256}.json`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function pathExists(absolutePath) {
|
|
249
|
+
try {
|
|
250
|
+
await lstat(absolutePath);
|
|
251
|
+
return true;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error?.code === "ENOENT") return false;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function coreBindingSetSha256(bindings) {
|
|
259
|
+
return coreSuccessorArtifactSha256V1(bindings);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function normalizedFleetCoreBindings(fleetDiscovery, bindings) {
|
|
263
|
+
if (
|
|
264
|
+
!Array.isArray(bindings)
|
|
265
|
+
|| bindings.length !== fleetDiscovery.workspaceRoots.length
|
|
266
|
+
) {
|
|
267
|
+
throw new Error("core_successor_fleet_core_bindings_invalid");
|
|
268
|
+
}
|
|
269
|
+
const byWorkspace = new Map();
|
|
270
|
+
for (const binding of bindings) {
|
|
271
|
+
if (
|
|
272
|
+
!binding
|
|
273
|
+
|| typeof binding.workspaceRoot !== "string"
|
|
274
|
+
|| byWorkspace.has(binding.workspaceRoot)
|
|
275
|
+
) {
|
|
276
|
+
throw new Error("core_successor_fleet_core_bindings_invalid");
|
|
277
|
+
}
|
|
278
|
+
assertCoreIdentity(binding.fromCore, "fleet_from_core");
|
|
279
|
+
byWorkspace.set(binding.workspaceRoot, {
|
|
280
|
+
workspaceRoot: binding.workspaceRoot,
|
|
281
|
+
fromCore: cloneJson(binding.fromCore, "fleet_from_core"),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const ordered = fleetDiscovery.workspaceRoots.map((workspaceRoot) => {
|
|
285
|
+
const binding = byWorkspace.get(workspaceRoot);
|
|
286
|
+
if (!binding) throw new Error("core_successor_fleet_core_bindings_invalid");
|
|
287
|
+
return binding;
|
|
288
|
+
});
|
|
289
|
+
return ordered;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function uniformFromCore(bindings) {
|
|
293
|
+
const [first] = bindings;
|
|
294
|
+
return first && bindings.every(binding => sameCore(binding.fromCore, first.fromCore))
|
|
295
|
+
? cloneJson(first.fromCore, "from_core")
|
|
296
|
+
: null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function assertCompleteFleetDiscovery(discovery) {
|
|
300
|
+
if (
|
|
301
|
+
discovery?.schemaVersion !== "OwlCodaRunKitFleetDiscoveryV1"
|
|
302
|
+
|| discovery.authorizationGranted !== false
|
|
303
|
+
|| discovery.complete !== true
|
|
304
|
+
|| !Array.isArray(discovery.workspaceRoots)
|
|
305
|
+
|| discovery.workspaceRoots.length === 0
|
|
306
|
+
|| !Array.isArray(discovery.unreachableRoots)
|
|
307
|
+
|| discovery.unreachableRoots.length !== 0
|
|
308
|
+
|| !SHA256_REF.test(discovery.frozenManifestSha256)
|
|
309
|
+
) {
|
|
310
|
+
throw new Error("core_successor_fleet_incomplete");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function assertSourceCandidateBinding(frozen, verified) {
|
|
315
|
+
if (
|
|
316
|
+
frozen?.status !== "source_candidate_frozen"
|
|
317
|
+
|| frozen.authorizationGranted !== false
|
|
318
|
+
|| typeof frozen.candidatePath !== "string"
|
|
319
|
+
|| !SHA256_REF.test(frozen.candidateSha256)
|
|
320
|
+
|| verified?.status !== "valid"
|
|
321
|
+
|| verified.authorizationGranted !== false
|
|
322
|
+
|| verified.candidatePath !== frozen.candidatePath
|
|
323
|
+
|| verified.candidateSha256 !== frozen.candidateSha256
|
|
324
|
+
) {
|
|
325
|
+
throw new Error("core_successor_source_candidate_invalid");
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function planBody({
|
|
330
|
+
planId,
|
|
331
|
+
fleetCoreBindings,
|
|
332
|
+
toCore,
|
|
333
|
+
sourceCandidate,
|
|
334
|
+
fleetDiscoveryRequest,
|
|
335
|
+
fleetDiscovery,
|
|
336
|
+
}) {
|
|
337
|
+
const fromCore = uniformFromCore(fleetCoreBindings);
|
|
338
|
+
return {
|
|
339
|
+
schemaVersion: "OwlCodaRunKitCoreSuccessorPlanV1",
|
|
340
|
+
planId,
|
|
341
|
+
status: "frozen",
|
|
342
|
+
fromCore,
|
|
343
|
+
fromCoreMode: fromCore === null ? "per_project" : "uniform",
|
|
344
|
+
fromCoreSetSha256: coreBindingSetSha256(fleetCoreBindings),
|
|
345
|
+
fleetCoreBindings: cloneJson(fleetCoreBindings, "fleet_core_bindings"),
|
|
346
|
+
toCore: cloneJson(toCore, "to_core"),
|
|
347
|
+
sourceCandidate: {
|
|
348
|
+
candidatePath: sourceCandidate.candidatePath,
|
|
349
|
+
candidateSha256: sourceCandidate.candidateSha256,
|
|
350
|
+
sourceFingerprint: sourceCandidate.sourceFingerprint,
|
|
351
|
+
...(sourceCandidate.deliveryPacketPath
|
|
352
|
+
? { deliveryPacketPath: sourceCandidate.deliveryPacketPath }
|
|
353
|
+
: {}),
|
|
354
|
+
pathClosure: cloneJson(sourceCandidate.pathClosure, "path_closure"),
|
|
355
|
+
materializationProof: cloneJson(
|
|
356
|
+
sourceCandidate.materializationProof,
|
|
357
|
+
"materialization_proof",
|
|
358
|
+
),
|
|
359
|
+
},
|
|
360
|
+
fleetDiscoveryRequest: cloneJson(
|
|
361
|
+
fleetDiscoveryRequest,
|
|
362
|
+
"fleet_discovery_request",
|
|
363
|
+
),
|
|
364
|
+
fleetDiscovery: cloneJson(fleetDiscovery, "fleet_discovery"),
|
|
365
|
+
migrationContract: {
|
|
366
|
+
completeFleetRequired: true,
|
|
367
|
+
globalPreflightBeforeWrites: true,
|
|
368
|
+
ownerMigrationAuthorityRequired: true,
|
|
369
|
+
migrationScope: "project_runkit_config_and_local_migration_receipt_only",
|
|
370
|
+
projectOrder: "canonical_workspace_root",
|
|
371
|
+
},
|
|
372
|
+
repositoryActions: { ...REPOSITORY_ACTIONS },
|
|
373
|
+
authorizationGranted: false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function validatePlan(plan) {
|
|
378
|
+
if (
|
|
379
|
+
plan?.schemaVersion !== "OwlCodaRunKitCoreSuccessorPlanV1"
|
|
380
|
+
|| plan.status !== "frozen"
|
|
381
|
+
|| plan.authorizationGranted !== false
|
|
382
|
+
|| !SHA256_REF.test(plan.planSha256)
|
|
383
|
+
|| coreSuccessorArtifactSha256V1((({ planSha256, ...body }) => body)(plan))
|
|
384
|
+
!== plan.planSha256
|
|
385
|
+
|| plan.migrationContract?.completeFleetRequired !== true
|
|
386
|
+
|| plan.migrationContract?.globalPreflightBeforeWrites !== true
|
|
387
|
+
|| plan.migrationContract?.ownerMigrationAuthorityRequired !== true
|
|
388
|
+
|| JSON.stringify(plan.repositoryActions) !== JSON.stringify(REPOSITORY_ACTIONS)
|
|
389
|
+
) {
|
|
390
|
+
throw new Error("core_successor_plan_invalid");
|
|
391
|
+
}
|
|
392
|
+
assertIdentifier(plan.planId, "plan_id");
|
|
393
|
+
assertCoreIdentity(plan.toCore, "to_core");
|
|
394
|
+
const bindings = normalizedFleetCoreBindings(
|
|
395
|
+
plan.fleetDiscovery,
|
|
396
|
+
plan.fleetCoreBindings,
|
|
397
|
+
);
|
|
398
|
+
const expectedUniform = uniformFromCore(bindings);
|
|
399
|
+
if (
|
|
400
|
+
plan.fromCoreMode !== (expectedUniform === null ? "per_project" : "uniform")
|
|
401
|
+
|| (
|
|
402
|
+
expectedUniform === null
|
|
403
|
+
? plan.fromCore !== null
|
|
404
|
+
: !sameCore(plan.fromCore, expectedUniform)
|
|
405
|
+
)
|
|
406
|
+
|| plan.fromCoreSetSha256 !== coreBindingSetSha256(bindings)
|
|
407
|
+
) {
|
|
408
|
+
throw new Error("core_successor_fleet_core_bindings_invalid");
|
|
409
|
+
}
|
|
410
|
+
if (bindings.some(binding => sameCore(binding.fromCore, plan.toCore))) {
|
|
411
|
+
throw new Error("core_successor_core_transition_required");
|
|
412
|
+
}
|
|
413
|
+
assertCompleteFleetDiscovery(plan.fleetDiscovery);
|
|
414
|
+
if (
|
|
415
|
+
typeof plan.sourceCandidate?.candidatePath !== "string"
|
|
416
|
+
|| !SHA256_REF.test(plan.sourceCandidate?.candidateSha256)
|
|
417
|
+
|| plan.sourceCandidate?.pathClosure?.status !== "valid"
|
|
418
|
+
|| !SHA256_REF.test(plan.sourceCandidate?.pathClosure?.closureSha256)
|
|
419
|
+
|| plan.sourceCandidate?.materializationProof?.matchesToCore !== true
|
|
420
|
+
|| !sameCore(
|
|
421
|
+
plan.sourceCandidate?.materializationProof?.materializedCore,
|
|
422
|
+
plan.toCore,
|
|
423
|
+
)
|
|
424
|
+
) {
|
|
425
|
+
throw new Error("core_successor_plan_source_candidate_invalid");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validateOwnerAuthority(authority, plan) {
|
|
430
|
+
if (authority?.schemaVersion === "OwlCodaRunKitOwnerMigrationAuthorityV1") {
|
|
431
|
+
throw new Error("core_successor_owner_authority_untrusted");
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
authority?.schemaVersion !== "OwlCodaRunKitOwnerMigrationAuthorityV2"
|
|
435
|
+
|| typeof authority.authorityId !== "string"
|
|
436
|
+
|| authority.authorityId.length === 0
|
|
437
|
+
|| authority.decision !== "approved"
|
|
438
|
+
|| authority.scope !== "migrate_declared_core_successor_fleet"
|
|
439
|
+
|| authority.planSha256 !== plan.planSha256
|
|
440
|
+
|| authority.fromCoreSetSha256 !== plan.fromCoreSetSha256
|
|
441
|
+
|| authority.toCoreManifestSha256 !== plan.toCore.coreManifestSha256
|
|
442
|
+
|| authority.fleetManifestSha256
|
|
443
|
+
!== plan.fleetDiscovery.frozenManifestSha256
|
|
444
|
+
|| authority.authorizationGranted !== false
|
|
445
|
+
|| typeof authority.signerKeyId !== "string"
|
|
446
|
+
|| authority.signerKeyId.length === 0
|
|
447
|
+
|| authority.signatureAlgorithm !== "ed25519"
|
|
448
|
+
|| typeof authority.signature !== "string"
|
|
449
|
+
|| authority.signature.length === 0
|
|
450
|
+
|| !SHA256_REF.test(authority.authoritySha256)
|
|
451
|
+
|| ownerAuthorityArtifactSha256V1(authority) !== authority.authoritySha256
|
|
452
|
+
) {
|
|
453
|
+
throw new Error("core_successor_owner_authority_invalid");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function defaultDependencies(overrides = {}) {
|
|
458
|
+
return {
|
|
459
|
+
discoverFleet,
|
|
460
|
+
freezeSourceCandidate: freezeSourceCandidateV2,
|
|
461
|
+
verifySourceCandidate: verifySourceCandidateV2,
|
|
462
|
+
verifySourceCandidatePathClosure: verifySourceCandidatePathClosureV2,
|
|
463
|
+
materializeSourceCandidate: materializeSourceCandidateV2,
|
|
464
|
+
materializedCoreIdentity: coreIdentityFromSourceRoot,
|
|
465
|
+
async createMaterializationWorkspace({ sourceRoot }) {
|
|
466
|
+
const scratch = await mkdtemp(path.join(tmpdir(), "owlrunkit-core-successor-"));
|
|
467
|
+
const workspaceRoot = path.join(scratch, "materialized");
|
|
468
|
+
const cloned = spawnSync(
|
|
469
|
+
"git",
|
|
470
|
+
["clone", "--quiet", "--no-hardlinks", sourceRoot, workspaceRoot],
|
|
471
|
+
{ encoding: "utf8" },
|
|
472
|
+
);
|
|
473
|
+
if (cloned.error) throw cloned.error;
|
|
474
|
+
if (cloned.status !== 0) {
|
|
475
|
+
await rm(scratch, { recursive: true, force: true });
|
|
476
|
+
throw new Error("core_successor_materialization_clone_failed");
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
workspaceRoot,
|
|
480
|
+
async cleanup() {
|
|
481
|
+
await rm(scratch, { recursive: true, force: true });
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
inspectProjectControlState,
|
|
486
|
+
runtimeCoreIdentity: currentCoreIdentity,
|
|
487
|
+
verifyOwnerAuthority: ({ authority }) => verifyTrustedOwnerAuthorityV1({
|
|
488
|
+
authority,
|
|
489
|
+
expectedScope: "migrate_declared_core_successor_fleet",
|
|
490
|
+
expectedPurpose: "core_successor",
|
|
491
|
+
}),
|
|
492
|
+
migrateWorkspace: ({ workspaceRoot }) => initializeProjectRunKit({
|
|
493
|
+
workspaceRoot,
|
|
494
|
+
}),
|
|
495
|
+
...overrides,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function withFleetControlTransactions(workspaceRoots, operation, index = 0) {
|
|
500
|
+
if (index >= workspaceRoots.length) return operation();
|
|
501
|
+
return withControlTransaction(
|
|
502
|
+
workspaceRoots[index],
|
|
503
|
+
() => withFleetControlTransactions(workspaceRoots, operation, index + 1),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function proveMaterializedSourceCandidate({
|
|
508
|
+
root,
|
|
509
|
+
frozen,
|
|
510
|
+
toCore,
|
|
511
|
+
dependencies,
|
|
512
|
+
}) {
|
|
513
|
+
if (
|
|
514
|
+
!Array.isArray(frozen.ownedPaths)
|
|
515
|
+
|| frozen.ownedPaths.length === 0
|
|
516
|
+
) {
|
|
517
|
+
throw new Error("core_successor_source_candidate_owned_paths_missing");
|
|
518
|
+
}
|
|
519
|
+
const pathClosure = await dependencies.verifySourceCandidatePathClosure({
|
|
520
|
+
workspaceRoot: root,
|
|
521
|
+
candidatePath: frozen.candidatePath,
|
|
522
|
+
includedPaths: frozen.ownedPaths,
|
|
523
|
+
});
|
|
524
|
+
if (
|
|
525
|
+
pathClosure?.status !== "valid"
|
|
526
|
+
|| pathClosure.authorizationGranted !== false
|
|
527
|
+
|| !SHA256_REF.test(pathClosure.closureSha256)
|
|
528
|
+
) {
|
|
529
|
+
throw new Error("core_successor_source_candidate_path_closure_invalid");
|
|
530
|
+
}
|
|
531
|
+
const scratch = await dependencies.createMaterializationWorkspace({
|
|
532
|
+
sourceRoot: root,
|
|
533
|
+
});
|
|
534
|
+
const materializedRoot = scratch.workspaceRoot;
|
|
535
|
+
try {
|
|
536
|
+
const materialized = await dependencies.materializeSourceCandidate({
|
|
537
|
+
workspaceRoot: root,
|
|
538
|
+
candidatePath: frozen.candidatePath,
|
|
539
|
+
targetWorkspaceRoot: materializedRoot,
|
|
540
|
+
});
|
|
541
|
+
if (
|
|
542
|
+
materialized?.status !== "source_candidate_materialized"
|
|
543
|
+
|| materialized.authorizationGranted !== false
|
|
544
|
+
|| materialized.candidateSha256 !== frozen.candidateSha256
|
|
545
|
+
) {
|
|
546
|
+
throw new Error("core_successor_source_candidate_materialization_invalid");
|
|
547
|
+
}
|
|
548
|
+
const materializedCore = await dependencies.materializedCoreIdentity(
|
|
549
|
+
materializedRoot,
|
|
550
|
+
);
|
|
551
|
+
assertCoreIdentity(materializedCore, "materialized_core");
|
|
552
|
+
if (!sameCore(materializedCore, toCore)) {
|
|
553
|
+
throw new Error("core_successor_materialized_core_identity_mismatch");
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
pathClosure: cloneJson(pathClosure, "path_closure"),
|
|
557
|
+
materializationProof: {
|
|
558
|
+
candidateSha256: frozen.candidateSha256,
|
|
559
|
+
payloadSha256: materialized.payloadSha256,
|
|
560
|
+
materializedCore: cloneJson(materializedCore, "materialized_core"),
|
|
561
|
+
matchesToCore: true,
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
} finally {
|
|
565
|
+
await scratch.cleanup();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export async function createCoreSuccessorPlanV1({
|
|
570
|
+
controllerWorkspaceRoot,
|
|
571
|
+
planId,
|
|
572
|
+
planPath = null,
|
|
573
|
+
fromCore,
|
|
574
|
+
fleetCoreBindings = null,
|
|
575
|
+
toCore,
|
|
576
|
+
sourceCandidateRequest,
|
|
577
|
+
fleetDiscoveryRequest,
|
|
578
|
+
dependencies: dependencyOverrides = {},
|
|
579
|
+
} = {}) {
|
|
580
|
+
assertIdentifier(planId, "plan_id");
|
|
581
|
+
if (fleetCoreBindings === null) assertCoreIdentity(fromCore, "from_core");
|
|
582
|
+
assertCoreIdentity(toCore, "to_core");
|
|
583
|
+
if (fleetCoreBindings === null && sameCore(fromCore, toCore)) {
|
|
584
|
+
throw new Error("core_successor_core_transition_required");
|
|
585
|
+
}
|
|
586
|
+
const root = await canonicalWorkspaceRoot(controllerWorkspaceRoot);
|
|
587
|
+
const selectedPlanPath = planPath
|
|
588
|
+
?? `.owlcoda/runkit/core-successors/${planId}/plan.json`;
|
|
589
|
+
await prepareCreateOnlyPath(
|
|
590
|
+
root,
|
|
591
|
+
selectedPlanPath,
|
|
592
|
+
"core_successor_plan_exists",
|
|
593
|
+
);
|
|
594
|
+
const dependencies = defaultDependencies(dependencyOverrides);
|
|
595
|
+
const discoveryRequest = cloneJson(
|
|
596
|
+
fleetDiscoveryRequest,
|
|
597
|
+
"fleet_discovery_request",
|
|
598
|
+
);
|
|
599
|
+
const fleetDiscovery = await dependencies.discoverFleet(discoveryRequest);
|
|
600
|
+
assertCompleteFleetDiscovery(fleetDiscovery);
|
|
601
|
+
const bindings = normalizedFleetCoreBindings(
|
|
602
|
+
fleetDiscovery,
|
|
603
|
+
fleetCoreBindings ?? fleetDiscovery.workspaceRoots.map(workspaceRoot => ({
|
|
604
|
+
workspaceRoot,
|
|
605
|
+
fromCore,
|
|
606
|
+
})),
|
|
607
|
+
);
|
|
608
|
+
if (bindings.some(binding => sameCore(binding.fromCore, toCore))) {
|
|
609
|
+
throw new Error("core_successor_core_transition_required");
|
|
610
|
+
}
|
|
611
|
+
const frozen = await dependencies.freezeSourceCandidate({
|
|
612
|
+
workspaceRoot: root,
|
|
613
|
+
...sourceCandidateRequest,
|
|
614
|
+
});
|
|
615
|
+
const verified = await dependencies.verifySourceCandidate({
|
|
616
|
+
workspaceRoot: root,
|
|
617
|
+
candidatePath: frozen.candidatePath,
|
|
618
|
+
});
|
|
619
|
+
assertSourceCandidateBinding(frozen, verified);
|
|
620
|
+
const proof = await proveMaterializedSourceCandidate({
|
|
621
|
+
root,
|
|
622
|
+
frozen,
|
|
623
|
+
toCore,
|
|
624
|
+
dependencies,
|
|
625
|
+
});
|
|
626
|
+
const body = planBody({
|
|
627
|
+
planId,
|
|
628
|
+
fleetCoreBindings: bindings,
|
|
629
|
+
toCore,
|
|
630
|
+
sourceCandidate: {
|
|
631
|
+
...frozen,
|
|
632
|
+
...proof,
|
|
633
|
+
},
|
|
634
|
+
fleetDiscoveryRequest: discoveryRequest,
|
|
635
|
+
fleetDiscovery,
|
|
636
|
+
});
|
|
637
|
+
const plan = {
|
|
638
|
+
...body,
|
|
639
|
+
planSha256: coreSuccessorArtifactSha256V1(body),
|
|
640
|
+
};
|
|
641
|
+
const writtenPath = await writeCreateOnlyJson(
|
|
642
|
+
root,
|
|
643
|
+
selectedPlanPath,
|
|
644
|
+
plan,
|
|
645
|
+
"core_successor_plan_exists",
|
|
646
|
+
);
|
|
647
|
+
return {
|
|
648
|
+
status: "core_successor_plan_created",
|
|
649
|
+
exitCode: 0,
|
|
650
|
+
planId,
|
|
651
|
+
planPath: writtenPath,
|
|
652
|
+
planSha256: plan.planSha256,
|
|
653
|
+
sourceCandidateSha256: frozen.candidateSha256,
|
|
654
|
+
fromCoreMode: body.fromCoreMode,
|
|
655
|
+
fromCoreSetSha256: body.fromCoreSetSha256,
|
|
656
|
+
fleetManifestSha256: fleetDiscovery.frozenManifestSha256,
|
|
657
|
+
fleetSize: fleetDiscovery.workspaceRoots.length,
|
|
658
|
+
authorizationGranted: false,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function createCoreSuccessorPlanFromFleetV1({
|
|
663
|
+
controllerWorkspaceRoot,
|
|
664
|
+
planId,
|
|
665
|
+
planPath = null,
|
|
666
|
+
sourceCandidateRequest,
|
|
667
|
+
fleetDiscoveryRequest,
|
|
668
|
+
dependencies: dependencyOverrides = {},
|
|
669
|
+
} = {}) {
|
|
670
|
+
const dependencies = defaultDependencies(dependencyOverrides);
|
|
671
|
+
const discoveryRequest = cloneJson(
|
|
672
|
+
fleetDiscoveryRequest,
|
|
673
|
+
"fleet_discovery_request",
|
|
674
|
+
);
|
|
675
|
+
const fleetDiscovery = await dependencies.discoverFleet(discoveryRequest);
|
|
676
|
+
assertCompleteFleetDiscovery(fleetDiscovery);
|
|
677
|
+
const fleetCores = [];
|
|
678
|
+
for (const workspaceRoot of fleetDiscovery.workspaceRoots) {
|
|
679
|
+
const config = await readWorkspaceConfig(workspaceRoot);
|
|
680
|
+
const configGate = validateProjectConfigV2(config);
|
|
681
|
+
if (!configGate.valid) {
|
|
682
|
+
throw new Error("core_successor_fleet_config_invalid");
|
|
683
|
+
}
|
|
684
|
+
fleetCores.push({
|
|
685
|
+
workspaceRoot,
|
|
686
|
+
fromCore: config.core,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
if (fleetCores.length === 0) throw new Error("core_successor_fleet_incomplete");
|
|
690
|
+
const toCore = await dependencies.runtimeCoreIdentity();
|
|
691
|
+
assertCoreIdentity(toCore, "to_core");
|
|
692
|
+
const created = await createCoreSuccessorPlanV1({
|
|
693
|
+
controllerWorkspaceRoot,
|
|
694
|
+
planId,
|
|
695
|
+
planPath,
|
|
696
|
+
fleetCoreBindings: fleetCores,
|
|
697
|
+
toCore,
|
|
698
|
+
sourceCandidateRequest,
|
|
699
|
+
fleetDiscoveryRequest: discoveryRequest,
|
|
700
|
+
dependencies: {
|
|
701
|
+
...dependencyOverrides,
|
|
702
|
+
discoverFleet: async () => cloneJson(
|
|
703
|
+
fleetDiscovery,
|
|
704
|
+
"fleet_discovery",
|
|
705
|
+
),
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
return {
|
|
709
|
+
...created,
|
|
710
|
+
fromCore: uniformFromCore(fleetCores),
|
|
711
|
+
fromCoreMode: uniformFromCore(fleetCores) === null ? "per_project" : "uniform",
|
|
712
|
+
fromCoreSetSha256: coreBindingSetSha256(fleetCores),
|
|
713
|
+
toCore: cloneJson(toCore, "to_core"),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function readWorkspaceConfig(workspaceRoot) {
|
|
718
|
+
const root = await canonicalWorkspaceRoot(workspaceRoot);
|
|
719
|
+
const configPath = path.join(root, ".owlcoda/runkit/config.json");
|
|
720
|
+
try {
|
|
721
|
+
const stat = await lstat(configPath);
|
|
722
|
+
if (
|
|
723
|
+
stat.isSymbolicLink()
|
|
724
|
+
|| !stat.isFile()
|
|
725
|
+
|| stat.size > MAX_ARTIFACT_BYTES
|
|
726
|
+
|| await realpath(configPath) !== path.resolve(configPath)
|
|
727
|
+
) {
|
|
728
|
+
throw new Error("invalid");
|
|
729
|
+
}
|
|
730
|
+
return JSON.parse(await readFile(configPath, "utf8"));
|
|
731
|
+
} catch {
|
|
732
|
+
throw new Error("core_successor_project_config_invalid");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function discoveryMatchesPlan(current, frozen) {
|
|
737
|
+
return current?.complete === true
|
|
738
|
+
&& current.authorizationGranted === false
|
|
739
|
+
&& current.frozenManifestSha256 === frozen.frozenManifestSha256
|
|
740
|
+
&& JSON.stringify(current.workspaceRoots) === JSON.stringify(frozen.workspaceRoots)
|
|
741
|
+
&& JSON.stringify(current.coverageRoots) === JSON.stringify(frozen.coverageRoots)
|
|
742
|
+
&& JSON.stringify(current.unreachableRoots) === JSON.stringify(
|
|
743
|
+
frozen.unreachableRoots,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function preflightWorkspace({
|
|
748
|
+
workspaceRoot,
|
|
749
|
+
fromCore,
|
|
750
|
+
toCore,
|
|
751
|
+
successEvidence,
|
|
752
|
+
inspectControl,
|
|
753
|
+
}) {
|
|
754
|
+
const issueCodes = [];
|
|
755
|
+
let config;
|
|
756
|
+
let controlCore = fromCore;
|
|
757
|
+
try {
|
|
758
|
+
config = await readWorkspaceConfig(workspaceRoot);
|
|
759
|
+
const configGate = validateProjectConfigV2(config);
|
|
760
|
+
if (!configGate.valid) issueCodes.push("project_config_invalid");
|
|
761
|
+
if (configGate.valid) {
|
|
762
|
+
if (sameCore(config.core, fromCore)) {
|
|
763
|
+
if (successEvidence) {
|
|
764
|
+
issueCodes.push("project_success_receipt_config_drift");
|
|
765
|
+
}
|
|
766
|
+
} else if (sameCore(config.core, toCore)) {
|
|
767
|
+
controlCore = toCore;
|
|
768
|
+
if (!successEvidence) {
|
|
769
|
+
issueCodes.push("project_target_core_without_success_receipt");
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
issueCodes.push("project_core_drift");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
issueCodes.push("project_config_missing_or_invalid");
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
const control = await inspectControl({
|
|
780
|
+
workspaceRoot,
|
|
781
|
+
currentCore: controlCore,
|
|
782
|
+
});
|
|
783
|
+
if (control?.upgradeSafety?.status !== "safe") {
|
|
784
|
+
issueCodes.push("project_control_unsafe");
|
|
785
|
+
}
|
|
786
|
+
} catch {
|
|
787
|
+
issueCodes.push("project_control_unreadable");
|
|
788
|
+
}
|
|
789
|
+
const uniqueIssues = [...new Set(issueCodes)].sort();
|
|
790
|
+
return {
|
|
791
|
+
workspaceRoot,
|
|
792
|
+
fromCore: cloneJson(fromCore, "project_from_core"),
|
|
793
|
+
status: uniqueIssues.includes("project_core_drift")
|
|
794
|
+
? "core_drift"
|
|
795
|
+
: uniqueIssues.length > 0
|
|
796
|
+
? "unsafe"
|
|
797
|
+
: successEvidence
|
|
798
|
+
? "already_migrated"
|
|
799
|
+
: "safe",
|
|
800
|
+
issueCodes: uniqueIssues,
|
|
801
|
+
migrationAttempted: false,
|
|
802
|
+
...(successEvidence
|
|
803
|
+
? {
|
|
804
|
+
projectSuccessReceiptPath: successEvidence.receiptPath,
|
|
805
|
+
projectSuccessReceiptSha256: successEvidence.receiptSha256,
|
|
806
|
+
}
|
|
807
|
+
: {}),
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function batchReceiptBody({
|
|
812
|
+
plan,
|
|
813
|
+
ownerAuthority,
|
|
814
|
+
trustedAuthority,
|
|
815
|
+
receiptId,
|
|
816
|
+
status,
|
|
817
|
+
projects,
|
|
818
|
+
migrationAttempted,
|
|
819
|
+
attemptNumber,
|
|
820
|
+
receiptBasePath,
|
|
821
|
+
priorAttemptReceiptPath,
|
|
822
|
+
priorAttemptReceiptSha256,
|
|
823
|
+
projectSuccessReceipts,
|
|
824
|
+
orphanRecovery = false,
|
|
825
|
+
orphanSuccessReceiptCount = 0,
|
|
826
|
+
}) {
|
|
827
|
+
return {
|
|
828
|
+
schemaVersion: "OwlCodaRunKitCoreSuccessorBatchReceiptV1",
|
|
829
|
+
receiptId,
|
|
830
|
+
attemptNumber,
|
|
831
|
+
receiptBasePath,
|
|
832
|
+
priorAttemptReceiptPath,
|
|
833
|
+
priorAttemptReceiptSha256,
|
|
834
|
+
planId: plan.planId,
|
|
835
|
+
planSha256: plan.planSha256,
|
|
836
|
+
ownerAuthorityId: ownerAuthority.authorityId,
|
|
837
|
+
ownerAuthoritySha256: ownerAuthority.authoritySha256,
|
|
838
|
+
ownerAuthoritySignerKeyId: trustedAuthority.signerKeyId,
|
|
839
|
+
ownerAuthorityTrustStoreSha256: trustedAuthority.trustStoreSha256,
|
|
840
|
+
fromCore: cloneJson(plan.fromCore, "from_core"),
|
|
841
|
+
fromCoreMode: plan.fromCoreMode,
|
|
842
|
+
fromCoreSetSha256: plan.fromCoreSetSha256,
|
|
843
|
+
fleetCoreBindings: cloneJson(
|
|
844
|
+
plan.fleetCoreBindings,
|
|
845
|
+
"fleet_core_bindings",
|
|
846
|
+
),
|
|
847
|
+
toCore: cloneJson(plan.toCore, "to_core"),
|
|
848
|
+
fleetManifestSha256: plan.fleetDiscovery.frozenManifestSha256,
|
|
849
|
+
sourceCandidateSha256: plan.sourceCandidate.candidateSha256,
|
|
850
|
+
status,
|
|
851
|
+
migrationAttempted,
|
|
852
|
+
migratedCount: projectSuccessReceipts.length,
|
|
853
|
+
projectCount: projects.length,
|
|
854
|
+
orphanRecovery,
|
|
855
|
+
orphanSuccessReceiptCount,
|
|
856
|
+
projects: cloneJson(projects, "project_results"),
|
|
857
|
+
projectSuccessReceipts: cloneJson(
|
|
858
|
+
projectSuccessReceipts,
|
|
859
|
+
"project_success_receipts",
|
|
860
|
+
),
|
|
861
|
+
repositoryActions: { ...REPOSITORY_ACTIONS },
|
|
862
|
+
authorizationGranted: false,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function persistBatchReceipt({
|
|
867
|
+
root,
|
|
868
|
+
receiptPath,
|
|
869
|
+
plan,
|
|
870
|
+
ownerAuthority,
|
|
871
|
+
trustedAuthority,
|
|
872
|
+
receiptId,
|
|
873
|
+
status,
|
|
874
|
+
projects,
|
|
875
|
+
migrationAttempted,
|
|
876
|
+
attemptNumber,
|
|
877
|
+
receiptBasePath,
|
|
878
|
+
priorAttemptReceiptPath,
|
|
879
|
+
priorAttemptReceiptSha256,
|
|
880
|
+
projectSuccessReceipts,
|
|
881
|
+
orphanRecovery = false,
|
|
882
|
+
orphanSuccessReceiptCount = 0,
|
|
883
|
+
}) {
|
|
884
|
+
const body = batchReceiptBody({
|
|
885
|
+
plan,
|
|
886
|
+
ownerAuthority,
|
|
887
|
+
trustedAuthority,
|
|
888
|
+
receiptId,
|
|
889
|
+
status,
|
|
890
|
+
projects,
|
|
891
|
+
migrationAttempted,
|
|
892
|
+
attemptNumber,
|
|
893
|
+
receiptBasePath,
|
|
894
|
+
priorAttemptReceiptPath,
|
|
895
|
+
priorAttemptReceiptSha256,
|
|
896
|
+
projectSuccessReceipts,
|
|
897
|
+
orphanRecovery,
|
|
898
|
+
orphanSuccessReceiptCount,
|
|
899
|
+
});
|
|
900
|
+
const receipt = {
|
|
901
|
+
...body,
|
|
902
|
+
receiptSha256: coreSuccessorArtifactSha256V1(body),
|
|
903
|
+
};
|
|
904
|
+
const writtenPath = await writeCreateOnlyJson(
|
|
905
|
+
root,
|
|
906
|
+
receiptPath,
|
|
907
|
+
receipt,
|
|
908
|
+
"core_successor_receipt_exists",
|
|
909
|
+
);
|
|
910
|
+
return {
|
|
911
|
+
status,
|
|
912
|
+
exitCode: status === "applied" ? 0 : 2,
|
|
913
|
+
receiptId,
|
|
914
|
+
attemptNumber,
|
|
915
|
+
receiptPath: writtenPath,
|
|
916
|
+
receiptSha256: receipt.receiptSha256,
|
|
917
|
+
projectCount: projects.length,
|
|
918
|
+
migratedCount: receipt.migratedCount,
|
|
919
|
+
authorizationGranted: false,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function assertTrustedAuthority(trustedAuthority, ownerAuthority) {
|
|
924
|
+
if (
|
|
925
|
+
trustedAuthority?.status !== "trusted"
|
|
926
|
+
|| trustedAuthority.signerKeyId !== ownerAuthority.signerKeyId
|
|
927
|
+
|| trustedAuthority.authoritySha256 !== ownerAuthority.authoritySha256
|
|
928
|
+
) {
|
|
929
|
+
throw new Error("core_successor_owner_authority_untrusted");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function assertBatchReceiptSelfHash(receipt) {
|
|
934
|
+
if (
|
|
935
|
+
!SHA256_REF.test(receipt?.receiptSha256)
|
|
936
|
+
|| coreSuccessorArtifactSha256V1(
|
|
937
|
+
(({ receiptSha256, ...body }) => body)(receipt),
|
|
938
|
+
) !== receipt.receiptSha256
|
|
939
|
+
) {
|
|
940
|
+
throw new Error("core_successor_prior_attempt_receipt_invalid");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function validatePriorPartialReceipt({
|
|
945
|
+
receipt,
|
|
946
|
+
receiptPath,
|
|
947
|
+
plan,
|
|
948
|
+
ownerAuthority,
|
|
949
|
+
}) {
|
|
950
|
+
assertBatchReceiptSelfHash(receipt);
|
|
951
|
+
if (
|
|
952
|
+
receipt.schemaVersion !== "OwlCodaRunKitCoreSuccessorBatchReceiptV1"
|
|
953
|
+
|| receipt.status !== "partial_failure"
|
|
954
|
+
|| receipt.planId !== plan.planId
|
|
955
|
+
|| receipt.planSha256 !== plan.planSha256
|
|
956
|
+
|| receipt.ownerAuthorityId !== ownerAuthority.authorityId
|
|
957
|
+
|| receipt.ownerAuthoritySha256 !== ownerAuthority.authoritySha256
|
|
958
|
+
|| receipt.fromCoreSetSha256 !== plan.fromCoreSetSha256
|
|
959
|
+
|| receipt.fleetManifestSha256
|
|
960
|
+
!== plan.fleetDiscovery.frozenManifestSha256
|
|
961
|
+
|| receipt.sourceCandidateSha256
|
|
962
|
+
!== plan.sourceCandidate.candidateSha256
|
|
963
|
+
|| !sameCore(receipt.toCore, plan.toCore)
|
|
964
|
+
|| !Number.isInteger(receipt.attemptNumber)
|
|
965
|
+
|| receipt.attemptNumber < 1
|
|
966
|
+
|| typeof receipt.receiptBasePath !== "string"
|
|
967
|
+
|| receiptAttemptPath(
|
|
968
|
+
receipt.receiptBasePath,
|
|
969
|
+
receipt.attemptNumber,
|
|
970
|
+
) !== receiptPath
|
|
971
|
+
|| (
|
|
972
|
+
receipt.attemptNumber === 1
|
|
973
|
+
? receipt.priorAttemptReceiptPath !== null
|
|
974
|
+
|| receipt.priorAttemptReceiptSha256 !== null
|
|
975
|
+
: typeof receipt.priorAttemptReceiptPath !== "string"
|
|
976
|
+
|| !SHA256_REF.test(receipt.priorAttemptReceiptSha256)
|
|
977
|
+
|| receipt.priorAttemptReceiptPath !== receiptAttemptPath(
|
|
978
|
+
receipt.receiptBasePath,
|
|
979
|
+
receipt.attemptNumber - 1,
|
|
980
|
+
)
|
|
981
|
+
)
|
|
982
|
+
|| !Array.isArray(receipt.projects)
|
|
983
|
+
|| receipt.projects.length !== plan.fleetDiscovery.workspaceRoots.length
|
|
984
|
+
|| !Array.isArray(receipt.projectSuccessReceipts)
|
|
985
|
+
|| receipt.projectCount !== receipt.projects.length
|
|
986
|
+
|| receipt.migratedCount !== receipt.projectSuccessReceipts.length
|
|
987
|
+
|| typeof receipt.orphanRecovery !== "boolean"
|
|
988
|
+
|| !Number.isInteger(receipt.orphanSuccessReceiptCount)
|
|
989
|
+
|| receipt.orphanSuccessReceiptCount < 0
|
|
990
|
+
|| receipt.orphanSuccessReceiptCount > receipt.projectSuccessReceipts.length
|
|
991
|
+
|| receipt.orphanRecovery !== (receipt.orphanSuccessReceiptCount > 0)
|
|
992
|
+
|| receipt.migrationAttempted !== true
|
|
993
|
+
|| JSON.stringify(receipt.repositoryActions)
|
|
994
|
+
!== JSON.stringify(REPOSITORY_ACTIONS)
|
|
995
|
+
|| receipt.authorizationGranted !== false
|
|
996
|
+
) {
|
|
997
|
+
throw new Error("core_successor_prior_attempt_receipt_invalid");
|
|
998
|
+
}
|
|
999
|
+
const orderedRoots = receipt.projects.map(row => row.workspaceRoot);
|
|
1000
|
+
if (
|
|
1001
|
+
JSON.stringify(orderedRoots)
|
|
1002
|
+
!== JSON.stringify(plan.fleetDiscovery.workspaceRoots)
|
|
1003
|
+
) {
|
|
1004
|
+
throw new Error("core_successor_prior_attempt_project_order_invalid");
|
|
1005
|
+
}
|
|
1006
|
+
let failureSeen = false;
|
|
1007
|
+
const successRoots = [];
|
|
1008
|
+
for (const row of receipt.projects) {
|
|
1009
|
+
const binding = plan.fleetCoreBindings.find(
|
|
1010
|
+
candidate => candidate.workspaceRoot === row.workspaceRoot,
|
|
1011
|
+
);
|
|
1012
|
+
if (!binding || !sameCore(row.fromCore, binding.fromCore)) {
|
|
1013
|
+
throw new Error("core_successor_prior_attempt_receipt_invalid");
|
|
1014
|
+
}
|
|
1015
|
+
if (
|
|
1016
|
+
row.status === "migrated"
|
|
1017
|
+
|| row.status === "skipped_as_already_migrated"
|
|
1018
|
+
) {
|
|
1019
|
+
if (failureSeen) {
|
|
1020
|
+
throw new Error("core_successor_prior_attempt_project_order_invalid");
|
|
1021
|
+
}
|
|
1022
|
+
successRoots.push(row.workspaceRoot);
|
|
1023
|
+
} else if (row.status === "migration_failed") {
|
|
1024
|
+
if (failureSeen) {
|
|
1025
|
+
throw new Error("core_successor_prior_attempt_project_order_invalid");
|
|
1026
|
+
}
|
|
1027
|
+
failureSeen = true;
|
|
1028
|
+
} else if (row.status !== "not_attempted_after_failure" || !failureSeen) {
|
|
1029
|
+
throw new Error("core_successor_prior_attempt_project_order_invalid");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (!failureSeen) {
|
|
1033
|
+
throw new Error("core_successor_prior_attempt_receipt_invalid");
|
|
1034
|
+
}
|
|
1035
|
+
const successRefRoots = receipt.projectSuccessReceipts.map(
|
|
1036
|
+
row => row.workspaceRoot,
|
|
1037
|
+
);
|
|
1038
|
+
if (
|
|
1039
|
+
new Set(successRefRoots).size !== successRefRoots.length
|
|
1040
|
+
|| JSON.stringify(successRefRoots) !== JSON.stringify(successRoots)
|
|
1041
|
+
|| receipt.projectSuccessReceipts.some(reference => (
|
|
1042
|
+
reference.receiptBasePath !== receipt.receiptBasePath
|
|
1043
|
+
|| reference.receiptPath !== projectSuccessReceiptPath(
|
|
1044
|
+
receipt.receiptBasePath,
|
|
1045
|
+
receipt.receiptId,
|
|
1046
|
+
reference.workspaceRoot,
|
|
1047
|
+
)
|
|
1048
|
+
))
|
|
1049
|
+
) {
|
|
1050
|
+
throw new Error("core_successor_prior_attempt_success_set_invalid");
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function validatePriorAttemptLineage({
|
|
1055
|
+
root,
|
|
1056
|
+
receipt,
|
|
1057
|
+
receiptPath,
|
|
1058
|
+
plan,
|
|
1059
|
+
ownerAuthority,
|
|
1060
|
+
}) {
|
|
1061
|
+
let current = receipt;
|
|
1062
|
+
let currentPath = receiptPath;
|
|
1063
|
+
for (;;) {
|
|
1064
|
+
validatePriorPartialReceipt({
|
|
1065
|
+
receipt: current,
|
|
1066
|
+
receiptPath: currentPath,
|
|
1067
|
+
plan,
|
|
1068
|
+
ownerAuthority,
|
|
1069
|
+
});
|
|
1070
|
+
if (current.attemptNumber === 1) return;
|
|
1071
|
+
const parent = await readBoundedJson(
|
|
1072
|
+
root,
|
|
1073
|
+
current.priorAttemptReceiptPath,
|
|
1074
|
+
"prior_attempt_receipt_path",
|
|
1075
|
+
);
|
|
1076
|
+
if (
|
|
1077
|
+
parent.receiptSha256 !== current.priorAttemptReceiptSha256
|
|
1078
|
+
|| parent.attemptNumber !== current.attemptNumber - 1
|
|
1079
|
+
) {
|
|
1080
|
+
throw new Error("core_successor_prior_attempt_lineage_invalid");
|
|
1081
|
+
}
|
|
1082
|
+
current = parent;
|
|
1083
|
+
currentPath = current.priorAttemptReceiptPath
|
|
1084
|
+
? receiptAttemptPath(
|
|
1085
|
+
current.receiptBasePath,
|
|
1086
|
+
current.attemptNumber,
|
|
1087
|
+
)
|
|
1088
|
+
: current.receiptBasePath;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function assertNoResumeBranch({
|
|
1093
|
+
root,
|
|
1094
|
+
priorReceipt,
|
|
1095
|
+
}) {
|
|
1096
|
+
const basePath = safeRelativePath(
|
|
1097
|
+
priorReceipt.receiptBasePath,
|
|
1098
|
+
"receipt_base_path",
|
|
1099
|
+
);
|
|
1100
|
+
const directoryPath = path.resolve(root, path.dirname(basePath));
|
|
1101
|
+
const extension = path.extname(basePath);
|
|
1102
|
+
const baseName = path.basename(
|
|
1103
|
+
extension.length > 0
|
|
1104
|
+
? basePath.slice(0, -extension.length)
|
|
1105
|
+
: basePath,
|
|
1106
|
+
);
|
|
1107
|
+
const escapedBase = baseName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1108
|
+
const pattern = new RegExp(
|
|
1109
|
+
`^${escapedBase}-attempt-(\\d{3,})${extension.replace(
|
|
1110
|
+
/[.*+?^${}()|[\]\\]/gu,
|
|
1111
|
+
"\\$&",
|
|
1112
|
+
)}$`,
|
|
1113
|
+
"u",
|
|
1114
|
+
);
|
|
1115
|
+
let entries = [];
|
|
1116
|
+
try {
|
|
1117
|
+
entries = await readdir(directoryPath);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
if (error?.code !== "ENOENT") throw error;
|
|
1120
|
+
}
|
|
1121
|
+
for (const entry of entries) {
|
|
1122
|
+
const match = pattern.exec(entry);
|
|
1123
|
+
if (match && Number(match[1]) > priorReceipt.attemptNumber) {
|
|
1124
|
+
throw new Error("core_successor_resume_branch_exists");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async function validateProjectSuccessEvidence({
|
|
1130
|
+
root,
|
|
1131
|
+
reference,
|
|
1132
|
+
plan,
|
|
1133
|
+
ownerAuthority,
|
|
1134
|
+
receiptId,
|
|
1135
|
+
receiptBasePath,
|
|
1136
|
+
maximumAttemptNumber,
|
|
1137
|
+
expectedAttemptNumber = null,
|
|
1138
|
+
expectedPriorAttemptReceiptSha256 = undefined,
|
|
1139
|
+
}) {
|
|
1140
|
+
if (
|
|
1141
|
+
!reference
|
|
1142
|
+
|| typeof reference.workspaceRoot !== "string"
|
|
1143
|
+
|| typeof reference.receiptPath !== "string"
|
|
1144
|
+
|| !SHA256_REF.test(reference.receiptSha256)
|
|
1145
|
+
|| reference.receiptBasePath !== receiptBasePath
|
|
1146
|
+
|| reference.receiptPath !== projectSuccessReceiptPath(
|
|
1147
|
+
receiptBasePath,
|
|
1148
|
+
receiptId,
|
|
1149
|
+
reference.workspaceRoot,
|
|
1150
|
+
)
|
|
1151
|
+
) {
|
|
1152
|
+
throw new Error("core_successor_project_success_receipt_invalid");
|
|
1153
|
+
}
|
|
1154
|
+
const receipt = await readBoundedJson(
|
|
1155
|
+
root,
|
|
1156
|
+
reference.receiptPath,
|
|
1157
|
+
"project_success_receipt_path",
|
|
1158
|
+
);
|
|
1159
|
+
if (
|
|
1160
|
+
receipt.receiptSha256 !== reference.receiptSha256
|
|
1161
|
+
|| !SHA256_REF.test(receipt.receiptSha256)
|
|
1162
|
+
|| coreSuccessorArtifactSha256V1(
|
|
1163
|
+
(({ receiptSha256, ...body }) => body)(receipt),
|
|
1164
|
+
) !== receipt.receiptSha256
|
|
1165
|
+
|| receipt.schemaVersion
|
|
1166
|
+
!== "OwlCodaRunKitCoreSuccessorProjectSuccessReceiptV1"
|
|
1167
|
+
|| receipt.receiptId !== receiptId
|
|
1168
|
+
|| receipt.workspaceRoot !== reference.workspaceRoot
|
|
1169
|
+
|| receipt.planSha256 !== plan.planSha256
|
|
1170
|
+
|| receipt.ownerAuthoritySha256 !== ownerAuthority.authoritySha256
|
|
1171
|
+
|| receipt.fleetManifestSha256
|
|
1172
|
+
!== plan.fleetDiscovery.frozenManifestSha256
|
|
1173
|
+
|| !sameCore(receipt.fromCore, plan.fleetCoreBindings.find(
|
|
1174
|
+
row => row.workspaceRoot === receipt.workspaceRoot,
|
|
1175
|
+
)?.fromCore)
|
|
1176
|
+
|| !sameCore(receipt.toCore, plan.toCore)
|
|
1177
|
+
|| !Number.isInteger(receipt.attemptNumber)
|
|
1178
|
+
|| receipt.attemptNumber < 1
|
|
1179
|
+
|| receipt.attemptNumber > maximumAttemptNumber
|
|
1180
|
+
|| (
|
|
1181
|
+
expectedAttemptNumber !== null
|
|
1182
|
+
&& receipt.attemptNumber !== expectedAttemptNumber
|
|
1183
|
+
)
|
|
1184
|
+
|| (
|
|
1185
|
+
expectedPriorAttemptReceiptSha256 !== undefined
|
|
1186
|
+
&& receipt.priorAttemptReceiptSha256
|
|
1187
|
+
!== expectedPriorAttemptReceiptSha256
|
|
1188
|
+
)
|
|
1189
|
+
|| (
|
|
1190
|
+
receipt.attemptNumber === 1
|
|
1191
|
+
? receipt.priorAttemptReceiptSha256 !== null
|
|
1192
|
+
: !SHA256_REF.test(receipt.priorAttemptReceiptSha256)
|
|
1193
|
+
)
|
|
1194
|
+
|| JSON.stringify(receipt.repositoryActions)
|
|
1195
|
+
!== JSON.stringify(REPOSITORY_ACTIONS)
|
|
1196
|
+
|| receipt.authorizationGranted !== false
|
|
1197
|
+
) {
|
|
1198
|
+
throw new Error("core_successor_project_success_receipt_invalid");
|
|
1199
|
+
}
|
|
1200
|
+
const configArtifact = await readWorkspaceArtifact(
|
|
1201
|
+
receipt.workspaceRoot,
|
|
1202
|
+
".owlcoda/runkit/config.json",
|
|
1203
|
+
"post_config",
|
|
1204
|
+
);
|
|
1205
|
+
const configGate = validateProjectConfigV2(configArtifact.value);
|
|
1206
|
+
const migrationArtifact = await readWorkspaceArtifact(
|
|
1207
|
+
receipt.workspaceRoot,
|
|
1208
|
+
receipt.migrationReceiptPath,
|
|
1209
|
+
"migration_receipt",
|
|
1210
|
+
);
|
|
1211
|
+
if (
|
|
1212
|
+
!configGate.valid
|
|
1213
|
+
|| !sameCore(configArtifact.value.core, plan.toCore)
|
|
1214
|
+
|| configArtifact.sha256 !== receipt.postConfigSha256
|
|
1215
|
+
|| migrationArtifact.sha256 !== receipt.migrationReceiptSha256
|
|
1216
|
+
|| migrationArtifact.value?.schemaVersion
|
|
1217
|
+
!== "OwlCodaRunKitConfigMigrationReceiptV1"
|
|
1218
|
+
|| !sameCore(migrationArtifact.value.fromCore, receipt.fromCore)
|
|
1219
|
+
|| !sameCore(migrationArtifact.value.toCore, receipt.toCore)
|
|
1220
|
+
|| migrationArtifact.value.authorizationGranted !== false
|
|
1221
|
+
) {
|
|
1222
|
+
throw new Error("core_successor_project_success_evidence_drift");
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
...reference,
|
|
1226
|
+
receipt,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function orderProjectSuccessReferences(plan, references) {
|
|
1231
|
+
const byRoot = new Map(
|
|
1232
|
+
references.map(reference => [reference.workspaceRoot, reference]),
|
|
1233
|
+
);
|
|
1234
|
+
return plan.fleetDiscovery.workspaceRoots
|
|
1235
|
+
.filter(workspaceRoot => byRoot.has(workspaceRoot))
|
|
1236
|
+
.map(workspaceRoot => byRoot.get(workspaceRoot));
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
async function discoverOrphanProjectSuccessEvidence({
|
|
1240
|
+
root,
|
|
1241
|
+
plan,
|
|
1242
|
+
ownerAuthority,
|
|
1243
|
+
receiptId,
|
|
1244
|
+
receiptBasePath,
|
|
1245
|
+
attemptNumber,
|
|
1246
|
+
priorAttemptReceiptSha256,
|
|
1247
|
+
existingReferences,
|
|
1248
|
+
}) {
|
|
1249
|
+
const referencedRoots = new Set(
|
|
1250
|
+
existingReferences.map(reference => reference.workspaceRoot),
|
|
1251
|
+
);
|
|
1252
|
+
const discovered = [];
|
|
1253
|
+
for (const workspaceRoot of plan.fleetDiscovery.workspaceRoots) {
|
|
1254
|
+
if (referencedRoots.has(workspaceRoot)) continue;
|
|
1255
|
+
const receiptPath = projectSuccessReceiptPath(
|
|
1256
|
+
receiptBasePath,
|
|
1257
|
+
receiptId,
|
|
1258
|
+
workspaceRoot,
|
|
1259
|
+
);
|
|
1260
|
+
if (!await pathExists(path.resolve(root, receiptPath))) continue;
|
|
1261
|
+
const receipt = await readBoundedJson(
|
|
1262
|
+
root,
|
|
1263
|
+
receiptPath,
|
|
1264
|
+
"project_success_receipt_path",
|
|
1265
|
+
);
|
|
1266
|
+
const reference = {
|
|
1267
|
+
workspaceRoot,
|
|
1268
|
+
receiptBasePath,
|
|
1269
|
+
receiptPath,
|
|
1270
|
+
receiptSha256: receipt.receiptSha256,
|
|
1271
|
+
};
|
|
1272
|
+
const evidence = await validateProjectSuccessEvidence({
|
|
1273
|
+
root,
|
|
1274
|
+
reference,
|
|
1275
|
+
plan,
|
|
1276
|
+
ownerAuthority,
|
|
1277
|
+
receiptId,
|
|
1278
|
+
receiptBasePath,
|
|
1279
|
+
maximumAttemptNumber: attemptNumber,
|
|
1280
|
+
expectedAttemptNumber: attemptNumber,
|
|
1281
|
+
expectedPriorAttemptReceiptSha256: priorAttemptReceiptSha256,
|
|
1282
|
+
});
|
|
1283
|
+
discovered.push({ reference, evidence });
|
|
1284
|
+
}
|
|
1285
|
+
return discovered;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
async function matchingCommittedMigrationReceipt({
|
|
1289
|
+
workspaceRoot,
|
|
1290
|
+
fromCore,
|
|
1291
|
+
toCore,
|
|
1292
|
+
}) {
|
|
1293
|
+
const workspace = await canonicalWorkspaceRoot(workspaceRoot);
|
|
1294
|
+
const relativeRoot = ".owlcoda/runkit/config-migration-receipts";
|
|
1295
|
+
const absoluteRoot = path.resolve(workspace, relativeRoot);
|
|
1296
|
+
let rootStat;
|
|
1297
|
+
try {
|
|
1298
|
+
rootStat = await lstat(absoluteRoot);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
if (error?.code === "ENOENT") return null;
|
|
1301
|
+
throw error;
|
|
1302
|
+
}
|
|
1303
|
+
if (
|
|
1304
|
+
rootStat.isSymbolicLink()
|
|
1305
|
+
|| !rootStat.isDirectory()
|
|
1306
|
+
|| await realpath(absoluteRoot) !== absoluteRoot
|
|
1307
|
+
) {
|
|
1308
|
+
throw new Error("core_successor_migration_receipts_root_invalid");
|
|
1309
|
+
}
|
|
1310
|
+
const matches = [];
|
|
1311
|
+
for (const entry of (await readdir(absoluteRoot, {
|
|
1312
|
+
withFileTypes: true,
|
|
1313
|
+
})).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1314
|
+
if (
|
|
1315
|
+
entry.isSymbolicLink()
|
|
1316
|
+
|| !entry.isFile()
|
|
1317
|
+
|| !entry.name.endsWith(".json")
|
|
1318
|
+
) {
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const relativePath = `${relativeRoot}/${entry.name}`;
|
|
1322
|
+
const artifact = await readWorkspaceArtifact(
|
|
1323
|
+
workspace,
|
|
1324
|
+
relativePath,
|
|
1325
|
+
"migration_receipt",
|
|
1326
|
+
);
|
|
1327
|
+
if (
|
|
1328
|
+
artifact.value?.schemaVersion
|
|
1329
|
+
=== "OwlCodaRunKitConfigMigrationReceiptV1"
|
|
1330
|
+
&& sameCore(artifact.value.fromCore, fromCore)
|
|
1331
|
+
&& sameCore(artifact.value.toCore, toCore)
|
|
1332
|
+
&& artifact.value.authorizationGranted === false
|
|
1333
|
+
) {
|
|
1334
|
+
matches.push(relativePath);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return matches[0] ?? null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
async function recoverCommittedProjectSuccessEvidence({
|
|
1341
|
+
root,
|
|
1342
|
+
plan,
|
|
1343
|
+
ownerAuthority,
|
|
1344
|
+
receiptId,
|
|
1345
|
+
receiptBasePath,
|
|
1346
|
+
attemptNumber,
|
|
1347
|
+
priorAttemptReceiptSha256,
|
|
1348
|
+
existingReferences,
|
|
1349
|
+
}) {
|
|
1350
|
+
const referencedRoots = new Set(
|
|
1351
|
+
existingReferences.map(reference => reference.workspaceRoot),
|
|
1352
|
+
);
|
|
1353
|
+
const recovered = [];
|
|
1354
|
+
for (const binding of plan.fleetCoreBindings) {
|
|
1355
|
+
const workspaceRoot = binding.workspaceRoot;
|
|
1356
|
+
if (referencedRoots.has(workspaceRoot)) continue;
|
|
1357
|
+
let config;
|
|
1358
|
+
try {
|
|
1359
|
+
config = await readWorkspaceConfig(workspaceRoot);
|
|
1360
|
+
} catch {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
if (!sameCore(config.core, plan.toCore)) continue;
|
|
1364
|
+
const migrationReceipt = await matchingCommittedMigrationReceipt({
|
|
1365
|
+
workspaceRoot,
|
|
1366
|
+
fromCore: binding.fromCore,
|
|
1367
|
+
toCore: plan.toCore,
|
|
1368
|
+
});
|
|
1369
|
+
if (migrationReceipt === null) continue;
|
|
1370
|
+
const reference = await writeProjectSuccessReceipt({
|
|
1371
|
+
root,
|
|
1372
|
+
receiptBasePath,
|
|
1373
|
+
receiptId,
|
|
1374
|
+
attemptNumber,
|
|
1375
|
+
priorAttemptReceiptSha256,
|
|
1376
|
+
plan,
|
|
1377
|
+
ownerAuthority,
|
|
1378
|
+
workspaceRoot,
|
|
1379
|
+
fromCore: binding.fromCore,
|
|
1380
|
+
migrationResult: {
|
|
1381
|
+
status: "upgraded",
|
|
1382
|
+
migrationReceipt,
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
const evidence = await validateProjectSuccessEvidence({
|
|
1386
|
+
root,
|
|
1387
|
+
reference,
|
|
1388
|
+
plan,
|
|
1389
|
+
ownerAuthority,
|
|
1390
|
+
receiptId,
|
|
1391
|
+
receiptBasePath,
|
|
1392
|
+
maximumAttemptNumber: attemptNumber,
|
|
1393
|
+
expectedAttemptNumber: attemptNumber,
|
|
1394
|
+
expectedPriorAttemptReceiptSha256: priorAttemptReceiptSha256,
|
|
1395
|
+
});
|
|
1396
|
+
recovered.push({ reference, evidence });
|
|
1397
|
+
}
|
|
1398
|
+
return recovered;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
async function writeProjectSuccessReceipt({
|
|
1402
|
+
root,
|
|
1403
|
+
receiptBasePath,
|
|
1404
|
+
receiptId,
|
|
1405
|
+
attemptNumber,
|
|
1406
|
+
priorAttemptReceiptSha256,
|
|
1407
|
+
plan,
|
|
1408
|
+
ownerAuthority,
|
|
1409
|
+
workspaceRoot,
|
|
1410
|
+
fromCore,
|
|
1411
|
+
migrationResult,
|
|
1412
|
+
}) {
|
|
1413
|
+
const migrationArtifact = await readWorkspaceArtifact(
|
|
1414
|
+
workspaceRoot,
|
|
1415
|
+
migrationResult.migrationReceipt,
|
|
1416
|
+
"migration_receipt",
|
|
1417
|
+
);
|
|
1418
|
+
const configArtifact = await readWorkspaceArtifact(
|
|
1419
|
+
workspaceRoot,
|
|
1420
|
+
".owlcoda/runkit/config.json",
|
|
1421
|
+
"post_config",
|
|
1422
|
+
);
|
|
1423
|
+
const configGate = validateProjectConfigV2(configArtifact.value);
|
|
1424
|
+
if (
|
|
1425
|
+
migrationArtifact.value?.schemaVersion
|
|
1426
|
+
!== "OwlCodaRunKitConfigMigrationReceiptV1"
|
|
1427
|
+
|| !sameCore(migrationArtifact.value.fromCore, fromCore)
|
|
1428
|
+
|| !sameCore(migrationArtifact.value.toCore, plan.toCore)
|
|
1429
|
+
|| migrationArtifact.value.authorizationGranted !== false
|
|
1430
|
+
|| !configGate.valid
|
|
1431
|
+
|| !sameCore(configArtifact.value.core, plan.toCore)
|
|
1432
|
+
) {
|
|
1433
|
+
throw new Error("core_successor_migration_evidence_invalid");
|
|
1434
|
+
}
|
|
1435
|
+
const body = {
|
|
1436
|
+
schemaVersion: "OwlCodaRunKitCoreSuccessorProjectSuccessReceiptV1",
|
|
1437
|
+
receiptId,
|
|
1438
|
+
attemptNumber,
|
|
1439
|
+
priorAttemptReceiptSha256,
|
|
1440
|
+
workspaceRoot,
|
|
1441
|
+
fromCore: cloneJson(fromCore, "project_from_core"),
|
|
1442
|
+
toCore: cloneJson(plan.toCore, "to_core"),
|
|
1443
|
+
planSha256: plan.planSha256,
|
|
1444
|
+
ownerAuthoritySha256: ownerAuthority.authoritySha256,
|
|
1445
|
+
fleetManifestSha256: plan.fleetDiscovery.frozenManifestSha256,
|
|
1446
|
+
migrationStatus: migrationResult.status,
|
|
1447
|
+
migrationReceiptPath: migrationArtifact.relativePath,
|
|
1448
|
+
migrationReceiptSha256: migrationArtifact.sha256,
|
|
1449
|
+
postConfigSha256: configArtifact.sha256,
|
|
1450
|
+
repositoryActions: { ...REPOSITORY_ACTIONS },
|
|
1451
|
+
authorizationGranted: false,
|
|
1452
|
+
};
|
|
1453
|
+
const receipt = {
|
|
1454
|
+
...body,
|
|
1455
|
+
receiptSha256: coreSuccessorArtifactSha256V1(body),
|
|
1456
|
+
};
|
|
1457
|
+
const receiptPath = projectSuccessReceiptPath(
|
|
1458
|
+
receiptBasePath,
|
|
1459
|
+
receiptId,
|
|
1460
|
+
workspaceRoot,
|
|
1461
|
+
);
|
|
1462
|
+
await writeCreateOnlyJson(
|
|
1463
|
+
root,
|
|
1464
|
+
receiptPath,
|
|
1465
|
+
receipt,
|
|
1466
|
+
"core_successor_project_success_receipt_exists",
|
|
1467
|
+
);
|
|
1468
|
+
return {
|
|
1469
|
+
workspaceRoot,
|
|
1470
|
+
receiptBasePath,
|
|
1471
|
+
receiptPath,
|
|
1472
|
+
receiptSha256: receipt.receiptSha256,
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
async function performCoreSuccessorAttempt({
|
|
1477
|
+
root,
|
|
1478
|
+
planPath,
|
|
1479
|
+
plan,
|
|
1480
|
+
ownerAuthority,
|
|
1481
|
+
receiptId,
|
|
1482
|
+
receiptBasePath,
|
|
1483
|
+
selectedReceiptPath,
|
|
1484
|
+
attemptNumber,
|
|
1485
|
+
priorAttemptReceiptPath,
|
|
1486
|
+
priorAttemptReceiptSha256,
|
|
1487
|
+
priorReceipt,
|
|
1488
|
+
adoptOrphanSuccessReceipts = false,
|
|
1489
|
+
dependencies,
|
|
1490
|
+
}) {
|
|
1491
|
+
return withFleetControlTransactions(
|
|
1492
|
+
plan.fleetDiscovery.workspaceRoots,
|
|
1493
|
+
async () => {
|
|
1494
|
+
const currentPlan = await readBoundedJson(root, planPath, "plan_path");
|
|
1495
|
+
validatePlan(currentPlan);
|
|
1496
|
+
if (currentPlan.planSha256 !== plan.planSha256) {
|
|
1497
|
+
throw new Error("core_successor_plan_drift");
|
|
1498
|
+
}
|
|
1499
|
+
validateOwnerAuthority(ownerAuthority, currentPlan);
|
|
1500
|
+
let trustedAuthority;
|
|
1501
|
+
try {
|
|
1502
|
+
trustedAuthority = await dependencies.verifyOwnerAuthority({
|
|
1503
|
+
authority: ownerAuthority,
|
|
1504
|
+
plan: currentPlan,
|
|
1505
|
+
});
|
|
1506
|
+
} catch {
|
|
1507
|
+
throw new Error("core_successor_owner_authority_untrusted");
|
|
1508
|
+
}
|
|
1509
|
+
assertTrustedAuthority(trustedAuthority, ownerAuthority);
|
|
1510
|
+
|
|
1511
|
+
if (priorReceipt) {
|
|
1512
|
+
const currentPrior = await readBoundedJson(
|
|
1513
|
+
root,
|
|
1514
|
+
priorAttemptReceiptPath,
|
|
1515
|
+
"prior_attempt_receipt_path",
|
|
1516
|
+
);
|
|
1517
|
+
validatePriorPartialReceipt({
|
|
1518
|
+
receipt: currentPrior,
|
|
1519
|
+
receiptPath: priorAttemptReceiptPath,
|
|
1520
|
+
plan: currentPlan,
|
|
1521
|
+
ownerAuthority,
|
|
1522
|
+
});
|
|
1523
|
+
if (currentPrior.receiptSha256 !== priorAttemptReceiptSha256) {
|
|
1524
|
+
throw new Error("core_successor_prior_attempt_receipt_drift");
|
|
1525
|
+
}
|
|
1526
|
+
await validatePriorAttemptLineage({
|
|
1527
|
+
root,
|
|
1528
|
+
receipt: currentPrior,
|
|
1529
|
+
receiptPath: priorAttemptReceiptPath,
|
|
1530
|
+
plan: currentPlan,
|
|
1531
|
+
ownerAuthority,
|
|
1532
|
+
});
|
|
1533
|
+
await assertNoResumeBranch({ root, priorReceipt: currentPrior });
|
|
1534
|
+
} else if (await pathExists(path.resolve(root, selectedReceiptPath))) {
|
|
1535
|
+
const existing = await readBoundedJson(
|
|
1536
|
+
root,
|
|
1537
|
+
selectedReceiptPath,
|
|
1538
|
+
"receipt_path",
|
|
1539
|
+
);
|
|
1540
|
+
if (
|
|
1541
|
+
existing.receiptId === receiptId
|
|
1542
|
+
&& existing.planSha256 === currentPlan.planSha256
|
|
1543
|
+
&& existing.ownerAuthoritySha256 === ownerAuthority.authoritySha256
|
|
1544
|
+
&& existing.status === "partial_failure"
|
|
1545
|
+
) {
|
|
1546
|
+
throw new Error("core_successor_resume_required");
|
|
1547
|
+
}
|
|
1548
|
+
throw new Error("core_successor_receipt_exists");
|
|
1549
|
+
}
|
|
1550
|
+
await prepareCreateOnlyPath(
|
|
1551
|
+
root,
|
|
1552
|
+
selectedReceiptPath,
|
|
1553
|
+
"core_successor_receipt_exists",
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
const inheritedSuccessReferences = priorReceipt
|
|
1557
|
+
? priorReceipt.projectSuccessReceipts
|
|
1558
|
+
: [];
|
|
1559
|
+
const projectSuccessByRoot = new Map();
|
|
1560
|
+
for (const reference of inheritedSuccessReferences) {
|
|
1561
|
+
const evidence = await validateProjectSuccessEvidence({
|
|
1562
|
+
root,
|
|
1563
|
+
reference,
|
|
1564
|
+
plan: currentPlan,
|
|
1565
|
+
ownerAuthority,
|
|
1566
|
+
receiptId,
|
|
1567
|
+
receiptBasePath,
|
|
1568
|
+
maximumAttemptNumber: attemptNumber - 1,
|
|
1569
|
+
});
|
|
1570
|
+
projectSuccessByRoot.set(reference.workspaceRoot, evidence);
|
|
1571
|
+
}
|
|
1572
|
+
if (!priorReceipt && !adoptOrphanSuccessReceipts) {
|
|
1573
|
+
for (const workspaceRoot of currentPlan.fleetDiscovery.workspaceRoots) {
|
|
1574
|
+
const receiptPath = projectSuccessReceiptPath(
|
|
1575
|
+
receiptBasePath,
|
|
1576
|
+
receiptId,
|
|
1577
|
+
workspaceRoot,
|
|
1578
|
+
);
|
|
1579
|
+
if (await pathExists(path.resolve(root, receiptPath))) {
|
|
1580
|
+
throw new Error("core_successor_orphan_resume_required");
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const orphanSuccessEvidence = (
|
|
1585
|
+
priorReceipt || adoptOrphanSuccessReceipts
|
|
1586
|
+
)
|
|
1587
|
+
? await discoverOrphanProjectSuccessEvidence({
|
|
1588
|
+
root,
|
|
1589
|
+
plan: currentPlan,
|
|
1590
|
+
ownerAuthority,
|
|
1591
|
+
receiptId,
|
|
1592
|
+
receiptBasePath,
|
|
1593
|
+
attemptNumber,
|
|
1594
|
+
priorAttemptReceiptSha256,
|
|
1595
|
+
existingReferences: inheritedSuccessReferences,
|
|
1596
|
+
})
|
|
1597
|
+
: [];
|
|
1598
|
+
const recoveredCommittedEvidence = (
|
|
1599
|
+
priorReceipt || adoptOrphanSuccessReceipts
|
|
1600
|
+
)
|
|
1601
|
+
? await recoverCommittedProjectSuccessEvidence({
|
|
1602
|
+
root,
|
|
1603
|
+
plan: currentPlan,
|
|
1604
|
+
ownerAuthority,
|
|
1605
|
+
receiptId,
|
|
1606
|
+
receiptBasePath,
|
|
1607
|
+
attemptNumber,
|
|
1608
|
+
priorAttemptReceiptSha256,
|
|
1609
|
+
existingReferences: [
|
|
1610
|
+
...inheritedSuccessReferences,
|
|
1611
|
+
...orphanSuccessEvidence.map(row => row.reference),
|
|
1612
|
+
],
|
|
1613
|
+
})
|
|
1614
|
+
: [];
|
|
1615
|
+
if (
|
|
1616
|
+
adoptOrphanSuccessReceipts
|
|
1617
|
+
&& !priorReceipt
|
|
1618
|
+
&& orphanSuccessEvidence.length === 0
|
|
1619
|
+
&& recoveredCommittedEvidence.length === 0
|
|
1620
|
+
) {
|
|
1621
|
+
throw new Error("core_successor_orphan_success_receipts_missing");
|
|
1622
|
+
}
|
|
1623
|
+
for (const { reference, evidence } of [
|
|
1624
|
+
...orphanSuccessEvidence,
|
|
1625
|
+
...recoveredCommittedEvidence,
|
|
1626
|
+
]) {
|
|
1627
|
+
projectSuccessByRoot.set(reference.workspaceRoot, evidence);
|
|
1628
|
+
}
|
|
1629
|
+
const priorSuccessReferences = orderProjectSuccessReferences(
|
|
1630
|
+
currentPlan,
|
|
1631
|
+
[
|
|
1632
|
+
...inheritedSuccessReferences,
|
|
1633
|
+
...orphanSuccessEvidence.map(row => row.reference),
|
|
1634
|
+
...recoveredCommittedEvidence.map(row => row.reference),
|
|
1635
|
+
],
|
|
1636
|
+
);
|
|
1637
|
+
const orphanSuccessReceiptCount = (
|
|
1638
|
+
orphanSuccessEvidence.length + recoveredCommittedEvidence.length
|
|
1639
|
+
);
|
|
1640
|
+
const orphanRecovery = orphanSuccessReceiptCount > 0;
|
|
1641
|
+
|
|
1642
|
+
const runtimeGate = validateExecutionPin({
|
|
1643
|
+
expected: currentPlan.toCore,
|
|
1644
|
+
actual: await dependencies.runtimeCoreIdentity(),
|
|
1645
|
+
});
|
|
1646
|
+
if (runtimeGate.status !== "valid") {
|
|
1647
|
+
const projects = currentPlan.fleetDiscovery.workspaceRoots.map(
|
|
1648
|
+
workspaceRoot => ({
|
|
1649
|
+
workspaceRoot,
|
|
1650
|
+
fromCore: cloneJson(
|
|
1651
|
+
currentPlan.fleetCoreBindings.find(
|
|
1652
|
+
binding => binding.workspaceRoot === workspaceRoot,
|
|
1653
|
+
).fromCore,
|
|
1654
|
+
"project_from_core",
|
|
1655
|
+
),
|
|
1656
|
+
status: "core_drift",
|
|
1657
|
+
issueCodes: ["runtime_core_drift"],
|
|
1658
|
+
migrationAttempted: false,
|
|
1659
|
+
}),
|
|
1660
|
+
);
|
|
1661
|
+
return persistBatchReceipt({
|
|
1662
|
+
root,
|
|
1663
|
+
receiptPath: selectedReceiptPath,
|
|
1664
|
+
plan: currentPlan,
|
|
1665
|
+
ownerAuthority,
|
|
1666
|
+
trustedAuthority,
|
|
1667
|
+
receiptId,
|
|
1668
|
+
status: "blocked_preflight",
|
|
1669
|
+
projects,
|
|
1670
|
+
migrationAttempted: false,
|
|
1671
|
+
attemptNumber,
|
|
1672
|
+
receiptBasePath,
|
|
1673
|
+
priorAttemptReceiptPath,
|
|
1674
|
+
priorAttemptReceiptSha256,
|
|
1675
|
+
projectSuccessReceipts: priorSuccessReferences,
|
|
1676
|
+
orphanRecovery,
|
|
1677
|
+
orphanSuccessReceiptCount,
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const candidateGate = await dependencies.verifySourceCandidate({
|
|
1682
|
+
workspaceRoot: root,
|
|
1683
|
+
candidatePath: currentPlan.sourceCandidate.candidatePath,
|
|
1684
|
+
});
|
|
1685
|
+
const candidateValid = candidateGate?.status === "valid"
|
|
1686
|
+
&& candidateGate.authorizationGranted === false
|
|
1687
|
+
&& candidateGate.candidateSha256
|
|
1688
|
+
=== currentPlan.sourceCandidate.candidateSha256;
|
|
1689
|
+
const currentFleet = await dependencies.discoverFleet(
|
|
1690
|
+
cloneJson(
|
|
1691
|
+
currentPlan.fleetDiscoveryRequest,
|
|
1692
|
+
"fleet_discovery_request",
|
|
1693
|
+
),
|
|
1694
|
+
);
|
|
1695
|
+
const fleetValid = discoveryMatchesPlan(
|
|
1696
|
+
currentFleet,
|
|
1697
|
+
currentPlan.fleetDiscovery,
|
|
1698
|
+
);
|
|
1699
|
+
if (!candidateValid || !fleetValid) {
|
|
1700
|
+
const issueCodes = [
|
|
1701
|
+
...(!candidateValid ? ["source_candidate_drift"] : []),
|
|
1702
|
+
...(!fleetValid ? ["fleet_manifest_drift_or_incomplete"] : []),
|
|
1703
|
+
];
|
|
1704
|
+
const projects = currentPlan.fleetDiscovery.workspaceRoots.map(
|
|
1705
|
+
workspaceRoot => ({
|
|
1706
|
+
workspaceRoot,
|
|
1707
|
+
fromCore: cloneJson(
|
|
1708
|
+
currentPlan.fleetCoreBindings.find(
|
|
1709
|
+
binding => binding.workspaceRoot === workspaceRoot,
|
|
1710
|
+
).fromCore,
|
|
1711
|
+
"project_from_core",
|
|
1712
|
+
),
|
|
1713
|
+
status: "unsafe",
|
|
1714
|
+
issueCodes,
|
|
1715
|
+
migrationAttempted: false,
|
|
1716
|
+
}),
|
|
1717
|
+
);
|
|
1718
|
+
return persistBatchReceipt({
|
|
1719
|
+
root,
|
|
1720
|
+
receiptPath: selectedReceiptPath,
|
|
1721
|
+
plan: currentPlan,
|
|
1722
|
+
ownerAuthority,
|
|
1723
|
+
trustedAuthority,
|
|
1724
|
+
receiptId,
|
|
1725
|
+
status: "blocked_preflight",
|
|
1726
|
+
projects,
|
|
1727
|
+
migrationAttempted: false,
|
|
1728
|
+
attemptNumber,
|
|
1729
|
+
receiptBasePath,
|
|
1730
|
+
priorAttemptReceiptPath,
|
|
1731
|
+
priorAttemptReceiptSha256,
|
|
1732
|
+
projectSuccessReceipts: priorSuccessReferences,
|
|
1733
|
+
orphanRecovery,
|
|
1734
|
+
orphanSuccessReceiptCount,
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
const preflight = [];
|
|
1739
|
+
for (const workspaceRoot of currentFleet.workspaceRoots) {
|
|
1740
|
+
const binding = currentPlan.fleetCoreBindings.find(
|
|
1741
|
+
candidate => candidate.workspaceRoot === workspaceRoot,
|
|
1742
|
+
);
|
|
1743
|
+
const successEvidence = projectSuccessByRoot.get(workspaceRoot) ?? null;
|
|
1744
|
+
const row = await preflightWorkspace({
|
|
1745
|
+
workspaceRoot,
|
|
1746
|
+
fromCore: binding.fromCore,
|
|
1747
|
+
toCore: currentPlan.toCore,
|
|
1748
|
+
successEvidence,
|
|
1749
|
+
inspectControl: dependencies.inspectProjectControlState,
|
|
1750
|
+
});
|
|
1751
|
+
preflight.push(row);
|
|
1752
|
+
}
|
|
1753
|
+
if (preflight.some(row => !new Set([
|
|
1754
|
+
"safe",
|
|
1755
|
+
"already_migrated",
|
|
1756
|
+
]).has(row.status))) {
|
|
1757
|
+
return persistBatchReceipt({
|
|
1758
|
+
root,
|
|
1759
|
+
receiptPath: selectedReceiptPath,
|
|
1760
|
+
plan: currentPlan,
|
|
1761
|
+
ownerAuthority,
|
|
1762
|
+
trustedAuthority,
|
|
1763
|
+
receiptId,
|
|
1764
|
+
status: "blocked_preflight",
|
|
1765
|
+
projects: preflight,
|
|
1766
|
+
migrationAttempted: false,
|
|
1767
|
+
attemptNumber,
|
|
1768
|
+
receiptBasePath,
|
|
1769
|
+
priorAttemptReceiptPath,
|
|
1770
|
+
priorAttemptReceiptSha256,
|
|
1771
|
+
projectSuccessReceipts: priorSuccessReferences,
|
|
1772
|
+
orphanRecovery,
|
|
1773
|
+
orphanSuccessReceiptCount,
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
const projects = [];
|
|
1778
|
+
const projectSuccessReceipts = [...priorSuccessReferences];
|
|
1779
|
+
let failed = false;
|
|
1780
|
+
let migrationAttempted = false;
|
|
1781
|
+
for (const row of preflight) {
|
|
1782
|
+
if (row.status === "already_migrated") {
|
|
1783
|
+
projects.push({
|
|
1784
|
+
...row,
|
|
1785
|
+
status: "skipped_as_already_migrated",
|
|
1786
|
+
});
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (failed) {
|
|
1790
|
+
projects.push({
|
|
1791
|
+
...row,
|
|
1792
|
+
status: "not_attempted_after_failure",
|
|
1793
|
+
});
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
migrationAttempted = true;
|
|
1797
|
+
try {
|
|
1798
|
+
const result = await dependencies.migrateWorkspace({
|
|
1799
|
+
workspaceRoot: row.workspaceRoot,
|
|
1800
|
+
fromCore: cloneJson(row.fromCore, "from_core"),
|
|
1801
|
+
toCore: cloneJson(currentPlan.toCore, "to_core"),
|
|
1802
|
+
planSha256: currentPlan.planSha256,
|
|
1803
|
+
ownerAuthoritySha256: ownerAuthority.authoritySha256,
|
|
1804
|
+
});
|
|
1805
|
+
const config = await readWorkspaceConfig(row.workspaceRoot);
|
|
1806
|
+
if (
|
|
1807
|
+
!new Set(["upgraded", "initialized"]).has(result?.status)
|
|
1808
|
+
|| typeof result.migrationReceipt !== "string"
|
|
1809
|
+
|| validateExecutionPin({
|
|
1810
|
+
expected: currentPlan.toCore,
|
|
1811
|
+
actual: result.core,
|
|
1812
|
+
}).status !== "valid"
|
|
1813
|
+
|| validateExecutionPin({
|
|
1814
|
+
expected: currentPlan.toCore,
|
|
1815
|
+
actual: config.core,
|
|
1816
|
+
}).status !== "valid"
|
|
1817
|
+
) {
|
|
1818
|
+
throw new Error("migration_result_invalid");
|
|
1819
|
+
}
|
|
1820
|
+
const successReference = await writeProjectSuccessReceipt({
|
|
1821
|
+
root,
|
|
1822
|
+
receiptBasePath,
|
|
1823
|
+
receiptId,
|
|
1824
|
+
attemptNumber,
|
|
1825
|
+
priorAttemptReceiptSha256,
|
|
1826
|
+
plan: currentPlan,
|
|
1827
|
+
ownerAuthority,
|
|
1828
|
+
workspaceRoot: row.workspaceRoot,
|
|
1829
|
+
fromCore: row.fromCore,
|
|
1830
|
+
migrationResult: result,
|
|
1831
|
+
});
|
|
1832
|
+
projectSuccessReceipts.push(successReference);
|
|
1833
|
+
projects.push({
|
|
1834
|
+
workspaceRoot: row.workspaceRoot,
|
|
1835
|
+
fromCore: cloneJson(row.fromCore, "project_from_core"),
|
|
1836
|
+
status: "migrated",
|
|
1837
|
+
issueCodes: [],
|
|
1838
|
+
migrationAttempted: true,
|
|
1839
|
+
migrationStatus: result.status,
|
|
1840
|
+
migrationReceipt: result.migrationReceipt,
|
|
1841
|
+
migrationReceiptSha256: (
|
|
1842
|
+
await readWorkspaceArtifact(
|
|
1843
|
+
row.workspaceRoot,
|
|
1844
|
+
result.migrationReceipt,
|
|
1845
|
+
"migration_receipt",
|
|
1846
|
+
)
|
|
1847
|
+
).sha256,
|
|
1848
|
+
projectSuccessReceiptPath: successReference.receiptPath,
|
|
1849
|
+
projectSuccessReceiptSha256: successReference.receiptSha256,
|
|
1850
|
+
});
|
|
1851
|
+
} catch {
|
|
1852
|
+
failed = true;
|
|
1853
|
+
projects.push({
|
|
1854
|
+
workspaceRoot: row.workspaceRoot,
|
|
1855
|
+
fromCore: cloneJson(row.fromCore, "project_from_core"),
|
|
1856
|
+
status: "migration_failed",
|
|
1857
|
+
issueCodes: ["project_migration_failed"],
|
|
1858
|
+
migrationAttempted: true,
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
return persistBatchReceipt({
|
|
1863
|
+
root,
|
|
1864
|
+
receiptPath: selectedReceiptPath,
|
|
1865
|
+
plan: currentPlan,
|
|
1866
|
+
ownerAuthority,
|
|
1867
|
+
trustedAuthority,
|
|
1868
|
+
receiptId,
|
|
1869
|
+
status: failed ? "partial_failure" : "applied",
|
|
1870
|
+
projects,
|
|
1871
|
+
migrationAttempted,
|
|
1872
|
+
attemptNumber,
|
|
1873
|
+
receiptBasePath,
|
|
1874
|
+
priorAttemptReceiptPath,
|
|
1875
|
+
priorAttemptReceiptSha256,
|
|
1876
|
+
projectSuccessReceipts,
|
|
1877
|
+
orphanRecovery,
|
|
1878
|
+
orphanSuccessReceiptCount,
|
|
1879
|
+
});
|
|
1880
|
+
},
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
export async function applyCoreSuccessorPlanV1({
|
|
1885
|
+
controllerWorkspaceRoot,
|
|
1886
|
+
planPath,
|
|
1887
|
+
receiptId,
|
|
1888
|
+
receiptPath = null,
|
|
1889
|
+
ownerAuthority,
|
|
1890
|
+
dependencies: dependencyOverrides = {},
|
|
1891
|
+
} = {}) {
|
|
1892
|
+
assertIdentifier(receiptId, "receipt_id");
|
|
1893
|
+
const root = await canonicalWorkspaceRoot(controllerWorkspaceRoot);
|
|
1894
|
+
const plan = await readBoundedJson(root, planPath, "plan_path");
|
|
1895
|
+
validatePlan(plan);
|
|
1896
|
+
validateOwnerAuthority(ownerAuthority, plan);
|
|
1897
|
+
const receiptBasePath = receiptPath
|
|
1898
|
+
?? `${path.dirname(safeRelativePath(planPath, "plan_path"))}`
|
|
1899
|
+
+ `/apply-receipts/${receiptId}.json`;
|
|
1900
|
+
const selectedReceiptPath = safeRelativePath(
|
|
1901
|
+
receiptBasePath,
|
|
1902
|
+
"receipt_path",
|
|
1903
|
+
);
|
|
1904
|
+
if (await pathExists(path.resolve(root, selectedReceiptPath))) {
|
|
1905
|
+
const existing = await readBoundedJson(
|
|
1906
|
+
root,
|
|
1907
|
+
selectedReceiptPath,
|
|
1908
|
+
"receipt_path",
|
|
1909
|
+
);
|
|
1910
|
+
assertBatchReceiptSelfHash(existing);
|
|
1911
|
+
if (
|
|
1912
|
+
existing.receiptId === receiptId
|
|
1913
|
+
&& existing.planSha256 === plan.planSha256
|
|
1914
|
+
&& existing.ownerAuthoritySha256 === ownerAuthority.authoritySha256
|
|
1915
|
+
&& existing.status === "partial_failure"
|
|
1916
|
+
) {
|
|
1917
|
+
throw new Error("core_successor_resume_required");
|
|
1918
|
+
}
|
|
1919
|
+
throw new Error("core_successor_receipt_exists");
|
|
1920
|
+
}
|
|
1921
|
+
return performCoreSuccessorAttempt({
|
|
1922
|
+
root,
|
|
1923
|
+
planPath,
|
|
1924
|
+
plan,
|
|
1925
|
+
ownerAuthority,
|
|
1926
|
+
receiptId,
|
|
1927
|
+
receiptBasePath: selectedReceiptPath,
|
|
1928
|
+
selectedReceiptPath,
|
|
1929
|
+
attemptNumber: 1,
|
|
1930
|
+
priorAttemptReceiptPath: null,
|
|
1931
|
+
priorAttemptReceiptSha256: null,
|
|
1932
|
+
priorReceipt: null,
|
|
1933
|
+
dependencies: defaultDependencies(dependencyOverrides),
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
export async function resumeCoreSuccessorPlanV1({
|
|
1938
|
+
controllerWorkspaceRoot,
|
|
1939
|
+
planPath,
|
|
1940
|
+
fromReceiptPath = null,
|
|
1941
|
+
receiptId = null,
|
|
1942
|
+
receiptPath = null,
|
|
1943
|
+
adoptOrphanSuccessReceipts = false,
|
|
1944
|
+
ownerAuthority,
|
|
1945
|
+
dependencies: dependencyOverrides = {},
|
|
1946
|
+
} = {}) {
|
|
1947
|
+
const root = await canonicalWorkspaceRoot(controllerWorkspaceRoot);
|
|
1948
|
+
const plan = await readBoundedJson(root, planPath, "plan_path");
|
|
1949
|
+
validatePlan(plan);
|
|
1950
|
+
validateOwnerAuthority(ownerAuthority, plan);
|
|
1951
|
+
if (fromReceiptPath === null) {
|
|
1952
|
+
if (!adoptOrphanSuccessReceipts) {
|
|
1953
|
+
throw new Error("core_successor_prior_attempt_receipt_required");
|
|
1954
|
+
}
|
|
1955
|
+
assertIdentifier(receiptId, "receipt_id");
|
|
1956
|
+
const receiptBasePath = receiptPath
|
|
1957
|
+
?? `${path.dirname(safeRelativePath(planPath, "plan_path"))}`
|
|
1958
|
+
+ `/apply-receipts/${receiptId}.json`;
|
|
1959
|
+
const selectedReceiptPath = safeRelativePath(
|
|
1960
|
+
receiptBasePath,
|
|
1961
|
+
"receipt_path",
|
|
1962
|
+
);
|
|
1963
|
+
if (await pathExists(path.resolve(root, selectedReceiptPath))) {
|
|
1964
|
+
const existing = await readBoundedJson(
|
|
1965
|
+
root,
|
|
1966
|
+
selectedReceiptPath,
|
|
1967
|
+
"receipt_path",
|
|
1968
|
+
);
|
|
1969
|
+
assertBatchReceiptSelfHash(existing);
|
|
1970
|
+
if (
|
|
1971
|
+
existing.receiptId === receiptId
|
|
1972
|
+
&& existing.planSha256 === plan.planSha256
|
|
1973
|
+
&& existing.ownerAuthoritySha256 === ownerAuthority.authoritySha256
|
|
1974
|
+
&& existing.status === "partial_failure"
|
|
1975
|
+
) {
|
|
1976
|
+
throw new Error("core_successor_resume_from_receipt_required");
|
|
1977
|
+
}
|
|
1978
|
+
throw new Error("core_successor_receipt_exists");
|
|
1979
|
+
}
|
|
1980
|
+
return performCoreSuccessorAttempt({
|
|
1981
|
+
root,
|
|
1982
|
+
planPath,
|
|
1983
|
+
plan,
|
|
1984
|
+
ownerAuthority,
|
|
1985
|
+
receiptId,
|
|
1986
|
+
receiptBasePath: selectedReceiptPath,
|
|
1987
|
+
selectedReceiptPath,
|
|
1988
|
+
attemptNumber: 1,
|
|
1989
|
+
priorAttemptReceiptPath: null,
|
|
1990
|
+
priorAttemptReceiptSha256: null,
|
|
1991
|
+
priorReceipt: null,
|
|
1992
|
+
adoptOrphanSuccessReceipts: true,
|
|
1993
|
+
dependencies: defaultDependencies(dependencyOverrides),
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
if (adoptOrphanSuccessReceipts || receiptId !== null || receiptPath !== null) {
|
|
1997
|
+
throw new Error("core_successor_resume_mode_ambiguous");
|
|
1998
|
+
}
|
|
1999
|
+
const selectedPriorPath = safeRelativePath(
|
|
2000
|
+
fromReceiptPath,
|
|
2001
|
+
"prior_attempt_receipt_path",
|
|
2002
|
+
);
|
|
2003
|
+
const priorReceipt = await readBoundedJson(
|
|
2004
|
+
root,
|
|
2005
|
+
selectedPriorPath,
|
|
2006
|
+
"prior_attempt_receipt_path",
|
|
2007
|
+
);
|
|
2008
|
+
validatePriorPartialReceipt({
|
|
2009
|
+
receipt: priorReceipt,
|
|
2010
|
+
receiptPath: selectedPriorPath,
|
|
2011
|
+
plan,
|
|
2012
|
+
ownerAuthority,
|
|
2013
|
+
});
|
|
2014
|
+
await validatePriorAttemptLineage({
|
|
2015
|
+
root,
|
|
2016
|
+
receipt: priorReceipt,
|
|
2017
|
+
receiptPath: selectedPriorPath,
|
|
2018
|
+
plan,
|
|
2019
|
+
ownerAuthority,
|
|
2020
|
+
});
|
|
2021
|
+
assertIdentifier(priorReceipt.receiptId, "receipt_id");
|
|
2022
|
+
await assertNoResumeBranch({ root, priorReceipt });
|
|
2023
|
+
const attemptNumber = priorReceipt.attemptNumber + 1;
|
|
2024
|
+
const selectedReceiptPath = receiptAttemptPath(
|
|
2025
|
+
priorReceipt.receiptBasePath,
|
|
2026
|
+
attemptNumber,
|
|
2027
|
+
);
|
|
2028
|
+
return performCoreSuccessorAttempt({
|
|
2029
|
+
root,
|
|
2030
|
+
planPath,
|
|
2031
|
+
plan,
|
|
2032
|
+
ownerAuthority,
|
|
2033
|
+
receiptId: priorReceipt.receiptId,
|
|
2034
|
+
receiptBasePath: priorReceipt.receiptBasePath,
|
|
2035
|
+
selectedReceiptPath,
|
|
2036
|
+
attemptNumber,
|
|
2037
|
+
priorAttemptReceiptPath: selectedPriorPath,
|
|
2038
|
+
priorAttemptReceiptSha256: priorReceipt.receiptSha256,
|
|
2039
|
+
priorReceipt,
|
|
2040
|
+
adoptOrphanSuccessReceipts: false,
|
|
2041
|
+
dependencies: defaultDependencies(dependencyOverrides),
|
|
2042
|
+
});
|
|
2043
|
+
}
|