@abelxiaoxing/cadence 1.2.4 → 1.4.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 (46) hide show
  1. package/README.md +155 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +83 -15
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +399 -1079
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/openspec-cli.ts +494 -0
  23. package/src/package-delivery.ts +486 -0
  24. package/src/package-verification.ts +509 -0
  25. package/src/route-policy.ts +1 -11
  26. package/src/run-state.ts +4 -16
  27. package/src/run-store.ts +5 -144
  28. package/src/scoped-grep-worker.mjs +23 -0
  29. package/src/scoped-tools.ts +312 -116
  30. package/src/sqlite-schema.ts +171 -0
  31. package/src/storage-schema.ts +352 -0
  32. package/src/subagent-activity.ts +14 -1
  33. package/src/submit-tool.ts +20 -2
  34. package/src/task-ledger.ts +14 -55
  35. package/src/verification-capability.ts +298 -102
  36. package/src/verification-environment.ts +213 -0
  37. package/src/verification-identity.ts +77 -0
  38. package/src/worker-broker.ts +97 -34
  39. package/src/workflow-engine.ts +28 -8285
  40. package/src/workflow-policy.ts +1427 -0
  41. package/src/workflow-state-machine.ts +4478 -0
  42. package/src/workspace-io-entry.mjs +58 -0
  43. package/src/workspace-io-worker.mjs +984 -0
  44. package/src/workspace-io.ts +84 -0
  45. package/src/workspace-store.ts +125 -19
  46. package/skills/abel-workflow/SKILL.md +0 -205
@@ -2,14 +2,16 @@ import { createHash } from "node:crypto";
2
2
  import { chmodSync, lstatSync, mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
-
6
5
  import type { ArtifactStore } from "./artifact-store.ts";
6
+ import { compareCanonicalStrings } from "./canonical.ts";
7
7
  import {
8
8
  diffWritePaths,
9
9
  IMPLEMENTATION_PHASES,
10
10
  type ImplementationPhase,
11
11
  isValidRelativePath,
12
12
  } from "./contracts.ts";
13
+ import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
14
+ import { TASK_SCHEMA } from "./storage-schema.ts";
13
15
 
14
16
  export const TASK_LEDGER_LIMITS = {
15
17
  maxSegmentBytes: 128 * 1024,
@@ -413,7 +415,9 @@ function normalizeContextRefs(value: unknown): TaskContextReference[] {
413
415
  if (new Set(identities).size !== identities.length) {
414
416
  throw new Error("ledger-context-refs-invalid");
415
417
  }
416
- return refs.sort((left, right) => left.path.localeCompare(right.path));
418
+ return refs.sort((left, right) =>
419
+ compareCanonicalStrings(left.path, right.path),
420
+ );
417
421
  }
418
422
 
419
423
  function safeText(value: unknown, code: string, maximum = 256): string {
@@ -694,57 +698,8 @@ export class TaskLedger {
694
698
  throw new Error("task-ledger-database-invalid");
695
699
  }
696
700
  this.#database = new DatabaseSync(databasePath);
697
- this.#database.exec(
698
- "PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA foreign_keys = ON;",
699
- );
700
- this.#database.exec(`
701
- CREATE TABLE IF NOT EXISTS candidates (
702
- candidate_id TEXT PRIMARY KEY,
703
- identity_json TEXT NOT NULL,
704
- state TEXT NOT NULL,
705
- next_sequence INTEGER NOT NULL,
706
- total_bytes INTEGER NOT NULL,
707
- artifact_hash TEXT,
708
- candidate_hash TEXT,
709
- segment_count INTEGER,
710
- paths_json TEXT,
711
- pause_code TEXT
712
- ) STRICT;
713
- CREATE TABLE IF NOT EXISTS candidate_segments (
714
- candidate_id TEXT NOT NULL REFERENCES candidates(candidate_id) ON DELETE CASCADE,
715
- sequence INTEGER NOT NULL,
716
- artifact_hash TEXT NOT NULL,
717
- size_bytes INTEGER NOT NULL,
718
- segment_hash TEXT NOT NULL,
719
- PRIMARY KEY (candidate_id, sequence)
720
- ) STRICT;
721
- CREATE TABLE IF NOT EXISTS task_ledger (
722
- run_id TEXT NOT NULL,
723
- task_id TEXT NOT NULL,
724
- delivery_revision INTEGER NOT NULL,
725
- boundary_hash TEXT NOT NULL,
726
- objective TEXT NOT NULL,
727
- context_refs_json TEXT NOT NULL,
728
- initial_phase TEXT NOT NULL,
729
- PRIMARY KEY (run_id, task_id)
730
- ) STRICT;
731
- CREATE TABLE IF NOT EXISTS task_events (
732
- run_id TEXT NOT NULL,
733
- task_id TEXT NOT NULL,
734
- ordinal INTEGER NOT NULL,
735
- event_id TEXT NOT NULL,
736
- event_json TEXT NOT NULL,
737
- prior_hash TEXT NOT NULL,
738
- event_hash TEXT NOT NULL,
739
- PRIMARY KEY (run_id, task_id, ordinal),
740
- UNIQUE (run_id, task_id, event_id),
741
- FOREIGN KEY (run_id, task_id) REFERENCES task_ledger(run_id, task_id) ON DELETE CASCADE
742
- ) STRICT;
743
- CREATE TABLE IF NOT EXISTS durable_facts (
744
- fact_key TEXT PRIMARY KEY,
745
- fact_json TEXT NOT NULL
746
- ) STRICT;
747
- `);
701
+ configureSqlite(this.#database);
702
+ ensureSqliteSchema(this.#database, TASK_SCHEMA);
748
703
  chmodSync(databasePath, 0o600);
749
704
  }
750
705
 
@@ -1173,7 +1128,7 @@ export class TaskLedger {
1173
1128
  commitVerifiedEvent(input: VerifiedTaskEvent) {
1174
1129
  this.#assertOpen();
1175
1130
  const normalized = normalizeVerifiedEvent(input);
1176
- this.#task(normalized.runId, normalized.taskId);
1131
+ const task = this.#task(normalized.runId, normalized.taskId);
1177
1132
  const rows = this.#events(normalized.runId, normalized.taskId);
1178
1133
  const existing = rows.find((row) => row.event_id === normalized.eventId);
1179
1134
  const json = JSON.stringify(normalized);
@@ -1186,6 +1141,8 @@ export class TaskLedger {
1186
1141
  throw new Error("ledger-event-capacity-exceeded");
1187
1142
  }
1188
1143
  if (normalized.kind === "phase-verified") {
1144
+ if (task.initial_phase === "green" && normalized.phase === "red")
1145
+ throw new Error("ledger-phase-order-invalid");
1189
1146
  const completed = rows
1190
1147
  .map((row) => JSON.parse(row.event_json) as VerifiedTaskEvent)
1191
1148
  .filter(
@@ -1195,7 +1152,9 @@ export class TaskLedger {
1195
1152
  .map((event) => event.phase);
1196
1153
  const required =
1197
1154
  normalized.phase === "green"
1198
- ? "red"
1155
+ ? task.initial_phase === "green"
1156
+ ? undefined
1157
+ : "red"
1199
1158
  : normalized.phase === "refactor"
1200
1159
  ? "green"
1201
1160
  : undefined;
@@ -9,13 +9,21 @@ import path from "node:path";
9
9
  import {
10
10
  type AtomicVerificationContract,
11
11
  type StructuredVerificationContract,
12
+ VERIFICATION_CONFIGURATION_PATHS,
12
13
  type VerificationAdapterCode,
13
14
  validateVerificationContract,
15
+ verificationBoundInputPaths,
14
16
  verificationInputPaths,
15
17
  verificationSteps,
16
18
  } from "./contracts.ts";
17
- import { type Bound, isCurrent, snapshotFiles } from "./file-snapshot.ts";
19
+ import {
20
+ type Bound,
21
+ isCurrent,
22
+ snapshotFile,
23
+ snapshotFiles,
24
+ } from "./file-snapshot.ts";
18
25
  import { isSafeRegularFile, observeSafePath } from "./safe-path.ts";
26
+ import { verificationEnvironmentDigest } from "./verification-identity.ts";
19
27
 
20
28
  export type VerificationCapabilityDiagnostic =
21
29
  | {
@@ -43,6 +51,7 @@ export interface CurrentVerificationCapability {
43
51
  verificationId: string;
44
52
  runnerBindings: VerificationRunnerBinding[];
45
53
  inputSnapshot: Bound;
54
+ runnerSnapshot: { paths: string[]; digest: string };
46
55
  }
47
56
 
48
57
  export type CurrentVerificationCapabilityResult =
@@ -64,14 +73,12 @@ export interface VerificationRunnerEnvironment {
64
73
  }
65
74
 
66
75
  export interface VerificationCapabilityOptions {
76
+ /** Trusted plan write/delete authority, supplied only for isolated execution. */
77
+ executionWritePaths?: readonly string[];
67
78
  dependencyOwner?: string;
68
79
  runnerEnvironment?: VerificationRunnerEnvironment;
69
80
  }
70
81
 
71
- const SHELL_OPERATOR = /[;&|`$<>\n\r\0]/u;
72
- const SIMPLE_TOKEN =
73
- /^(?:[a-z0-9][a-z0-9._:@/=-]*|--?[a-z0-9][a-z0-9._:@/=-]*)$/iu;
74
- const NETWORKING_RUNNERS = new Set(["bunx", "pnpx"]);
75
82
  const WINDOWS_EXECUTABLE_EXTENSIONS = new Set([".exe", ".cmd"]);
76
83
  const DEFAULT_WINDOWS_PATH_EXT = ".COM;.EXE;.BAT;.CMD";
77
84
  const SYSTEM_MOUNT_ROOTS = ["/usr", "/bin", "/lib", "/lib64", "/etc"];
@@ -633,95 +640,13 @@ function packageManifest(root: string): Record<string, unknown> | null {
633
640
  }
634
641
  }
635
642
 
636
- function validateScriptCommand(
637
- root: string,
638
- verificationId: string,
639
- command: string,
640
- dependencyOwner: string,
641
- runnerEnvironment: VerificationRunnerEnvironment,
642
- ): VerificationCapabilityResult {
643
- if (
644
- command.length === 0 ||
645
- command.trim() !== command ||
646
- SHELL_OPERATOR.test(command)
647
- ) {
648
- return adapterFailure(
649
- verificationId,
650
- "script-unsafe",
651
- "approved package script contains unsupported shell syntax",
652
- );
653
- }
654
- const tokens = command.split(/\s+/u);
655
- if (!SIMPLE_TOKEN.test(tokens[0] ?? "")) {
656
- return adapterFailure(
657
- verificationId,
658
- "script-unsafe",
659
- "approved package script cannot be represented as safe tokens",
660
- );
661
- }
662
- const executable = tokens[0] ?? "";
663
- const subcommands = new Set(tokens.slice(1));
664
- if (
665
- NETWORKING_RUNNERS.has(executable) ||
666
- (executable === "bun" && subcommands.has("x")) ||
667
- (executable === "npm" &&
668
- (subcommands.has("exec") || subcommands.has("x"))) ||
669
- (executable === "pnpm" && subcommands.has("dlx")) ||
670
- (executable === "yarn" && subcommands.has("dlx"))
671
- ) {
672
- return adapterFailure(
673
- verificationId,
674
- "script-unsafe",
675
- "package script uses a runner that can download implicitly",
676
- );
677
- }
678
- if (executable === "npx") {
679
- const local = tokens[1] === "--no-install" ? tokens[2] : undefined;
680
- if (
681
- !local ||
682
- !localExecutable(root, local, dependencyOwner, runnerEnvironment)
683
- ) {
684
- return adapterFailure(
685
- verificationId,
686
- "local-executable-missing",
687
- "npx requires --no-install and an installed local executable",
688
- );
689
- }
690
- const runner = resolveVerificationRunner("npx", runnerEnvironment);
691
- return runner
692
- ? capabilitySuccess(verificationId, [runner])
693
- : adapterFailure(
694
- verificationId,
695
- "runner-missing",
696
- "npx is unavailable or cannot be mounted safely",
697
- );
698
- }
699
- if (["node", "bun"].includes(executable)) {
700
- const runner = resolveVerificationRunner(executable, runnerEnvironment);
701
- return runner
702
- ? capabilitySuccess(verificationId, [runner])
703
- : adapterFailure(
704
- verificationId,
705
- "runner-missing",
706
- `approved runner ${executable} is unavailable or cannot be mounted safely`,
707
- );
708
- }
709
- return localExecutable(root, executable, dependencyOwner, runnerEnvironment)
710
- ? capabilitySuccess(verificationId)
711
- : adapterFailure(
712
- verificationId,
713
- "local-executable-missing",
714
- `local executable ${executable} is unavailable`,
715
- );
716
- }
717
-
718
643
  function validatePackageScript(
719
644
  root: string,
720
645
  verificationId: string,
721
646
  packageManager: string,
722
647
  script: string,
723
648
  approvedCommand: string,
724
- dependencyOwner = root,
649
+ _dependencyOwner = root,
725
650
  runnerEnvironment: VerificationRunnerEnvironment = {},
726
651
  ): VerificationCapabilityResult {
727
652
  const packageRunner = resolveVerificationRunner(
@@ -755,19 +680,91 @@ function validatePackageScript(
755
680
  `package.json script ${script} differs from the Gate B contract`,
756
681
  );
757
682
  }
758
- const scriptCapability = validateScriptCommand(
759
- root,
760
- verificationId,
761
- actual,
762
- dependencyOwner,
763
- runnerEnvironment,
683
+ // Approved scripts are opaque shell programs and may invoke nested scripts
684
+ // or hooks. Supply the available supported runtimes without parsing shell
685
+ // syntax or rejecting composition; isolation still owns their execution.
686
+ const runtimes = ["node", "bun", "npm", "npx", "pnpm", "yarn"].flatMap(
687
+ (command) => {
688
+ const binding = resolveVerificationRunner(command, runnerEnvironment);
689
+ return binding ? [binding] : [];
690
+ },
764
691
  );
765
- return scriptCapability.ok
766
- ? capabilitySuccess(verificationId, [
767
- packageRunner,
768
- ...scriptCapability.runnerBindings,
769
- ])
770
- : scriptCapability;
692
+ return capabilitySuccess(verificationId, [packageRunner, ...runtimes]);
693
+ }
694
+
695
+ /** Inspect active directives, never comments or arbitrary words in values. */
696
+ function unsupportedExecutionConfiguration(source: string): boolean {
697
+ const restricted = new Set([
698
+ "scriptshell",
699
+ "userconfig",
700
+ "globalconfig",
701
+ "yarnpath",
702
+ "plugins",
703
+ "auth",
704
+ "authtoken",
705
+ "password",
706
+ "token",
707
+ "npmauthtoken",
708
+ "npmauthident",
709
+ ]);
710
+ for (const line of source.split(/\r?\n/u)) {
711
+ let quote = "";
712
+ let escaped = false;
713
+ let active = "";
714
+ const parts: string[] = [];
715
+ let flowDepth = 0;
716
+ for (const character of line) {
717
+ if (escaped) {
718
+ active += character;
719
+ escaped = false;
720
+ continue;
721
+ }
722
+ if (character === "\\" && quote === '"') {
723
+ active += character;
724
+ escaped = true;
725
+ continue;
726
+ }
727
+ if (quote) {
728
+ active += character;
729
+ if (character === quote) quote = "";
730
+ } else if (character === '"' || character === "'") {
731
+ quote = character;
732
+ active += character;
733
+ } else if (
734
+ character === "#" ||
735
+ (character === ";" && active.trim() === "")
736
+ ) {
737
+ break;
738
+ } else if (
739
+ character === "{" ||
740
+ character === "}" ||
741
+ (character === "," && flowDepth > 0)
742
+ ) {
743
+ if (character === "{") flowDepth++;
744
+ if (character === "}") flowDepth = Math.max(0, flowDepth - 1);
745
+ parts.push(active);
746
+ active = "";
747
+ } else active += character;
748
+ }
749
+ parts.push(active);
750
+ for (const part of parts) {
751
+ if (part.includes("${")) return true;
752
+ const directive = part.match(
753
+ /^\s*(?:"((?:\\.|[^"\\])*)"|'([^']*)'|([^\s=]+))\s*(.*)$/u,
754
+ );
755
+ if (!directive) continue;
756
+ const rawKey = directive[1] ?? directive[2] ?? directive[3];
757
+ if (!rawKey.endsWith(":") && !directive[4]) continue;
758
+ const key = rawKey
759
+ .replace(/:$/u, "")
760
+ .split(":")
761
+ .at(-1)
762
+ ?.replace(/[-_]/gu, "")
763
+ .toLowerCase();
764
+ if (key && restricted.has(key)) return true;
765
+ }
766
+ }
767
+ return false;
771
768
  }
772
769
 
773
770
  function validateAtomicCapability(
@@ -775,7 +772,73 @@ function validateAtomicCapability(
775
772
  step: AtomicVerificationContract,
776
773
  dependencyOwner: string,
777
774
  runnerEnvironment: VerificationRunnerEnvironment,
775
+ executionWritePaths: readonly string[],
778
776
  ): VerificationCapabilityResult {
777
+ if (
778
+ step.kind === "vitest" &&
779
+ [
780
+ ...step.args,
781
+ ...(step.runner.kind === "package-script"
782
+ ? step.runner.command.split(/\s+/u)
783
+ : []),
784
+ ].some((arg) => /^--(?:reporters?|outputFile)(?:[.=]|$)/u.test(arg))
785
+ ) {
786
+ return adapterFailure(
787
+ step.id,
788
+ "report-arguments-conflict",
789
+ "Vitest report arguments are owned by the adapter",
790
+ );
791
+ }
792
+ if (step.executionBindings) {
793
+ for (const [relative, expected] of Object.entries(step.executionBindings)) {
794
+ if (executionWritePaths.includes(relative)) continue;
795
+ const observation = observeSafePath(root, relative);
796
+ if (
797
+ (expected === null && observation.kind !== "absent") ||
798
+ (expected !== null && snapshotFile(root, relative)?.sha256 !== expected)
799
+ ) {
800
+ return adapterFailure(
801
+ step.id,
802
+ "verification-config-mismatch",
803
+ "Bound execution inputs changed; recompile the verification contract",
804
+ );
805
+ }
806
+ }
807
+ }
808
+ for (const relative of verificationBoundInputPaths(step).filter((file) =>
809
+ VERIFICATION_CONFIGURATION_PATHS.includes(file),
810
+ )) {
811
+ const observation = observeSafePath(root, relative);
812
+ if (!["file", "absent"].includes(observation.kind))
813
+ return adapterFailure(
814
+ step.id,
815
+ "verification-config-unsafe",
816
+ "Execution configuration must be a regular file",
817
+ );
818
+ if (
819
+ observation.kind !== "file" ||
820
+ ![
821
+ ".npmrc",
822
+ ".yarnrc",
823
+ ".yarnrc.yml",
824
+ "bunfig.toml",
825
+ "pnpm-workspace.yaml",
826
+ ].includes(relative)
827
+ )
828
+ continue;
829
+ if (
830
+ lstatSync(path.join(root, relative)).size > 64 * 1024 ||
831
+ unsupportedExecutionConfiguration(
832
+ readFileSync(path.join(root, relative), "utf8"),
833
+ )
834
+ ) {
835
+ return adapterFailure(
836
+ step.id,
837
+ "verification-config-unsafe",
838
+ "Execution configuration requires unsupported credentials or host configuration",
839
+ );
840
+ }
841
+ }
779
842
  if (step.kind === "package-script") {
780
843
  return validatePackageScript(
781
844
  root,
@@ -862,6 +925,57 @@ function validateAtomicCapability(
862
925
  return capabilitySuccess(step.id);
863
926
  }
864
927
 
928
+ /** Enrich only a new design draft, never rewrite proof-bound canonical delivery bytes. */
929
+ export function bindDraftVerificationInputs(
930
+ root: string,
931
+ draft: unknown,
932
+ ): unknown {
933
+ const visit = (value: unknown): unknown => {
934
+ if (Array.isArray(value)) return value.map(visit);
935
+ if (!value || typeof value !== "object") return value;
936
+ const item = Object.fromEntries(
937
+ Object.entries(value).map(([key, child]) => [
938
+ key,
939
+ // Gate A is immutable approval authority, not an execution contract.
940
+ key === "changeContract" ? structuredClone(child) : visit(child),
941
+ ]),
942
+ );
943
+ if (
944
+ item.kind === "package-script" &&
945
+ item.command === undefined &&
946
+ typeof item.script === "string"
947
+ ) {
948
+ const scripts = packageManifest(root)?.scripts as
949
+ | Record<string, unknown>
950
+ | undefined;
951
+ item.command = scripts?.[item.script];
952
+ }
953
+ if (
954
+ ["vitest", "package-script", "static-check"].includes(
955
+ String(item.kind),
956
+ ) &&
957
+ typeof item.id === "string"
958
+ ) {
959
+ const validation = validateVerificationContract(item);
960
+ if (
961
+ validation.ok &&
962
+ verificationBoundInputPaths(validation.value).some((file) =>
963
+ VERIFICATION_CONFIGURATION_PATHS.includes(file),
964
+ )
965
+ ) {
966
+ item.executionBindings = Object.fromEntries(
967
+ VERIFICATION_CONFIGURATION_PATHS.map((relative) => [
968
+ relative,
969
+ snapshotFile(root, relative)?.sha256 ?? null,
970
+ ]),
971
+ );
972
+ }
973
+ }
974
+ return item;
975
+ };
976
+ return visit(draft);
977
+ }
978
+
865
979
  export function validateVerificationCapability(
866
980
  root: string,
867
981
  value: unknown,
@@ -882,6 +996,26 @@ export function bindCurrentVerificationCapability(
882
996
  if (!validation.ok) {
883
997
  throw new Error("verification-capability-invariant");
884
998
  }
999
+ let paths: string[];
1000
+ let runnerDigest: string;
1001
+ try {
1002
+ paths = verificationRunnerFiles(
1003
+ options.dependencyOwner ?? root,
1004
+ validation.value,
1005
+ capability.runnerBindings,
1006
+ );
1007
+ runnerDigest = verificationEnvironmentDigest(paths);
1008
+ } catch {
1009
+ return {
1010
+ ok: false,
1011
+ diagnostic: {
1012
+ kind: "verification-adapter",
1013
+ verificationId: capability.verificationId,
1014
+ code: "runner-missing",
1015
+ message: "Installed verification runner changed or is unavailable",
1016
+ },
1017
+ };
1018
+ }
885
1019
  return {
886
1020
  ok: true,
887
1021
  value: {
@@ -892,8 +1026,9 @@ export function bindCurrentVerificationCapability(
892
1026
  })),
893
1027
  inputSnapshot: snapshotFiles(
894
1028
  root,
895
- verificationInputPaths(validation.value),
1029
+ verificationBoundInputPaths(validation.value),
896
1030
  ),
1031
+ runnerSnapshot: { paths, digest: runnerDigest },
897
1032
  },
898
1033
  };
899
1034
  }
@@ -902,7 +1037,65 @@ export function isVerificationCapabilityCurrent(
902
1037
  root: string,
903
1038
  capability: CurrentVerificationCapability,
904
1039
  ): boolean {
905
- return isCurrent(root, capability.inputSnapshot);
1040
+ try {
1041
+ return (
1042
+ isCurrent(root, capability.inputSnapshot) &&
1043
+ verificationEnvironmentDigest(capability.runnerSnapshot.paths) ===
1044
+ capability.runnerSnapshot.digest
1045
+ );
1046
+ } catch {
1047
+ return false;
1048
+ }
1049
+ }
1050
+
1051
+ export function verificationRunnerFiles(
1052
+ dependencyOwner: string,
1053
+ verification: StructuredVerificationContract,
1054
+ bindings: readonly VerificationRunnerBinding[],
1055
+ ): string[] {
1056
+ const files = bindings.flatMap((binding) => [
1057
+ binding.executablePath,
1058
+ ...(binding.fixedArgs ?? []).filter((arg) => path.isAbsolute(arg)),
1059
+ ]);
1060
+ for (const file of [...new Set(files)]) {
1061
+ // Keep the launcher identity and explicitly include its resolved bytes:
1062
+ // the digest intentionally does not follow arbitrary dependency symlinks.
1063
+ const resolved = realpathSync(file);
1064
+ files.push(resolved);
1065
+ // System runners have no mountSource. Bind their installed package too,
1066
+ // including implementation modules and bundled dependencies beyond bin/.
1067
+ for (
1068
+ let directory = path.dirname(resolved);
1069
+ directory !== path.dirname(directory);
1070
+ directory = path.dirname(directory)
1071
+ ) {
1072
+ if (
1073
+ lstatSync(path.join(directory, "package.json"), {
1074
+ throwIfNoEntry: false,
1075
+ })?.isFile()
1076
+ ) {
1077
+ files.push(directory);
1078
+ break;
1079
+ }
1080
+ }
1081
+ }
1082
+ for (const step of verificationSteps(verification)) {
1083
+ if (
1084
+ !("runner" in step) ||
1085
+ step.runner.kind === "node" ||
1086
+ step.runner.kind === "package-script"
1087
+ )
1088
+ continue;
1089
+ const executable =
1090
+ step.kind === "vitest" ? "vitest" : step.runner.executable;
1091
+ const file = path.join(dependencyOwner, "node_modules/.bin", executable);
1092
+ try {
1093
+ files.push(file, realpathSync(file));
1094
+ } catch {
1095
+ /* Admission owns missing runner diagnostics. */
1096
+ }
1097
+ }
1098
+ return [...new Set(files)];
906
1099
  }
907
1100
 
908
1101
  export function validateVerificationAdapterCapability(
@@ -945,6 +1138,7 @@ function validateCapability(
945
1138
  dependencyOwner,
946
1139
  options.runnerEnvironment ?? {},
947
1140
  requireInputAvailability,
1141
+ options.executionWritePaths ?? [],
948
1142
  );
949
1143
  }
950
1144
 
@@ -954,6 +1148,7 @@ function validateAcceptedVerificationCapability(
954
1148
  dependencyOwner: string,
955
1149
  runnerEnvironment: VerificationRunnerEnvironment,
956
1150
  requireInputAvailability: boolean,
1151
+ executionWritePaths: readonly string[],
957
1152
  ): VerificationCapabilityResult {
958
1153
  if (requireInputAvailability) {
959
1154
  for (const input of verificationInputPaths(verification)) {
@@ -973,6 +1168,7 @@ function validateAcceptedVerificationCapability(
973
1168
  step,
974
1169
  dependencyOwner,
975
1170
  runnerEnvironment,
1171
+ executionWritePaths,
976
1172
  );
977
1173
  if (!capability.ok) return capability;
978
1174
  runnerBindings.push(...capability.runnerBindings);