owlrunkit 0.19.1 → 0.20.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 (42) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +107 -3
  3. package/docs/architecture/OWLCODA_RUNKIT_0191_PROFILE_RECONCILE_PATCH_PRODUCT_DECISION_20260813.md +11 -5
  4. package/docs/architecture/OWLCODA_RUNKIT_0192_FORMAL_USABILITY_PRODUCT_DECISION_20260813.md +141 -0
  5. package/docs/architecture/OWLCODA_RUNKIT_020_TEAM_DELIVERY_PHASE1_PRODUCT_DECISION_20260813.md +296 -0
  6. package/integrations/codex/skills/owlcoda-runkit/SKILL.md +74 -9
  7. package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +3 -3
  8. package/package.json +1 -1
  9. package/packages/attest/distribution-manifest-v1.json +10 -0
  10. package/packages/attest/src/quick.mjs +42 -4
  11. package/registry-release-policy-v1.json +5 -5
  12. package/sbom.spdx.json +5 -5
  13. package/scripts/runkit-contract/core-contract.mjs +42 -2
  14. package/scripts/runkit-contract/efficiency-metrics.mjs +9 -7
  15. package/scripts/runkit-contract/formal-preflight.mjs +352 -0
  16. package/scripts/runkit-contract/formal-supplemental-evidence.mjs +713 -0
  17. package/scripts/runkit-contract/formal-workflow.mjs +529 -59
  18. package/scripts/runkit-contract/onboarding-doctor.mjs +32 -13
  19. package/scripts/runkit-contract/project-cli-resolver.mjs +33 -3
  20. package/scripts/runkit-contract/project-external-delivery.mjs +16 -9
  21. package/scripts/runkit-contract/quick-attest.mjs +24 -0
  22. package/scripts/runkit-contract/runkit-cli.mjs +445 -52
  23. package/scripts/runkit-contract/schemas/candidate-transfer-packet-v1.schema.json +98 -0
  24. package/scripts/runkit-contract/schemas/formal-check-v2.schema.json +231 -0
  25. package/scripts/runkit-contract/schemas/formal-envelope-preflight-v1.schema.json +212 -0
  26. package/scripts/runkit-contract/schemas/formal-evidence-summary-v1.schema.json +114 -0
  27. package/scripts/runkit-contract/schemas/formal-supplemental-evidence-v1.schema.json +103 -0
  28. package/scripts/runkit-contract/schemas/team-acceptance-packet-v2.schema.json +109 -0
  29. package/scripts/runkit-contract/schemas/team-delivery-status-v1.schema.json +72 -0
  30. package/scripts/runkit-contract/schemas/team-packet-validation-v1.schema.json +33 -0
  31. package/scripts/runkit-contract/schemas/team-project-event-v5.schema.json +89 -0
  32. package/scripts/runkit-contract/schemas/team-project-status-v3.schema.json +173 -0
  33. package/scripts/runkit-contract/schemas/team-recommendation-v1.schema.json +121 -0
  34. package/scripts/runkit-contract/schemas/team-task-packet-v2.schema.json +228 -0
  35. package/scripts/runkit-contract/snapshot.mjs +125 -5
  36. package/scripts/runkit-contract/source-candidate.mjs +271 -69
  37. package/scripts/runkit-contract/sync-release-identity.mjs +39 -4
  38. package/scripts/runkit-contract/team-delivery.mjs +906 -0
  39. package/scripts/runkit-contract/team-project-successor.mjs +7 -4
  40. package/scripts/runkit-contract/team-project.mjs +440 -28
  41. package/scripts/runkit-contract/usage-mode.mjs +9 -3
  42. package/scripts/runkit-contract/verification-envelope.mjs +425 -37
@@ -8,6 +8,7 @@ import {
8
8
  } from "node:fs";
9
9
  import path from "node:path";
10
10
 
11
+ import { validateReplayableEvidence } from "./acceptance-evidence.mjs";
11
12
  import { canonicalSourceFingerprint } from "./source-fingerprint.mjs";
12
13
  import {
13
14
  assertAllowedKeys,
@@ -185,21 +186,140 @@ function validateRequest(request) {
185
186
  if (!context.valid) throw new Error(`Snapshot verification context is invalid: ${context.issues.join("; ")}`);
186
187
  }
187
188
 
188
- export function verifySnapshotEvidence({ workspaceRoot, runId, snapshot }) {
189
+ function exactKeys(value, label, keys, { optional = [] } = {}) {
190
+ assertAllowedKeys(value, label, [...keys, ...optional]);
191
+ if (!isRecord(value) || keys.some(key => !Object.hasOwn(value, key))) {
192
+ throw new Error(`${label} is missing required fields.`);
193
+ }
194
+ }
195
+
196
+ function validRepositoryState(state, label) {
197
+ try {
198
+ exactKeys(state, label, [
199
+ "head",
200
+ "statusMode",
201
+ "statusSha256",
202
+ "statusByteCount",
203
+ "statusEntryCount",
204
+ "selectedFiles",
205
+ "manifestFingerprint",
206
+ ]);
207
+ const files = normalizedFileMap(state.selectedFiles);
208
+ return typeof state.head === "string"
209
+ && state.head.length > 0
210
+ && state.statusMode === STATUS_MODE
211
+ && /^[a-f0-9]{64}$/u.test(state.statusSha256)
212
+ && Number.isSafeInteger(state.statusByteCount)
213
+ && state.statusByteCount >= 0
214
+ && Number.isSafeInteger(state.statusEntryCount)
215
+ && state.statusEntryCount >= 0
216
+ && files !== null
217
+ && canonicalSourceFingerprint(files) === state.manifestFingerprint;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ function snapshotContractIssues(snapshot) {
189
224
  const issues = [];
190
- if (!isRecord(snapshot) || snapshot.schemaVersion !== "OwlCodaRunKitSnapshotV1") {
191
- return { valid: false, issues: ["Unsupported snapshot schemaVersion."] };
225
+ try {
226
+ exactKeys(snapshot, "Snapshot", [
227
+ "schemaVersion",
228
+ "runId",
229
+ "snapshotId",
230
+ "status",
231
+ "mode",
232
+ "targetRoot",
233
+ "verificationContextFingerprint",
234
+ "repositoryBefore",
235
+ "repositoryAfter",
236
+ "command",
237
+ "authorizationGranted",
238
+ ], { optional: ["visual"] });
239
+ exactKeys(snapshot.command, "Snapshot command", [
240
+ "startedAt",
241
+ "endedAt",
242
+ "durationMs",
243
+ "exitCode",
244
+ "stdoutPath",
245
+ "stdoutSha256",
246
+ "stdoutLineCount",
247
+ "stderrPath",
248
+ "stderrSha256",
249
+ "stderrLineCount",
250
+ "evidence",
251
+ ]);
252
+ } catch (error) {
253
+ return [error instanceof Error ? error.message : String(error)];
254
+ }
255
+ if (
256
+ snapshot.schemaVersion !== "OwlCodaRunKitSnapshotV1"
257
+ || typeof snapshot.runId !== "string"
258
+ || snapshot.runId.length === 0
259
+ || typeof snapshot.snapshotId !== "string"
260
+ || snapshot.snapshotId.length === 0
261
+ || !new Set(["project", "foreign_readonly"]).has(snapshot.mode)
262
+ || typeof snapshot.targetRoot !== "string"
263
+ || snapshot.targetRoot.length === 0
264
+ || !/^[a-f0-9]{64}$/u.test(snapshot.verificationContextFingerprint)
265
+ || snapshot.authorizationGranted !== false
266
+ || !validRepositoryState(snapshot.repositoryBefore, "repositoryBefore")
267
+ || !validRepositoryState(snapshot.repositoryAfter, "repositoryAfter")
268
+ || typeof snapshot.command.startedAt !== "string"
269
+ || typeof snapshot.command.endedAt !== "string"
270
+ || !Number.isSafeInteger(snapshot.command.durationMs)
271
+ || snapshot.command.durationMs < 0
272
+ || !Number.isInteger(snapshot.command.exitCode)
273
+ || typeof snapshot.command.stdoutPath !== "string"
274
+ || typeof snapshot.command.stderrPath !== "string"
275
+ || !/^[a-f0-9]{64}$/u.test(snapshot.command.stdoutSha256)
276
+ || !/^[a-f0-9]{64}$/u.test(snapshot.command.stderrSha256)
277
+ || !Number.isSafeInteger(snapshot.command.stdoutLineCount)
278
+ || snapshot.command.stdoutLineCount < 0
279
+ || !Number.isSafeInteger(snapshot.command.stderrLineCount)
280
+ || snapshot.command.stderrLineCount < 0
281
+ ) {
282
+ issues.push("Snapshot contract is invalid.");
192
283
  }
284
+ const evidence = validateReplayableEvidence(snapshot.command.evidence);
285
+ if (!evidence.valid) issues.push(...evidence.issues);
286
+ const stable = stableTarget(snapshot.repositoryBefore, snapshot.repositoryAfter);
287
+ const expectedStatus = !stable
288
+ ? "invalidated_by_target_write"
289
+ : snapshot.command.exitCode === 0
290
+ ? "snapshot_passed"
291
+ : "snapshot_failed";
292
+ if (snapshot.status !== expectedStatus) {
293
+ issues.push("Snapshot status does not match its command and repository state.");
294
+ }
295
+ return issues;
296
+ }
297
+
298
+ export function verifySnapshotEvidence({
299
+ workspaceRoot,
300
+ runId,
301
+ snapshot,
302
+ requirePassed = true,
303
+ }) {
304
+ const issues = [];
305
+ issues.push(...snapshotContractIssues(snapshot));
306
+ if (issues.length > 0) return { valid: false, issues };
193
307
  if (snapshot.runId !== runId) issues.push("Snapshot runId does not match the execution.");
194
- if (snapshot.status !== "snapshot_passed") issues.push("Snapshot did not pass.");
308
+ if (requirePassed && snapshot.status !== "snapshot_passed") {
309
+ issues.push("Snapshot did not pass.");
310
+ }
195
311
  for (const [label, relativePath, expected] of [
196
312
  ["stdout", snapshot.command?.stdoutPath, snapshot.command?.stdoutSha256],
197
313
  ["stderr", snapshot.command?.stderrPath, snapshot.command?.stderrSha256],
198
314
  ]) {
199
315
  try {
200
316
  const absolutePath = resolveWithinRoot(workspaceRoot, relativePath, `${label} evidence path`);
201
- const actual = sha256(readFileSync(absolutePath));
317
+ const bytes = readFileSync(absolutePath);
318
+ const actual = sha256(bytes);
202
319
  if (actual !== expected) issues.push(`${label} evidence hash mismatch.`);
320
+ if (lineCount(bytes) !== snapshot.command[`${label}LineCount`]) {
321
+ issues.push(`${label} evidence line count mismatch.`);
322
+ }
203
323
  } catch (error) {
204
324
  issues.push(error instanceof Error ? error.message : String(error));
205
325
  }
@@ -28,10 +28,15 @@ import {
28
28
  relativeToWorkspace,
29
29
  resolveExistingArtifact,
30
30
  safeIdentifier,
31
+ safeRelativePath,
31
32
  sha256,
32
33
  writeJsonAtomically,
33
34
  writeJsonExclusiveAtomically,
34
35
  } from "./provenance-common.mjs";
36
+
37
+ export const SOURCE_CANDIDATE_V2_MAX_BYTES = 64 * 1024 * 1024;
38
+ export const SOURCE_CANDIDATE_V2_MAX_NODES = 2_000_000;
39
+ import { readWorkspaceJsonBounded } from "./onboarding-doctor.mjs";
35
40
  import { verifyDeliveryPacket } from "./source-fingerprint.mjs";
36
41
 
37
42
  function canonicalJson(value) {
@@ -51,6 +56,18 @@ function candidateHash(value) {
51
56
  return `sha256:${sha256(canonicalJson(value))}`;
52
57
  }
53
58
 
59
+ function hasExactKeys(value, keys) {
60
+ return Boolean(value)
61
+ && typeof value === "object"
62
+ && !Array.isArray(value)
63
+ && canonicalJson(Object.keys(value).sort()) === canonicalJson([...keys].sort());
64
+ }
65
+
66
+ function validIdentifier(value) {
67
+ return typeof value === "string"
68
+ && /^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(value);
69
+ }
70
+
54
71
  function repositoryActionsFalse() {
55
72
  return {
56
73
  worktreeCreated: false,
@@ -281,15 +298,56 @@ function sourceManifestV2({ workspaceRoot, ownedPaths }) {
281
298
  };
282
299
  }
283
300
 
284
- function freezePayloadV1({
301
+ function sourceCandidateNodeCount(value) {
302
+ let count = 0;
303
+ const pending = [value];
304
+ while (pending.length > 0) {
305
+ const current = pending.pop();
306
+ count += 1;
307
+ if (Array.isArray(current)) {
308
+ pending.push(...current);
309
+ } else if (current !== null && typeof current === "object") {
310
+ pending.push(...Object.values(current));
311
+ }
312
+ }
313
+ return count;
314
+ }
315
+
316
+ export function validateSourceCandidateDocumentBoundsV2(value, {
317
+ maxBytes = SOURCE_CANDIDATE_V2_MAX_BYTES,
318
+ maxNodes = SOURCE_CANDIDATE_V2_MAX_NODES,
319
+ } = {}) {
320
+ const bytes = Buffer.byteLength(`${JSON.stringify(value, null, 2)}\n`, "utf8");
321
+ const nodes = sourceCandidateNodeCount(value);
322
+ return {
323
+ valid: bytes <= maxBytes && nodes <= maxNodes,
324
+ bytes,
325
+ nodes,
326
+ maxBytes,
327
+ maxNodes,
328
+ };
329
+ }
330
+
331
+ function assertSourceCandidateDocumentBoundsV2(value) {
332
+ const result = validateSourceCandidateDocumentBoundsV2(value);
333
+ if (!result.valid) {
334
+ throw new Error(
335
+ "SourceCandidateV2 exceeds its supported serialized size or JSON node limit.",
336
+ );
337
+ }
338
+ return result;
339
+ }
340
+
341
+ function planPayloadV1({
285
342
  workspaceRoot,
286
343
  candidateRoot,
287
344
  candidateId,
288
345
  manifest,
289
346
  }) {
290
347
  const payloadRoot = path.join(candidateRoot, `${candidateId}.payload`);
291
- mkdirSync(payloadRoot);
292
- mkdirSync(path.join(payloadRoot, "sha256"));
348
+ if (existsSync(payloadRoot)) {
349
+ throw new Error(`source candidate payload already exists: ${candidateId}`);
350
+ }
293
351
  const payloadRootRelative = relativeToWorkspace(workspaceRoot, payloadRoot);
294
352
  const entries = [];
295
353
  const blobs = new Map();
@@ -308,13 +366,10 @@ function freezePayloadV1({
308
366
  }
309
367
  const payloadPath = path.join(payloadRoot, "sha256", digest);
310
368
  if (!blobs.has(digest)) {
311
- writeFileSync(payloadPath, bytes, { flag: "wx", mode: 0o600 });
312
- blobs.set(digest, bytes.length);
313
- } else if (
314
- lstatSync(payloadPath).isSymbolicLink()
315
- || sha256(readFileSync(payloadPath)) !== digest
316
- ) {
317
- throw new Error(`Source payload blob drifted during freeze: ${digest}`);
369
+ blobs.set(digest, {
370
+ sourcePath,
371
+ sizeBytes: bytes.length,
372
+ });
318
373
  }
319
374
  entries.push({
320
375
  path: manifestEntry.path,
@@ -329,15 +384,46 @@ function freezePayloadV1({
329
384
  storage: "create_only_content_addressed_sha256",
330
385
  root: payloadRootRelative,
331
386
  blobCount: blobs.size,
332
- totalBytes: [...blobs.values()].reduce((total, size) => total + size, 0),
387
+ totalBytes: [...blobs.values()]
388
+ .reduce((total, row) => total + row.sizeBytes, 0),
333
389
  entries,
334
390
  };
335
391
  return {
336
- ...body,
337
- sha256: candidateHash(body),
392
+ payload: {
393
+ ...body,
394
+ sha256: candidateHash(body),
395
+ },
396
+ blobs,
397
+ payloadRoot,
338
398
  };
339
399
  }
340
400
 
401
+ function persistPayloadPlanV1({ candidateRoot, plan }) {
402
+ if (!existsSync(candidateRoot)) mkdirSync(candidateRoot);
403
+ const candidateRootStat = lstatSync(candidateRoot);
404
+ if (
405
+ candidateRootStat.isSymbolicLink()
406
+ || !candidateRootStat.isDirectory()
407
+ || realpathSync(candidateRoot) !== path.resolve(candidateRoot)
408
+ ) {
409
+ throw new Error("Source candidate directory must be a regular directory.");
410
+ }
411
+ mkdirSync(plan.payloadRoot);
412
+ const blobRoot = path.join(plan.payloadRoot, "sha256");
413
+ mkdirSync(blobRoot);
414
+ for (const [digest, row] of plan.blobs) {
415
+ const bytes = readFileSync(row.sourcePath);
416
+ if (bytes.length !== row.sizeBytes || sha256(bytes) !== digest) {
417
+ throw new Error(`Source changed while persisting payload: ${digest}`);
418
+ }
419
+ writeFileSync(path.join(blobRoot, digest), bytes, {
420
+ flag: "wx",
421
+ mode: 0o600,
422
+ });
423
+ }
424
+ return plan.payload;
425
+ }
426
+
341
427
  function payloadContractIssues({
342
428
  workspaceRoot,
343
429
  candidatePath,
@@ -354,7 +440,16 @@ function payloadContractIssues({
354
440
  (entry) => entry.operation !== "deleted",
355
441
  );
356
442
  if (
357
- payload?.schemaVersion !== "OwlCodaRunKitSourcePayloadV1"
443
+ !hasExactKeys(payload, [
444
+ "schemaVersion",
445
+ "storage",
446
+ "root",
447
+ "blobCount",
448
+ "totalBytes",
449
+ "entries",
450
+ "sha256",
451
+ ])
452
+ || payload?.schemaVersion !== "OwlCodaRunKitSourcePayloadV1"
358
453
  || payload.storage !== "create_only_content_addressed_sha256"
359
454
  || payload.root !== expectedRoot
360
455
  || !Number.isInteger(payload.blobCount)
@@ -376,7 +471,14 @@ function payloadContractIssues({
376
471
  for (const entry of payload.entries) {
377
472
  const manifestEntry = expectedByPath.get(entry?.path);
378
473
  if (
379
- !manifestEntry
474
+ !hasExactKeys(entry, [
475
+ "path",
476
+ "payloadPath",
477
+ "sha256",
478
+ "sizeBytes",
479
+ "mode",
480
+ ])
481
+ || !manifestEntry
380
482
  || seenPaths.has(entry.path)
381
483
  || entry.sha256 !== manifestEntry.sha256
382
484
  || !Number.isInteger(entry.sizeBytes)
@@ -438,7 +540,14 @@ function activeLease({ workspaceRoot, executionRoot, workItemId }) {
438
540
 
439
541
  function manifestContractValid(manifest) {
440
542
  if (
441
- manifest?.schemaVersion !== "OwlCodaRunKitDirtySourceManifestV2"
543
+ !hasExactKeys(manifest, [
544
+ "schemaVersion",
545
+ "statusMode",
546
+ "ownedPaths",
547
+ "entries",
548
+ "sha256",
549
+ ])
550
+ || manifest?.schemaVersion !== "OwlCodaRunKitDirtySourceManifestV2"
442
551
  || manifest.statusMode !== "porcelain-v1-z-untracked-all-runkit-excluded"
443
552
  || !Array.isArray(manifest.ownedPaths)
444
553
  || manifest.ownedPaths.length === 0
@@ -450,8 +559,14 @@ function manifestContractValid(manifest) {
450
559
  return false;
451
560
  }
452
561
  for (const entry of manifest.entries) {
562
+ const expectedEntryKeys = entry?.operation === "renamed"
563
+ ? ["operation", "status", "path", "previousPath", "sha256"]
564
+ : entry?.operation === "deleted"
565
+ ? ["operation", "status", "path"]
566
+ : ["operation", "status", "path", "sha256"];
453
567
  if (
454
568
  !entry
569
+ || !hasExactKeys(entry, expectedEntryKeys)
455
570
  || !["added", "deleted", "modified", "renamed"].includes(entry.operation)
456
571
  || typeof entry.status !== "string"
457
572
  || entry.status.length !== 2
@@ -513,18 +628,39 @@ function repositoryActionsAreFalse(actions) {
513
628
  function candidateV2ContractIssues(candidate) {
514
629
  const { candidateSha256, ...body } = candidate ?? {};
515
630
  if (
516
- candidate?.schemaVersion !== "OwlCodaRunKitSourceCandidateV2"
631
+ !hasExactKeys(candidate, [
632
+ "schemaVersion",
633
+ "runId",
634
+ "candidateId",
635
+ "status",
636
+ "sourceMode",
637
+ "baseline",
638
+ "sourceManifest",
639
+ "sourceFingerprint",
640
+ "payload",
641
+ "discovery",
642
+ "repositoryActions",
643
+ "authorizationGranted",
644
+ "candidateSha256",
645
+ ])
646
+ || candidate?.schemaVersion !== "OwlCodaRunKitSourceCandidateV2"
517
647
  || candidate.status !== "frozen"
518
648
  || candidate.sourceMode !== "dirty_worktree_exact_manifest_v2"
519
649
  || candidate.authorizationGranted !== false
520
- || typeof candidateSha256 !== "string"
650
+ || !/^sha256:[a-f0-9]{64}$/u.test(candidateSha256)
521
651
  || candidateSha256 !== candidateHash(body)
522
- || typeof candidate.runId !== "string"
523
- || typeof candidate.candidateId !== "string"
524
- || typeof candidate.baseline?.branch !== "string"
525
- || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(candidate.baseline?.head)
526
- || typeof candidate.discovery?.fromLease !== "string"
527
- || typeof candidate.discovery?.leasePath !== "string"
652
+ || !validIdentifier(candidate.runId)
653
+ || !validIdentifier(candidate.candidateId)
654
+ || !hasExactKeys(candidate.baseline, ["branch", "head"])
655
+ || typeof candidate.baseline.branch !== "string"
656
+ || candidate.baseline.branch.length === 0
657
+ || !/^[a-f0-9]{40}$/u.test(candidate.baseline.head)
658
+ || !hasExactKeys(candidate.sourceFingerprint, ["sha256"])
659
+ || !/^[a-f0-9]{64}$/u.test(candidate.sourceFingerprint.sha256)
660
+ || !hasExactKeys(candidate.discovery, ["fromLease", "leasePath"])
661
+ || !validIdentifier(candidate.discovery.fromLease)
662
+ || typeof candidate.discovery.leasePath !== "string"
663
+ || candidate.discovery.leasePath.length === 0
528
664
  || !repositoryActionsAreFalse(candidate.repositoryActions)
529
665
  || !manifestContractValid(candidate.sourceManifest)
530
666
  || candidate.sourceFingerprint?.sha256
@@ -643,13 +779,14 @@ export function freezeSourceCandidateWithinControlTransactionV2({
643
779
  const { executionRoot, pinGate } = loadActiveExecution(root, runId);
644
780
  if (pinGate.status !== "valid") return { ...pinGate, authorizationGranted: false };
645
781
  const candidateRoot = path.join(executionRoot, "source-candidates");
646
- if (!existsSync(candidateRoot)) mkdirSync(candidateRoot);
647
- const candidateRootStat = lstatSync(candidateRoot);
648
- if (candidateRootStat.isSymbolicLink() || !candidateRootStat.isDirectory()) {
649
- throw new Error("Source candidate directory must be a regular directory.");
650
- }
651
- if (!within(realpathSync(executionRoot), realpathSync(candidateRoot))) {
652
- throw new Error("Source candidate directory escapes its execution root.");
782
+ if (existsSync(candidateRoot)) {
783
+ const candidateRootStat = lstatSync(candidateRoot);
784
+ if (candidateRootStat.isSymbolicLink() || !candidateRootStat.isDirectory()) {
785
+ throw new Error("Source candidate directory must be a regular directory.");
786
+ }
787
+ if (!within(realpathSync(executionRoot), realpathSync(candidateRoot))) {
788
+ throw new Error("Source candidate directory escapes its execution root.");
789
+ }
653
790
  }
654
791
  const candidatePath = path.join(candidateRoot, `${candidateId}.json`);
655
792
  if (existsSync(candidatePath)) {
@@ -665,12 +802,43 @@ export function freezeSourceCandidateWithinControlTransactionV2({
665
802
  workspaceRoot: root,
666
803
  ownedPaths: lease.ownedPaths,
667
804
  });
668
- const payload = freezePayloadV1({
805
+ const payloadPlan = planPayloadV1({
669
806
  workspaceRoot: root,
670
807
  candidateRoot,
671
808
  candidateId,
672
809
  manifest: statusBefore,
673
810
  });
811
+ const buildCandidate = ({ baseline, sourceManifest, payload }) => {
812
+ const body = {
813
+ schemaVersion: "OwlCodaRunKitSourceCandidateV2",
814
+ runId,
815
+ candidateId,
816
+ status: "frozen",
817
+ sourceMode: "dirty_worktree_exact_manifest_v2",
818
+ baseline,
819
+ sourceManifest,
820
+ sourceFingerprint: {
821
+ sha256: sourceManifest.sha256.slice("sha256:".length),
822
+ },
823
+ payload,
824
+ discovery: {
825
+ fromLease: workItemId,
826
+ leasePath: lease.leaseRelativePath,
827
+ },
828
+ repositoryActions: repositoryActionsFalse(),
829
+ authorizationGranted: false,
830
+ };
831
+ return {
832
+ ...body,
833
+ candidateSha256: candidateHash(body),
834
+ };
835
+ };
836
+ assertSourceCandidateDocumentBoundsV2(buildCandidate({
837
+ baseline: baselineBefore,
838
+ sourceManifest: statusBefore,
839
+ payload: payloadPlan.payload,
840
+ }));
841
+ const payload = persistPayloadPlanV1({ candidateRoot, plan: payloadPlan });
674
842
  const statusAfter = sourceManifestV2({
675
843
  workspaceRoot: root,
676
844
  ownedPaths: lease.ownedPaths,
@@ -682,29 +850,12 @@ export function freezeSourceCandidateWithinControlTransactionV2({
682
850
  ) {
683
851
  throw new Error("Owned workspace source changed during candidate freeze.");
684
852
  }
685
- const body = {
686
- schemaVersion: "OwlCodaRunKitSourceCandidateV2",
687
- runId,
688
- candidateId,
689
- status: "frozen",
690
- sourceMode: "dirty_worktree_exact_manifest_v2",
853
+ const candidate = buildCandidate({
691
854
  baseline: baselineAfter,
692
855
  sourceManifest: statusAfter,
693
- sourceFingerprint: {
694
- sha256: statusAfter.sha256.slice("sha256:".length),
695
- },
696
856
  payload,
697
- discovery: {
698
- fromLease: workItemId,
699
- leasePath: lease.leaseRelativePath,
700
- },
701
- repositoryActions: repositoryActionsFalse(),
702
- authorizationGranted: false,
703
- };
704
- const candidate = {
705
- ...body,
706
- candidateSha256: candidateHash(body),
707
- };
857
+ });
858
+ assertSourceCandidateDocumentBoundsV2(candidate);
708
859
  writeJsonExclusiveAtomically(candidatePath, candidate);
709
860
  return {
710
861
  status: "source_candidate_frozen",
@@ -721,6 +872,49 @@ export function freezeSourceCandidateWithinControlTransactionV2({
721
872
  };
722
873
  }
723
874
 
875
+ export function previewSourceCandidateWithinControlTransactionV2({
876
+ workspaceRoot,
877
+ runId,
878
+ workItemId,
879
+ }) {
880
+ const root = realpathSync(workspaceRoot);
881
+ const { executionRoot, pinGate } = loadActiveExecution(root, runId);
882
+ if (pinGate.status !== "valid") {
883
+ return { ...pinGate, authorizationGranted: false };
884
+ }
885
+ const lease = activeLease({
886
+ workspaceRoot: root,
887
+ executionRoot,
888
+ workItemId,
889
+ });
890
+ const baselineBefore = currentBaseline(root);
891
+ const manifestBefore = sourceManifestV2({
892
+ workspaceRoot: root,
893
+ ownedPaths: lease.ownedPaths,
894
+ });
895
+ const manifestAfter = sourceManifestV2({
896
+ workspaceRoot: root,
897
+ ownedPaths: lease.ownedPaths,
898
+ });
899
+ const baselineAfter = currentBaseline(root);
900
+ if (
901
+ manifestBefore.sha256 !== manifestAfter.sha256
902
+ || canonicalJson(baselineBefore) !== canonicalJson(baselineAfter)
903
+ ) {
904
+ throw new Error("Owned workspace source changed during candidate preview.");
905
+ }
906
+ return {
907
+ status: "source_candidate_previewed",
908
+ exitCode: 0,
909
+ runId,
910
+ workItemId,
911
+ baseline: baselineAfter,
912
+ sourceManifest: manifestAfter,
913
+ sourceFingerprint: manifestAfter.sha256.slice("sha256:".length),
914
+ authorizationGranted: false,
915
+ };
916
+ }
917
+
724
918
  export function freezeSourceCandidateV2(options) {
725
919
  const workspaceRoot = realpathSync(options.workspaceRoot);
726
920
  return withControlTransaction(
@@ -732,31 +926,31 @@ export function freezeSourceCandidateV2(options) {
732
926
  );
733
927
  }
734
928
 
735
- export function verifySourceCandidateV2({
929
+ export function verifySourceCandidateDetailsV2({
736
930
  workspaceRoot,
737
931
  candidatePath,
932
+ requireCurrentWorkspace = true,
738
933
  }) {
739
934
  const root = realpathSync(workspaceRoot);
740
935
  const issueCodes = [];
741
936
  let absoluteCandidatePath;
742
937
  let candidate;
743
938
  try {
744
- absoluteCandidatePath = resolveExistingArtifact(
745
- root,
746
- candidatePath,
747
- "candidatePath",
748
- );
749
- const stat = lstatSync(absoluteCandidatePath);
750
- if (stat.isSymbolicLink() || !stat.isFile()) {
751
- throw new Error("candidate is not a regular file");
752
- }
753
- candidate = readJson(absoluteCandidatePath);
939
+ const relativeCandidatePath = safeRelativePath(candidatePath, "candidatePath");
940
+ candidate = readWorkspaceJsonBounded(root, relativeCandidatePath, {
941
+ maxBytes: SOURCE_CANDIDATE_V2_MAX_BYTES,
942
+ maxNodes: SOURCE_CANDIDATE_V2_MAX_NODES,
943
+ });
944
+ absoluteCandidatePath = path.join(root, relativeCandidatePath);
754
945
  } catch {
755
946
  return {
756
- status: "invalid",
757
- exitCode: 2,
758
- issueCodes: ["source_candidate_unreadable"],
759
- authorizationGranted: false,
947
+ gate: {
948
+ status: "invalid",
949
+ exitCode: 2,
950
+ issueCodes: ["source_candidate_unreadable"],
951
+ authorizationGranted: false,
952
+ },
953
+ candidateDocument: null,
760
954
  };
761
955
  }
762
956
  const candidateSha256 = candidate.candidateSha256;
@@ -792,7 +986,7 @@ export function verifySourceCandidateV2({
792
986
  issueCodes.push("source_candidate_lease_unreadable");
793
987
  }
794
988
  }
795
- if (issueCodes.length === 0) {
989
+ if (issueCodes.length === 0 && requireCurrentWorkspace) {
796
990
  try {
797
991
  const currentManifest = sourceManifestV2({
798
992
  workspaceRoot: root,
@@ -812,7 +1006,7 @@ export function verifySourceCandidateV2({
812
1006
  issueCodes.push("source_candidate_baseline_drift");
813
1007
  }
814
1008
  }
815
- return {
1009
+ const gate = {
816
1010
  status: issueCodes.length === 0 ? "valid" : "invalid",
817
1011
  exitCode: issueCodes.length === 0 ? 0 : 2,
818
1012
  candidatePath: relativeToWorkspace(root, absoluteCandidatePath),
@@ -821,6 +1015,14 @@ export function verifySourceCandidateV2({
821
1015
  issueCodes: [...new Set(issueCodes)].sort(),
822
1016
  authorizationGranted: false,
823
1017
  };
1018
+ return {
1019
+ gate,
1020
+ candidateDocument: gate.status === "valid" ? candidate : null,
1021
+ };
1022
+ }
1023
+
1024
+ export function verifySourceCandidateV2(options) {
1025
+ return verifySourceCandidateDetailsV2(options).gate;
824
1026
  }
825
1027
 
826
1028
  class SimulatedMaterializationInterruption extends Error {