owlrunkit 0.15.0 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +274 -66
  3. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +223 -117
  4. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  5. package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +2 -2
  6. package/package.json +2 -2
  7. package/packages/attest/distribution-manifest-v1.json +10 -0
  8. package/packages/attest/src/quick.mjs +10 -0
  9. package/registry-release-policy-v1.json +8 -2
  10. package/sbom.spdx.json +5 -5
  11. package/scripts/runkit-contract/assurance-router.mjs +119 -0
  12. package/scripts/runkit-contract/core-contract.mjs +250 -115
  13. package/scripts/runkit-contract/core-successor.mjs +2043 -0
  14. package/scripts/runkit-contract/deployment-workflow.mjs +2040 -0
  15. package/scripts/runkit-contract/finalize.mjs +242 -21
  16. package/scripts/runkit-contract/fleet-discovery.mjs +456 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +850 -0
  18. package/scripts/runkit-contract/human-status.mjs +553 -0
  19. package/scripts/runkit-contract/inspect-presentation.mjs +25 -8
  20. package/scripts/runkit-contract/install-codex-skill.mjs +73 -30
  21. package/scripts/runkit-contract/lease-lifecycle.mjs +8 -2
  22. package/scripts/runkit-contract/lifecycle-orchestration.mjs +135 -12
  23. package/scripts/runkit-contract/onboarding-doctor.mjs +43 -90
  24. package/scripts/runkit-contract/owner-authority-trust.mjs +230 -0
  25. package/scripts/runkit-contract/owner-deployment-decision.mjs +385 -0
  26. package/scripts/runkit-contract/profile-onboarding.mjs +841 -13
  27. package/scripts/runkit-contract/project-cli-resolver.mjs +701 -0
  28. package/scripts/runkit-contract/project-control-parser.mjs +830 -0
  29. package/scripts/runkit-contract/project-control-state.mjs +28 -0
  30. package/scripts/runkit-contract/provenance-common.mjs +19 -2
  31. package/scripts/runkit-contract/ready-for-commit.mjs +330 -18
  32. package/scripts/runkit-contract/remote-deployment.mjs +1596 -0
  33. package/scripts/runkit-contract/remote-process-adapter.mjs +177 -0
  34. package/scripts/runkit-contract/runkit-bootstrap.mjs +131 -0
  35. package/scripts/runkit-contract/runkit-cli.mjs +1097 -21
  36. package/scripts/runkit-contract/schemas/assurance-route-v1.schema.json +26 -0
  37. package/scripts/runkit-contract/schemas/core-successor-batch-receipt-v1.schema.json +126 -0
  38. package/scripts/runkit-contract/schemas/core-successor-plan-v1.schema.json +155 -0
  39. package/scripts/runkit-contract/schemas/core-successor-project-success-receipt-v1.schema.json +72 -0
  40. package/scripts/runkit-contract/schemas/deployment-execute-request-v1.schema.json +63 -0
  41. package/scripts/runkit-contract/schemas/deployment-lineage-v1.schema.json +292 -0
  42. package/scripts/runkit-contract/schemas/deployment-prepare-receipt-v1.schema.json +140 -0
  43. package/scripts/runkit-contract/schemas/fleet-discovery-v1.schema.json +31 -0
  44. package/scripts/runkit-contract/schemas/fleet-manifest-v1.schema.json +20 -0
  45. package/scripts/runkit-contract/schemas/fleet-root-registry-v1.schema.json +34 -0
  46. package/scripts/runkit-contract/schemas/human-status-v1.schema.json +102 -0
  47. package/scripts/runkit-contract/schemas/owner-authority-trust-v1.schema.json +66 -0
  48. package/scripts/runkit-contract/schemas/owner-deployment-authority-v1.schema.json +46 -0
  49. package/scripts/runkit-contract/schemas/owner-deployment-authority-v2.schema.json +94 -0
  50. package/scripts/runkit-contract/schemas/owner-deployment-decision-v1.schema.json +102 -0
  51. package/scripts/runkit-contract/schemas/owner-migration-authority-v1.schema.json +33 -0
  52. package/scripts/runkit-contract/schemas/owner-migration-authority-v2.schema.json +67 -0
  53. package/scripts/runkit-contract/schemas/profile-apply-receipt-v1.schema.json +32 -0
  54. package/scripts/runkit-contract/schemas/profile-detection-v2.schema.json +145 -0
  55. package/scripts/runkit-contract/schemas/ready-for-commit-v2.schema.json +37 -0
  56. package/scripts/runkit-contract/schemas/remote-deployment-manifest-v1.schema.json +379 -0
  57. package/scripts/runkit-contract/schemas/remote-deployment-result-v1.schema.json +153 -0
  58. package/scripts/runkit-contract/schemas/remote-deployment-stage-journal-v1.schema.json +183 -0
  59. package/scripts/runkit-contract/schemas/remote-target-v1.schema.json +32 -0
  60. package/scripts/runkit-contract/schemas/source-candidate-v2.schema.json +152 -0
  61. package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +13 -0
  62. package/scripts/runkit-contract/snapshot.mjs +1 -1
  63. package/scripts/runkit-contract/source-candidate.mjs +1382 -0
  64. package/scripts/runkit-contract/ssh-remote-adapter.mjs +1184 -0
  65. package/scripts/runkit-contract/sync-core016-release-identity.mjs +161 -0
  66. package/scripts/runkit-contract/templates/assurance-request.json +13 -0
  67. package/scripts/runkit-contract/templates/core-successor-owner-authority.json +15 -0
  68. package/scripts/runkit-contract/templates/deployment-execute-request.json +39 -0
  69. package/scripts/runkit-contract/templates/deployment-owner-authority.json +23 -0
  70. package/scripts/runkit-contract/templates/deployment-owner-decision.json +26 -0
  71. package/scripts/runkit-contract/templates/fleet-manifest.json +8 -0
  72. package/scripts/runkit-contract/templates/fleet-root-registry.json +8 -0
  73. package/scripts/runkit-contract/templates/owner-authority-trust.json +16 -0
  74. package/scripts/runkit-contract/templates/ready-for-commit-request.json +2 -2
  75. package/scripts/runkit-contract/templates/remote-deployment-manifest.json +108 -0
  76. package/scripts/runkit-contract/verification-receipt-gate.mjs +28 -0
@@ -4,33 +4,45 @@ import {
4
4
  lstatSync,
5
5
  mkdirSync,
6
6
  readFileSync,
7
- readdirSync,
8
7
  realpathSync,
9
8
  renameSync,
10
9
  rmSync,
11
10
  writeFileSync,
12
11
  } from "node:fs";
13
- import { isAbsolute, relative, resolve, win32 } from "node:path";
12
+ import { isAbsolute, relative, resolve, sep, win32 } from "node:path";
14
13
  import { fileURLToPath } from "node:url";
14
+ import { parseProjectControlState } from "./project-control-parser.mjs";
15
15
 
16
16
  export const CONTRACT_VERSION = "0.2";
17
- export const CORE_VERSION = "0.15.0";
17
+ export const CORE_VERSION = "0.16.0";
18
18
  export const RUNTIME_ROOT = ".owlcoda/runkit";
19
19
 
20
20
  const CORE_FILES = [
21
21
  "acceptance-evidence.mjs",
22
+ "assurance-router.mjs",
23
+ "core-successor.mjs",
22
24
  "coverage-adoption.mjs",
23
25
  "core-contract.mjs",
24
26
  "delivery-create.mjs",
25
27
  "delivery-selection.mjs",
28
+ "deployment-workflow.mjs",
29
+ "fleet-discovery.mjs",
26
30
  "finalize.mjs",
31
+ "formal-workflow.mjs",
32
+ "human-status.mjs",
27
33
  "inspect-presentation.mjs",
34
+ "install-codex-skill.mjs",
28
35
  "lease-lifecycle.mjs",
29
36
  "lifecycle-orchestration.mjs",
30
37
  "offline-store.mjs",
31
38
  "onboarding-doctor.mjs",
39
+ "owner-authority-trust.mjs",
40
+ "owner-deployment-decision.mjs",
32
41
  "profile-impact.mjs",
33
42
  "profile-onboarding.mjs",
43
+ "project-cli-resolver.mjs",
44
+ "project-control-parser.mjs",
45
+ "project-control-state.mjs",
34
46
  "provenance-common.mjs",
35
47
  "quick-attest.mjs",
36
48
  "quick-canonical.mjs",
@@ -45,24 +57,53 @@ const CORE_FILES = [
45
57
  "repair-execution.mjs",
46
58
  "resume-execution.mjs",
47
59
  "resource-preflight.mjs",
60
+ "remote-deployment.mjs",
61
+ "remote-process-adapter.mjs",
62
+ "ssh-remote-adapter.mjs",
63
+ "runkit-bootstrap.mjs",
48
64
  "runkit-cli.mjs",
49
65
  "snapshot.mjs",
66
+ "source-candidate.mjs",
50
67
  "source-fingerprint.mjs",
68
+ "sync-core016-release-identity.mjs",
51
69
  "verification-receipt-gate.mjs",
52
70
  "verification-context.mjs",
53
71
  "verification-plan.mjs",
54
72
  "visual-smoke.mjs",
73
+ "schemas/assurance-route-v1.schema.json",
55
74
  "schemas/closeout-receipt-v2.schema.json",
56
75
  "schemas/adoption-evidence-v1.schema.json",
57
76
  "schemas/coverage-adopt-request-v1.schema.json",
58
77
  "schemas/core-artifact.schema.json",
59
78
  "schemas/core-artifact-v2.schema.json",
79
+ "schemas/core-successor-batch-receipt-v1.schema.json",
80
+ "schemas/core-successor-plan-v1.schema.json",
81
+ "schemas/core-successor-project-success-receipt-v1.schema.json",
82
+ "schemas/deployment-execute-request-v1.schema.json",
83
+ "schemas/deployment-lineage-v1.schema.json",
84
+ "schemas/deployment-prepare-receipt-v1.schema.json",
60
85
  "schemas/engine-pin.schema.json",
61
86
  "schemas/engine-pin-v2.schema.json",
62
87
  "schemas/evidence-coverage-index-v1.schema.json",
88
+ "schemas/fleet-discovery-v1.schema.json",
89
+ "schemas/fleet-manifest-v1.schema.json",
90
+ "schemas/fleet-root-registry-v1.schema.json",
91
+ "schemas/human-status-v1.schema.json",
92
+ "schemas/owner-authority-trust-v1.schema.json",
93
+ "schemas/owner-migration-authority-v1.schema.json",
94
+ "schemas/owner-migration-authority-v2.schema.json",
95
+ "schemas/owner-deployment-authority-v1.schema.json",
96
+ "schemas/owner-deployment-authority-v2.schema.json",
97
+ "schemas/owner-deployment-decision-v1.schema.json",
98
+ "schemas/profile-apply-receipt-v1.schema.json",
99
+ "schemas/profile-detection-v2.schema.json",
63
100
  "schemas/project-config.schema.json",
64
101
  "schemas/project-config-v2.schema.json",
65
102
  "schemas/profiles.schema.json",
103
+ "schemas/remote-deployment-manifest-v1.schema.json",
104
+ "schemas/remote-deployment-result-v1.schema.json",
105
+ "schemas/remote-deployment-stage-journal-v1.schema.json",
106
+ "schemas/remote-target-v1.schema.json",
66
107
  "schemas/resume-attempt-v1.schema.json",
67
108
  "schemas/resume-request-v1.schema.json",
68
109
  "schemas/resource-preflight-request-v1.schema.json",
@@ -70,7 +111,9 @@ const CORE_FILES = [
70
111
  "schemas/replayable-evidence-v1.schema.json",
71
112
  "schemas/finalize-request-v1.schema.json",
72
113
  "schemas/ready-for-commit-v1.schema.json",
114
+ "schemas/ready-for-commit-v2.schema.json",
73
115
  "schemas/snapshot-v1.schema.json",
116
+ "schemas/source-candidate-v2.schema.json",
74
117
  "schemas/visual-smoke-request-v1.schema.json",
75
118
  "schemas/visual-smoke-result-v1.schema.json",
76
119
  "schemas/verification-context-v1.schema.json",
@@ -80,11 +123,20 @@ const CORE_FILES = [
80
123
  "schemas/verification-receipt-v2.schema.json",
81
124
  "schemas/verify-plan-request-v1.schema.json",
82
125
  "templates/goal-contract.json",
126
+ "templates/assurance-request.json",
127
+ "templates/core-successor-owner-authority.json",
128
+ "templates/owner-authority-trust.json",
83
129
  "templates/coverage-adopt-request.json",
130
+ "templates/deployment-execute-request.json",
131
+ "templates/deployment-owner-authority.json",
132
+ "templates/deployment-owner-decision.json",
133
+ "templates/fleet-manifest.json",
134
+ "templates/fleet-root-registry.json",
84
135
  "templates/finalize-request.json",
85
136
  "templates/ready-for-commit-request.json",
86
137
  "templates/resume-request.json",
87
138
  "templates/resource-preflight-request.json",
139
+ "templates/remote-deployment-manifest.json",
88
140
  "templates/snapshot-request.json",
89
141
  "templates/visual-smoke-request.json",
90
142
  "templates/verify-plan-request.json",
@@ -125,6 +177,80 @@ function atomicWriteJson(filePath, value) {
125
177
  renameSync(temporaryPath, filePath);
126
178
  }
127
179
 
180
+ function pathEntryExists(filePath) {
181
+ try {
182
+ lstatSync(filePath);
183
+ return true;
184
+ } catch (error) {
185
+ if (error?.code === "ENOENT") return false;
186
+ throw error;
187
+ }
188
+ }
189
+
190
+ function canonicalWorkspaceRoot(workspaceRoot) {
191
+ const requested = resolve(workspaceRoot);
192
+ const stat = lstatSync(requested);
193
+ if (
194
+ stat.isSymbolicLink()
195
+ || !stat.isDirectory()
196
+ ) {
197
+ throw new Error("workspaceRoot must be a real directory.");
198
+ }
199
+ return realpathSync(requested);
200
+ }
201
+
202
+ function ensureRealDirectory(root, target, label) {
203
+ const remainder = relative(root, target);
204
+ if (
205
+ remainder === ""
206
+ || remainder === ".."
207
+ || remainder.startsWith(`..${sep}`)
208
+ || isAbsolute(remainder)
209
+ ) {
210
+ throw new Error(`${label} escapes the workspace.`);
211
+ }
212
+ let current = root;
213
+ for (const segment of remainder.split(/[\\/]/u)) {
214
+ current = resolve(current, segment);
215
+ if (!pathEntryExists(current)) {
216
+ try {
217
+ mkdirSync(current);
218
+ } catch (error) {
219
+ if (error?.code !== "EEXIST") throw error;
220
+ }
221
+ }
222
+ const stat = lstatSync(current);
223
+ if (
224
+ stat.isSymbolicLink()
225
+ || !stat.isDirectory()
226
+ || realpathSync(current) !== current
227
+ ) {
228
+ throw new Error(`${label} must be a real directory without symlink ancestors.`);
229
+ }
230
+ }
231
+ return target;
232
+ }
233
+
234
+ function assertRegularRuntimeFile(root, filePath, label) {
235
+ const remainder = relative(root, filePath);
236
+ if (
237
+ remainder === ""
238
+ || remainder === ".."
239
+ || remainder.startsWith(`..${sep}`)
240
+ || isAbsolute(remainder)
241
+ ) {
242
+ throw new Error(`${label} escapes the workspace.`);
243
+ }
244
+ const stat = lstatSync(filePath);
245
+ if (
246
+ stat.isSymbolicLink()
247
+ || !stat.isFile()
248
+ || realpathSync(filePath) !== filePath
249
+ ) {
250
+ throw new Error(`${label} must be a regular file without symlink ancestors.`);
251
+ }
252
+ }
253
+
128
254
  function projectConfig(core = currentCoreIdentity()) {
129
255
  return {
130
256
  schemaVersion: "OwlCodaRunKitConfigV2",
@@ -169,7 +295,11 @@ function migrateProjectConfig({ runtimeRoot, configPath, config, migration }) {
169
295
  authorizationGranted: false,
170
296
  };
171
297
  const receiptsRoot = resolve(runtimeRoot, "config-migration-receipts");
172
- mkdirSync(receiptsRoot, { recursive: true });
298
+ ensureRealDirectory(
299
+ runtimeRoot,
300
+ receiptsRoot,
301
+ "Config migration receipt directory",
302
+ );
173
303
  const receiptName = writeMigrationReceiptAppendOnly(
174
304
  receiptsRoot,
175
305
  migrationReceiptName(migration, config.core, next.core),
@@ -183,17 +313,17 @@ function coreDirectory() {
183
313
  return resolve(fileURLToPath(new URL(".", import.meta.url)));
184
314
  }
185
315
 
186
- function coreManifestStream() {
187
- const coreRoot = coreDirectory();
188
- const projectRoot = resolve(coreRoot, "../..");
316
+ function coreManifestStreamFromRoots(coreRoot, projectRoot) {
189
317
  return [
190
318
  ...CORE_FILES.map((name) => `${name}\tsha256:${sha256(readFileSync(resolve(coreRoot, name)))}\n`),
191
319
  ...CORE_DEPENDENCY_FILES.map((name) => `${name}\tsha256:${sha256(readFileSync(resolve(projectRoot, name)))}\n`),
192
320
  ].join("");
193
321
  }
194
322
 
195
- export function currentCoreIdentity() {
196
- const manifestSha256 = sha256(coreManifestStream());
323
+ export function coreIdentityFromSourceRoot(sourceRoot) {
324
+ const projectRoot = realpathSync(sourceRoot);
325
+ const coreRoot = resolve(projectRoot, "scripts/runkit-contract");
326
+ const manifestSha256 = sha256(coreManifestStreamFromRoots(coreRoot, projectRoot));
197
327
  return {
198
328
  contractVersion: CONTRACT_VERSION,
199
329
  coreVersion: CORE_VERSION,
@@ -202,6 +332,10 @@ export function currentCoreIdentity() {
202
332
  };
203
333
  }
204
334
 
335
+ export function currentCoreIdentity() {
336
+ return coreIdentityFromSourceRoot(resolve(coreDirectory(), "../.."));
337
+ }
338
+
205
339
  export function isDirectExecution(moduleUrl, argvPath = process.argv[1]) {
206
340
  if (!argvPath) return false;
207
341
  try {
@@ -234,9 +368,15 @@ export function validateLeaseOwnedPaths(paths) {
234
368
 
235
369
  export function validateExecutionPin({ expected, actual } = {}) {
236
370
  const fields = ["contractVersion", "coreVersion", "coreManifestSha256", "coreSourceRef"];
237
- const malformed = fields.filter((field) => typeof expected?.[field] !== "string" || typeof actual?.[field] !== "string");
238
- if (malformed.length > 0) {
239
- return { status: "invalid_input", exitCode: 3, issues: malformed.map((field) => `missing engine pin field: ${field}`) };
371
+ try {
372
+ validateCoreIdentity(expected, { requireSourceBinding: false });
373
+ validateCoreIdentity(actual, { requireSourceBinding: false });
374
+ } catch (error) {
375
+ return {
376
+ status: "invalid_input",
377
+ exitCode: 3,
378
+ issues: [error instanceof Error ? `invalid engine pin: ${error.message}` : String(error)],
379
+ };
240
380
  }
241
381
  const issues = fields.filter((field) => expected[field] !== actual[field]).map((field) => `${field} changed during execution`);
242
382
  return issues.length === 0
@@ -244,11 +384,27 @@ export function validateExecutionPin({ expected, actual } = {}) {
244
384
  : { status: "engine_changed_during_execution", exitCode: 2, issues };
245
385
  }
246
386
 
247
- function validateCoreIdentity(core) {
387
+ function validateCoreIdentity(core, { requireSourceBinding = true } = {}) {
248
388
  if (!isRecord(core)) throw new Error("Core identity must be an object.");
389
+ const coreKeys = ["contractVersion", "coreManifestSha256", "coreSourceRef", "coreVersion"];
390
+ const unsupportedCoreKeys = Object.keys(core).filter(key => !coreKeys.includes(key));
391
+ if (unsupportedCoreKeys.length > 0) {
392
+ throw new Error(`Core identity contains unsupported field: ${unsupportedCoreKeys[0]}`);
393
+ }
249
394
  if (typeof core.contractVersion !== "string" || typeof core.coreVersion !== "string") throw new Error("Core identity requires contractVersion and coreVersion.");
395
+ if (!new Set(["0.1", CONTRACT_VERSION]).has(core.contractVersion)) {
396
+ throw new Error("Core identity contractVersion must be a supported RunKit contract version.");
397
+ }
398
+ if (!/^\d+\.\d+\.\d+$/u.test(core.coreVersion)) {
399
+ throw new Error("Core identity coreVersion must be a three-part semantic version.");
400
+ }
250
401
  if (!isSha256Ref(core.coreManifestSha256)) throw new Error("Core identity requires coreManifestSha256.");
251
- if (typeof core.coreSourceRef !== "string" || core.coreSourceRef.length === 0) throw new Error("Core identity requires coreSourceRef.");
402
+ if (typeof core.coreSourceRef !== "string" || core.coreSourceRef.length === 0) {
403
+ throw new Error("Core identity coreSourceRef must be a non-empty string.");
404
+ }
405
+ if (requireSourceBinding && core.coreSourceRef !== `artifact:${core.coreManifestSha256}`) {
406
+ throw new Error("Core identity coreSourceRef must bind coreManifestSha256.");
407
+ }
252
408
  }
253
409
 
254
410
  export function validateProjectConfigV2(config) {
@@ -281,102 +437,36 @@ export function validateProjectConfigV2(config) {
281
437
  }
282
438
  }
283
439
 
284
- export function inspectProjectUpgradeSafety({ workspaceRoot } = {}) {
285
- if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) {
286
- throw new Error("workspaceRoot is required.");
287
- }
288
- const root = realpathSync(workspaceRoot);
289
- const executionsRoot = resolve(root, RUNTIME_ROOT, "executions");
290
- if (!existsSync(executionsRoot)) {
291
- return {
292
- status: "safe",
293
- activeRunIds: [],
294
- activeLeaseIds: [],
295
- issues: [],
296
- };
297
- }
298
- const executionsStat = lstatSync(executionsRoot);
299
- if (executionsStat.isSymbolicLink() || !executionsStat.isDirectory()) {
300
- return {
301
- status: "unsafe",
302
- activeRunIds: [],
303
- activeLeaseIds: [],
304
- issues: ["RunKit executions root must be a regular directory."],
305
- };
306
- }
307
- const activeRunIds = [];
308
- const activeLeaseIds = [];
309
- const issues = [];
310
- const entries = readdirSync(executionsRoot, { withFileTypes: true })
311
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
312
- for (const entry of entries) {
313
- if (entry.isSymbolicLink()) {
314
- issues.push(`Execution directory must not be a symlink: ${entry.name}`);
315
- continue;
316
- }
317
- if (!entry.isDirectory()) continue;
318
- const executionRoot = resolve(executionsRoot, entry.name);
319
- const closeoutPath = resolve(executionRoot, "closeout-receipt.json");
320
- if (!existsSync(closeoutPath)) {
321
- activeRunIds.push(entry.name);
322
- } else {
323
- const closeoutStat = lstatSync(closeoutPath);
324
- if (closeoutStat.isSymbolicLink() || !closeoutStat.isFile()) {
325
- issues.push(`Closeout receipt must be a regular file: ${entry.name}`);
326
- } else {
327
- try {
328
- const closeout = JSON.parse(readFileSync(closeoutPath, "utf8"));
329
- if (closeout?.artifact?.payload?.runId !== entry.name) {
330
- issues.push(`Closeout receipt runId mismatch: ${entry.name}`);
331
- }
332
- } catch {
333
- issues.push(`Closeout receipt is invalid JSON: ${entry.name}`);
334
- }
335
- }
336
- }
337
- const leasesRoot = resolve(executionRoot, "leases");
338
- if (!existsSync(leasesRoot)) continue;
339
- const leasesStat = lstatSync(leasesRoot);
340
- if (leasesStat.isSymbolicLink() || !leasesStat.isDirectory()) {
341
- issues.push(`Lease directory must be a regular directory: ${entry.name}`);
342
- continue;
343
- }
344
- const leaseEntries = readdirSync(leasesRoot, { withFileTypes: true })
345
- .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
346
- for (const leaseEntry of leaseEntries) {
347
- if (!leaseEntry.name.endsWith(".json")) continue;
348
- if (leaseEntry.isSymbolicLink() || !leaseEntry.isFile()) {
349
- issues.push(`Lease artifact must be a regular file: ${entry.name}/${leaseEntry.name}`);
350
- continue;
351
- }
352
- try {
353
- const lease = JSON.parse(readFileSync(resolve(leasesRoot, leaseEntry.name), "utf8"));
354
- if (lease.state === "active") {
355
- activeLeaseIds.push(`${entry.name}:${lease.workItemId ?? leaseEntry.name}`);
356
- } else if (lease.state !== "released") {
357
- issues.push(`Lease state is invalid: ${entry.name}/${leaseEntry.name}`);
358
- }
359
- } catch {
360
- issues.push(`Lease artifact is invalid JSON: ${entry.name}/${leaseEntry.name}`);
361
- }
362
- }
363
- }
440
+ function projectControlParserContract() {
364
441
  return {
365
- status: activeRunIds.length === 0
366
- && activeLeaseIds.length === 0
367
- && issues.length === 0
368
- ? "safe"
369
- : "unsafe",
370
- activeRunIds,
371
- activeLeaseIds,
372
- issues,
442
+ runtimeRoot: RUNTIME_ROOT,
443
+ acceptedCloseoutVerificationIssues,
444
+ validateCoreArtifact,
445
+ validateExecutionPin,
446
+ validateLeaseOwnedPaths,
373
447
  };
374
448
  }
375
449
 
450
+ export function inspectProjectUpgradeSafety({ workspaceRoot } = {}) {
451
+ return parseProjectControlState({
452
+ workspaceRoot,
453
+ currentCore: currentCoreIdentity(),
454
+ contract: projectControlParserContract(),
455
+ }).upgradeSafety;
456
+ }
457
+
376
458
  function validateProducer(producer) {
377
459
  if (!isRecord(producer) || typeof producer.adapterKind !== "string" || typeof producer.adapterVersion !== "string") {
378
460
  throw new Error("Producer requires adapterKind and adapterVersion.");
379
461
  }
462
+ if (producer.adapterKind.length === 0 || producer.adapterVersion.length === 0) {
463
+ throw new Error("Producer adapterKind and adapterVersion must be non-empty strings.");
464
+ }
465
+ const producerKeys = ["adapterKind", "adapterVersion"];
466
+ const unsupportedProducerKeys = Object.keys(producer).filter(key => !producerKeys.includes(key));
467
+ if (unsupportedProducerKeys.length > 0) {
468
+ throw new Error(`Producer contains unsupported field: ${unsupportedProducerKeys[0]}`);
469
+ }
380
470
  }
381
471
 
382
472
  function validateExtensions(extensions) {
@@ -416,7 +506,18 @@ export function createCoreArtifact({ core = currentCoreIdentity(), producer, pay
416
506
  export function validateCoreArtifact(artifact) {
417
507
  try {
418
508
  if (!isRecord(artifact) || !new Set(["RunKitCoreArtifactV1", "RunKitCoreArtifactV2"]).has(artifact.schemaVersion)) throw new Error("Unsupported core artifact schemaVersion.");
509
+ const artifactKeys = ["core", "extensions", "payload", "producer", "schemaVersion"];
510
+ const unsupportedArtifactKeys = Object.keys(artifact).filter(key => !artifactKeys.includes(key));
511
+ if (unsupportedArtifactKeys.length > 0) {
512
+ throw new Error(`Core artifact contains unsupported field: ${unsupportedArtifactKeys[0]}`);
513
+ }
419
514
  validateCoreIdentity(artifact.core);
515
+ const expectedContractVersion = artifact.schemaVersion === "RunKitCoreArtifactV1"
516
+ ? "0.1"
517
+ : CONTRACT_VERSION;
518
+ if (artifact.core.contractVersion !== expectedContractVersion) {
519
+ throw new Error(`Core artifact ${artifact.schemaVersion} requires contractVersion ${expectedContractVersion}.`);
520
+ }
420
521
  validateProducer(artifact.producer);
421
522
  if (!isRecord(artifact.payload)) throw new Error("Core artifact payload must be an object.");
422
523
  validateExtensions(artifact.extensions);
@@ -429,12 +530,26 @@ export function validateCoreArtifact(artifact) {
429
530
  export function acceptedCloseoutVerificationIssues(artifact) {
430
531
  if (artifact?.core?.contractVersion !== "0.2" || artifact?.payload?.decision !== "accepted") return [];
431
532
  const verification = artifact.payload.verification;
533
+ const sourceArtifactValid = verification?.sourceArtifact === undefined
534
+ || (
535
+ isRecord(verification.sourceArtifact)
536
+ && new Set(["delivery_packet_v1", "source_candidate_v2"])
537
+ .has(verification.sourceArtifact.kind)
538
+ && verification.sourceArtifact.runId === artifact.payload.runId
539
+ && typeof verification.sourceArtifact.path === "string"
540
+ && verification.sourceArtifact.path.length > 0
541
+ && typeof verification.sourceArtifact.sha256 === "string"
542
+ && /^[a-f0-9]{64}$/.test(verification.sourceArtifact.sha256)
543
+ && verification.sourceArtifact.sourceFingerprint
544
+ === verification.sourceFingerprint
545
+ );
432
546
  const validFields = [
433
547
  verification?.contractVersion === "0.2",
434
548
  verification?.gateDecision === "accepted_passed",
435
549
  typeof verification?.gateInputSha256 === "string" && /^[a-f0-9]{64}$/.test(verification.gateInputSha256),
436
550
  typeof verification?.activeReceiptSha256 === "string" && /^[a-f0-9]{64}$/.test(verification.activeReceiptSha256),
437
551
  typeof verification?.sourceFingerprint === "string" && /^[a-f0-9]{64}$/.test(verification.sourceFingerprint),
552
+ sourceArtifactValid,
438
553
  typeof verification?.verificationContextFingerprint === "string" && /^[a-f0-9]{64}$/.test(verification.verificationContextFingerprint),
439
554
  verification?.leaseState === "released",
440
555
  Array.isArray(verification?.selectedProfileIds)
@@ -450,15 +565,27 @@ export function acceptedCloseoutVerificationIssues(artifact) {
450
565
 
451
566
  export async function initializeProjectRunKit({ workspaceRoot } = {}) {
452
567
  if (typeof workspaceRoot !== "string" || workspaceRoot.length === 0) throw new Error("workspaceRoot is required.");
453
- const root = realpathSync(workspaceRoot);
454
- if (existsSync(resolve(root, ".owlrunkit"))) throw new Error("Legacy .owlrunkit exists; explicit migration is required before initialization.");
568
+ const root = canonicalWorkspaceRoot(workspaceRoot);
569
+ if (pathEntryExists(resolve(root, ".owlrunkit"))) throw new Error("Legacy .owlrunkit exists; explicit migration is required before initialization.");
455
570
  const runtimeRoot = resolve(root, RUNTIME_ROOT);
456
- const remainder = relative(root, runtimeRoot);
457
- if (remainder.startsWith("..") || isAbsolute(remainder)) throw new Error("Runtime root escapes the workspace.");
571
+ ensureRealDirectory(root, resolve(root, ".owlcoda"), "OwlCoda control directory");
572
+ ensureRealDirectory(root, runtimeRoot, "RunKit runtime directory");
573
+ ensureRealDirectory(
574
+ runtimeRoot,
575
+ resolve(runtimeRoot, "executions"),
576
+ "RunKit executions directory",
577
+ );
458
578
  const configPath = resolve(runtimeRoot, "config.json");
459
579
  const profilesPath = resolve(runtimeRoot, "profiles.json");
460
- if (!existsSync(configPath)) {
461
- mkdirSync(resolve(runtimeRoot, "executions"), { recursive: true });
580
+ const configExists = pathEntryExists(configPath);
581
+ const profilesExist = pathEntryExists(profilesPath);
582
+ if (configExists) {
583
+ assertRegularRuntimeFile(runtimeRoot, configPath, "Project config");
584
+ }
585
+ if (profilesExist) {
586
+ assertRegularRuntimeFile(runtimeRoot, profilesPath, "Project profiles");
587
+ }
588
+ if (!configExists) {
462
589
  atomicWriteJson(configPath, projectConfig());
463
590
  } else {
464
591
  const config = JSON.parse(readFileSync(configPath, "utf8"));
@@ -486,8 +613,11 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
486
613
  config,
487
614
  migration: "config-v01-to-v02",
488
615
  });
489
- if (!existsSync(profilesPath)) {
490
- writeFileSync(profilesPath, `${JSON.stringify({ schemaVersion: "OwlCodaRunKitProfilesV1", profiles: [] }, null, 2)}\n`);
616
+ if (!profilesExist) {
617
+ atomicWriteJson(profilesPath, {
618
+ schemaVersion: "OwlCodaRunKitProfilesV1",
619
+ profiles: [],
620
+ });
491
621
  }
492
622
  return { status: "upgraded", exitCode: 0, runtimeRoot: RUNTIME_ROOT, core: current, migrationReceipt };
493
623
  } else if (config.schemaVersion === "OwlCodaRunKitConfigV2" && config.core?.contractVersion === CONTRACT_VERSION) {
@@ -507,17 +637,22 @@ export async function initializeProjectRunKit({ workspaceRoot } = {}) {
507
637
  config,
508
638
  migration: "config-v02-core-refresh",
509
639
  });
510
- if (!existsSync(profilesPath)) {
511
- writeFileSync(profilesPath, `${JSON.stringify({ schemaVersion: "OwlCodaRunKitProfilesV1", profiles: [] }, null, 2)}\n`);
640
+ if (!profilesExist) {
641
+ atomicWriteJson(profilesPath, {
642
+ schemaVersion: "OwlCodaRunKitProfilesV1",
643
+ profiles: [],
644
+ });
512
645
  }
513
646
  return { status: "upgraded", exitCode: 0, runtimeRoot: RUNTIME_ROOT, core: current, migrationReceipt };
514
647
  } else {
515
648
  throw new Error(`Unsupported project config migration: ${config.schemaVersion ?? "missing schemaVersion"}.`);
516
649
  }
517
650
  }
518
- mkdirSync(resolve(runtimeRoot, "executions"), { recursive: true });
519
- if (!existsSync(profilesPath)) {
520
- writeFileSync(profilesPath, `${JSON.stringify({ schemaVersion: "OwlCodaRunKitProfilesV1", profiles: [] }, null, 2)}\n`);
651
+ if (!profilesExist) {
652
+ atomicWriteJson(profilesPath, {
653
+ schemaVersion: "OwlCodaRunKitProfilesV1",
654
+ profiles: [],
655
+ });
521
656
  }
522
657
  return { status: "initialized", exitCode: 0, runtimeRoot: RUNTIME_ROOT, core: currentCoreIdentity() };
523
658
  }