@rightkit/release 0.6.13 → 0.6.15

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 (111) hide show
  1. package/README.md +49 -1
  2. package/adapter-env.mjs +165 -0
  3. package/android-ndk.mjs +5 -1
  4. package/android-release.mjs +73 -8
  5. package/app-check.mjs +22 -8
  6. package/app-closure.mjs +214 -0
  7. package/assemble-app-command.mjs +24 -7
  8. package/atomic-write.mjs +57 -0
  9. package/bin/rightkit-gate.mjs +1193 -337
  10. package/broker-client.mjs +106 -0
  11. package/broker-exception.mjs +211 -0
  12. package/broker-handshake.mjs +160 -0
  13. package/broker-pins.mjs +27 -8
  14. package/broker-wire.mjs +262 -0
  15. package/build-launch.mjs +364 -12
  16. package/build-ownership.mjs +117 -49
  17. package/build-release.mjs +3 -2
  18. package/cache-command.mjs +7 -2
  19. package/candidate-build-receipt.mjs +96 -0
  20. package/candidate-receipt-command.mjs +29 -0
  21. package/cargo-contract.mjs +4 -3
  22. package/cargo-ledger.mjs +1 -1
  23. package/cargo-license-notices.mjs +2 -2
  24. package/cargo-shapes.mjs +332 -0
  25. package/cargo-target-shim.mjs +44 -5
  26. package/cargo-target.mjs +5 -0
  27. package/carried-adapter-run.mjs +31 -0
  28. package/carried-cargo-run.mjs +9 -0
  29. package/carried-cargo.mjs +237 -0
  30. package/carrier-census.mjs +124 -0
  31. package/class-a-launch-guard.mjs +266 -33
  32. package/cli/right-release.mjs +2 -0
  33. package/clippy-route.mjs +23 -11
  34. package/compile-inputs.mjs +9 -12
  35. package/control-plane.mjs +18 -0
  36. package/deps-cargo-lock.mjs +208 -0
  37. package/deps.mjs +5 -0
  38. package/deterministic-scans.mjs +273 -44
  39. package/development-signing.mjs +14 -1
  40. package/development.mjs +112 -26
  41. package/direct-origin.mjs +107 -0
  42. package/disk-command.mjs +1 -0
  43. package/disk-policy.mjs +18 -3
  44. package/freshness-census.mjs +206 -58
  45. package/gate-executor.mjs +30 -12
  46. package/gate-stages.mjs +93 -16
  47. package/github-release.mjs +2 -1
  48. package/github-upload-gate.mjs +2 -1
  49. package/install-tauri-authenticode.mjs +346 -0
  50. package/ios-dev.mjs +77 -23
  51. package/ios-package-recipe.mjs +100 -41
  52. package/linux-artifacts.mjs +17 -2
  53. package/linux-qualifier.mjs +83 -0
  54. package/local-publish.mjs +36 -14
  55. package/local-runtime.mjs +2 -2
  56. package/lock-identity.mjs +138 -0
  57. package/lsclean.sh +0 -0
  58. package/managed-cargo.mjs +161 -44
  59. package/managed-roots.mjs +130 -12
  60. package/media-sidecar-catalog.mjs +1 -1
  61. package/native-adapters.mjs +411 -0
  62. package/native-helpers.mjs +26 -18
  63. package/native-tool-planning.mjs +94 -0
  64. package/ownership-mark.mjs +182 -0
  65. package/pack-digest.mjs +122 -0
  66. package/package.json +12 -12
  67. package/pipeline-receipts.mjs +302 -302
  68. package/platform-dir.mjs +14 -0
  69. package/pnpm-adapter-grammar.mjs +226 -0
  70. package/progress-control.mjs +7 -2
  71. package/provenance-real-receipt.pins.json +7 -0
  72. package/provenance-verify.mjs +262 -1
  73. package/publication-outcomes.mjs +21 -0
  74. package/publish-update.mjs +2 -1
  75. package/qa-journeys-prebuilt-run.mjs +18 -0
  76. package/qa-journeys-prebuilt.mjs +148 -0
  77. package/qa-snapshot-run.mjs +53 -0
  78. package/qa-snapshot.mjs +164 -0
  79. package/qualification-verify.mjs +144 -0
  80. package/qualify-reuse.mjs +38 -0
  81. package/recipe-build.mjs +16 -3
  82. package/release-qa-audit.mjs +2 -2
  83. package/release-qa-command.mjs +2 -1
  84. package/release-qa-receipt.mjs +3 -2
  85. package/release-state.mjs +3 -2
  86. package/release.mjs +14 -3
  87. package/rightkit-drift.mjs +27 -5
  88. package/rightkit-versions.json +15 -11
  89. package/rustright-names.mjs +32 -0
  90. package/scan-tool-enrolment.mjs +167 -0
  91. package/sign-windows.mjs +212 -112
  92. package/signing-files.mjs +35 -0
  93. package/signing-identity.mjs +7 -0
  94. package/snapshot-broker.mjs +528 -57
  95. package/snapshot-check.mjs +95 -11
  96. package/snapshot-metadata.mjs +123 -0
  97. package/snapshot-request.mjs +389 -96
  98. package/stage-binary.mjs +22 -4
  99. package/stage-commands.mjs +56 -13
  100. package/stage-sidecar.mjs +93 -21
  101. package/suite-signing-policy.mjs +657 -0
  102. package/suite-signing.mjs +223 -0
  103. package/swift-scratch.mjs +25 -0
  104. package/tauri-locked.mjs +25 -0
  105. package/test-command.mjs +164 -29
  106. package/unit-gate.mjs +201 -31
  107. package/upload-large.mjs +0 -0
  108. package/web-build.mjs +2 -2
  109. package/win-package-recipe.mjs +26 -2
  110. package/bin/darwin/rightkit-swap +0 -0
  111. package/bin/darwin/rightkit-swap.sign.json +0 -6
@@ -28,6 +28,10 @@
28
28
  // rightkit-gate [--base <ref>] [--plan] [--no-cache]
29
29
  // run the gate (default base origin/main); --plan prints the scans and runs none;
30
30
  // --no-cache (or RK_GATE_CACHE=0) reruns every scan
31
+ // rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform p] [--commit <rev>]
32
+ // prints one JSON line (schema rightkit-gate.route/1) naming the route this host takes for the
33
+ // stage and the refusal it would meet, and runs nothing: no ownership, snapshot, check, run record
34
+ // or receipt (rightkit#1297; the release canary probes it for every app). Exit 0 answered, 2 not
31
35
  // rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]
32
36
  // the deterministic stage: the app check (../app-check.mjs), then every class A scan
33
37
  // (lockfiles, cargo-deny/audit, secrets, licences, manifests, workflow lint, syntax) over the
@@ -64,20 +68,28 @@
64
68
  // SIGINT/SIGTERM stop scheduling, kill every running scan's process tree and exit non-zero listing the scans not run.
65
69
  import fs from "node:fs";
66
70
  import { createHash, randomUUID } from "node:crypto";
67
- import { resolveManagedRoots } from "../managed-roots.mjs";
71
+ import { envNamed, gateRunsRoot, resolveManagedRoots } from "../managed-roots.mjs";
72
+ import { resolveBrokerClient, spawnBrokerClient } from "../broker-client.mjs";
68
73
  import os from "node:os";
69
74
  import path from "node:path";
70
75
  import { FOREIGN_PRE_COMMIT, retireHooksIn } from "../hook-retirement.mjs";
71
76
  import { fileURLToPath } from "node:url";
72
- import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
77
+ import { BIOME_CONFIG_FILES, ESLINT_CONFIG_FILES, WORKSPACE_RUST_FILE, ancestorConfigChanged, ancestorDirsOf, canonicalJson, defaultSpawn, defaultSpawnAsync, digestScanSet, fileScanCache, findRustfmt, metadataArgs, planScans, runScans, scanToolPolicyFromPipeline, scanDeferrals, scanFailures } from "../deterministic-scans.mjs";
73
78
  import { changedLockNames, parseLock, reach } from "../lock-graph.mjs";
74
79
  import { resolveExecutor } from "../gate-executor.mjs";
75
- import { directPlatform } from "../managed-cargo.mjs";
76
- import { bindingOf, endRun, readReceipt, RECEIPT_SCHEMA, receiptKey, sameBinding, sameSource, sealedFor, stagesTo, startRun, STAGES, STAGE_SOURCES, writeReceipt } from "../gate-stages.mjs";
80
+ import { ALL_UNITS_ERRORS, ALL_UNITS_EXCEPTIONS_FILE, BROKER_EXCEPTIONS_FILE, BROKER_EXCEPTION_ERRORS, CARGO_WORKSPACE_LIST_FILE, CARGO_WORKSPACE_LIST_KEY, RIGHTKIT_IDENTITY_FILE, authorizeAllUnits, authorizeNoBroker, checkCargoWorkspaceList, describeAllUnitsException, describeBrokerException, isRightKitIdentity, unitsOfSelector } from "../broker-exception.mjs";
81
+ import { SIGNING_REFUSAL_CODES, suiteSigningRefusals } from "../suite-signing-policy.mjs";
82
+ import { gitListRustFiles } from "../signing-files.mjs";
83
+ import { LINUX_BROKER_ROUTE, NO_BROKER_ENV, directPlatform, linuxBrokerDeclared } from "../managed-cargo.mjs";
84
+ import { bindingOf, compileOutputsRecord, compileReuseProblem, endRun, readReceipt, RECEIPT_SCHEMA, receiptKey, sameBinding, sameSource, sealedFor, stagesTo, startRun, STAGES, STAGE_SOURCES, writeReceipt } from "../gate-stages.mjs";
77
85
  import { spawnSync as spawnInherit } from "../background-process.mjs";
78
86
  import { readDiskPolicy, scratchRoots } from "../disk-policy.mjs";
79
87
  import { buildDirectReceipt, directVerdict, resolveTargetDirectory, verifyDirectReceipt, writeDirectDiagnostics } from "../direct-receipt.mjs";
80
88
  import { createFreshnessCensus } from "../freshness-census.mjs";
89
+ import { platformDirOf } from "../platform-dir.mjs";
90
+ import { nativeToolRefusal, nativeToolUses } from "../native-tool-planning.mjs";
91
+ import { BUILD_CONTROL_PROTOCOL, HANDSHAKE_PROBE_ID, RUNTIME_MANIFEST_FILE, mergeCapabilityViews, parseRuntimeIdentity, queryBrokerHandshake } from "../broker-handshake.mjs";
92
+ import net from "node:net";
81
93
 
82
94
  const ZERO = /^0+$/;
83
95
  const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
@@ -91,7 +103,9 @@ export const PARTIAL_EXIT = 3;
91
103
  /** The platforms a door run can package: desktop (signed, sealed, installed QA, publisher upload) and mobile (signed, sealed, store test track). */
92
104
  export const PLATFORMS = Object.freeze(["mac", "win", "ios", "android"]);
93
105
  export const MOBILE_PLATFORMS = Object.freeze(new Set(["ios", "android"]));
94
- const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
106
+ const USAGE = `usage: rightkit-gate --to check|compile|package|qualify|release [--platform mac|win|ios|android] [--tier patch|update] [--receipt-in <file>] [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify|release --unit npm:@rightkit/<name>|cargo:rightkit-<name> [--receipt-out <file>] | rightkit-gate --to check|compile|package|qualify --units-file <json {"units":[<selector>,...]} written for this run> | rightkit-gate --to check|compile|package|qualify --all-units-exception <id> (RightKit only: its unit is named on every run, or every unit by Adrian's entry in ci/all-units-exceptions.json of the commit) | rightkit-gate --to check|compile|package|qualify [--unit <selector>] --no-broker <exception-id> (Mac and Dell only; Adrian's recorded exception in .rightkit/broker-exceptions.json of the commit) | rightkit-gate --to dev|test [--commit <rev>] [-- <right-release args>] | rightkit-gate --to check|compile|package|qualify|dev|test --route [--platform mac|win|ios|android] [--commit <rev>] | rightkit-gate stage --platform mac|win [--tier patch|update] | rightkit-gate [--base <ref>] [--plan] [--no-cache] | rightkit-gate install-hook | rightkit-gate env-digest | rightkit-gate snapshot [--commit <rev>] [--verify [--env <sha256>]] [--json] | rightkit-gate live-scans --repo <root> | rightkit-gate clippy [<cargo clippy args>] | rightkit-gate env-digest [--snapshot <dir> --commit <sha>] | rightkit-gate check [--repo <dir>] [--commit <rev> | --worktree] [--rule-tier compile|release] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] | rightkit-gate check --commit <sha> --full-a --snapshot <dir> | --version`;
107
+ /** The host platform (process.platform) that constructs each platform's artifact; a platform missing here (android) builds on any host. */
108
+ const STAGE_NATIVE_HOST = Object.freeze({ mac: "darwin", ios: "darwin", win: "win32" });
95
109
 
96
110
  // ---- pure helpers (exported for tests) ----------------------------------------------------------------------
97
111
 
@@ -232,13 +246,13 @@ function workspaceMetadata({ root, spawn, cargoCommand, dir, realpath }) {
232
246
  * { present, units: [{ name, dir, publish }], cargoDir, uncovered }.
233
247
  */
234
248
  /**
235
- * Where the snapshot export lives. The RustRight broker refuses a job whose workspace is on the system volume
236
- * (RK_INTERNAL_DRIVE_REFUSED: agents never write to the internal drive), and os.tmpdir() is on it on every Mac and Dell, so on a
237
- * host with a disk-policy scratch root (/Volumes/D/scratch, D:\scratch) the export goes there. RK_GATE_TMPDIR overrides; os.tmpdir()
238
- * is the fallback where no scratch root exists (CI runners, cloud containers), where plain cargo runs and nothing refuses the system
239
- * volume. (rightkit#1215)
249
+ * Where the gate's own temporary files live (cargo-deny's metadata file and config overlay, read by the broker by absolute path). The
250
+ * RustRight broker refuses paths on the system volume (RK_INTERNAL_DRIVE_REFUSED: agents never write to the internal drive), and
251
+ * os.tmpdir() is on it on every Mac and Dell, so on a host with a disk-policy scratch root (/Volumes/D/scratch, D:\scratch) they go
252
+ * there. RK_GATE_TMPDIR overrides; os.tmpdir() is the fallback where no scratch root exists (CI runners, cloud containers). The gate
253
+ * exports nothing and copies no source: this is not a source location (docs/GATE-CONTRACT.md, 2026-10-29: no export or copy).
240
254
  */
241
- export function exportTmpRoot({ env = process.env, platform = process.platform, exists = fs.existsSync, policy = null } = {}) {
255
+ export function gateTmpRoot({ env = process.env, platform = process.platform, exists = fs.existsSync, policy = null } = {}) {
242
256
  if (env.RK_GATE_TMPDIR) return env.RK_GATE_TMPDIR;
243
257
  try {
244
258
  const roots = scratchRoots(policy ?? readDiskPolicy(), { platform, env });
@@ -248,64 +262,6 @@ export function exportTmpRoot({ env = process.env, platform = process.platform,
248
262
  return os.tmpdir();
249
263
  }
250
264
 
251
- /** The files cargo metadata, deny and audit read, in the repository root and in every workspace that has a manifest. */
252
- export const CARGO_INPUT_NAMES = Object.freeze(["Cargo.toml", "Cargo.lock", "deny.toml", "rust-toolchain.toml", "rust-toolchain", ".cargo"]);
253
-
254
- /**
255
- * The Cargo inputs of the live checkout `root` that are not blob-identical to `snapshot`'s (a commit or tree): a changed or deleted
256
- * tracked input, or any untracked file (ignored ones included, cargo reads them) under those paths. Empty means the broker may scan
257
- * the primary checkout for exactly the snapshot's result. `manifests` are the snapshot's tracked Cargo.toml paths (posix).
258
- */
259
- export function cargoInputsDiffer({ root, snapshot, manifests, spawn = defaultSpawn }) {
260
- const git = (...a) => spawn("git", a, { cwd: root });
261
- // Every manifest directory and every ancestor up to the root: cargo reads a .cargo/config.toml or a rust-toolchain file from any of them.
262
- const dirs = new Set([""]);
263
- for (const m of manifests) { const parts = path.posix.dirname(m).split("/").filter((x) => x && x !== "."); for (let i = 1; i <= parts.length; i++) dirs.add(parts.slice(0, i).join("/")); }
264
- const dirList = [...dirs];
265
- const paths = dirList.flatMap((d) => CARGO_INPUT_NAMES.map((n) => (d ? `${d}/${n}` : n)));
266
- // Pathspecs go to git in bounded batches: a repository with many crates (RightKit: ~1000 input paths) overflows Windows' 32767-
267
- // character command line in one argv (ENAMETOOLONG on the Dell, 2026-10-27; G4 was the same for rustfmt). Each batch's results
268
- // are merged; a failed batch fails the whole listing closed.
269
- const batches = pathspecBatches(paths);
270
- const listedAll = batches.map((b) => git("ls-tree", "-r", "-z", snapshot, "--", ...b));
271
- const badList = listedAll.find((r) => failed(r));
272
- if (badList) return [`(cannot list ${snapshot}: ${why(badList)})`];
273
- const snap = new Map();
274
- for (const listed of listedAll) for (const entry of text(listed).split("\0").filter(Boolean)) { const tab = entry.indexOf("\t"); const [, , id] = entry.slice(0, tab).split(" "); snap.set(entry.slice(tab + 1), id); }
275
- const differ = new Set();
276
- for (const [p, id] of snap) {
277
- const live = path.join(root, ...p.split("/"));
278
- let liveId = null;
279
- // Cargo reads the live bytes, so the live bytes are hashed raw (`--no-filters`: no clean filter, eol or text attribute can make
280
- // two different byte strings look the same). The one accepted difference is an exact CRLF checkout of an LF blob (D13, Dell
281
- // 2026-10-27: core.autocrlf), which every TOML/toolchain reader treats as the same newline; anything else differs.
282
- try { if (fs.statSync(live).isFile()) { const h = git("hash-object", "--no-filters", "--", live); liveId = failed(h) ? null : text(h).trim(); } } catch { liveId = null; }
283
- if (liveId !== id && !isCrlfCheckoutOf({ git, id, live })) differ.add(p);
284
- }
285
- // Untracked under the input paths, ignored files included: cargo reads a .cargo/config.toml or a deny.toml whether or not git tracks it.
286
- for (const batch of batches) {
287
- const others = git("ls-files", "-z", "--others", "--", ...batch);
288
- if (failed(others)) differ.add(`(cannot list untracked Cargo inputs: ${why(others)})`); // fail closed: an unlisted input is an unknown input
289
- else for (const p of text(others).split("\0").filter(Boolean)) if (!snap.has(p)) differ.add(p);
290
- // A tracked input the snapshot lacks (added in HEAD after the snapshot, say) is live-only too.
291
- const tracked = git("ls-files", "-z", "--", ...batch);
292
- if (failed(tracked)) differ.add(`(cannot list tracked Cargo inputs: ${why(tracked)})`);
293
- else for (const p of text(tracked).split("\0").filter(Boolean)) if (!snap.has(p)) differ.add(p);
294
- }
295
- return [...differ].sort();
296
- }
297
-
298
- /** True only when the live file's bytes are exactly blob `id` with every LF written as CRLF (an autocrlf checkout); fails closed. */
299
- export function isCrlfCheckoutOf({ git, id, live, readFile = fs.readFileSync }) {
300
- try {
301
- const blob = git("cat-file", "blob", id);
302
- if (failed(blob)) return false;
303
- const committed = text(blob);
304
- if (committed.includes("\r")) return false;
305
- return readFile(live, "utf8") === committed.replace(/\n/g, "\r\n");
306
- } catch { return false; }
307
- }
308
-
309
265
  /**
310
266
  * Tool caches inside node_modules that nothing installs: test-result caches (vitest), the vite optimizer and bundled-config temp
311
267
  * files, transpile and other tool caches (.cache). Some hold code a later step executes, so the environment a verdict carries binds
@@ -1111,21 +1067,32 @@ export const QUIET_LEASE_WAIT_DRAIN_MS = 2 * 60 * 1000;
1111
1067
  * Without `rustright` on PATH, or when the broker refuses, the lane runs without a lease and the gate says so: the tests still run,
1112
1068
  * their verdict is simply taken on a possibly busy host. `o.spawn` is the gate's synchronous spawner.
1113
1069
  */
1114
- export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-gate-quiet-tests", ttlMs: baseTtlMs = QUIET_LEASE_TTL_MS, waitDrainMs = QUIET_LEASE_WAIT_DRAIN_MS, pid = process.pid } = {}) {
1070
+ export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-gate-quiet-tests", ttlMs: baseTtlMs = QUIET_LEASE_TTL_MS, waitDrainMs = QUIET_LEASE_WAIT_DRAIN_MS, pid = process.pid, executor = null } = {}) {
1071
+ // The declared Linux broker route (managed-cargo.mjs): its validated client and endpoint, never a PATH lookup.
1072
+ const linuxRoute = executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor : null;
1115
1073
  const which = (cmd) => {
1116
1074
  const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]);
1117
1075
  return failed(r) ? null : (text(r).split(/\r?\n/).map((l) => l.trim()).find(Boolean) ?? null);
1118
1076
  };
1077
+ // The client is started the one way every gate child starts it (broker-client.mjs): on Windows `rustright` is rustright.cmd, which node cannot
1078
+ // spawn, so its target .exe is read from the shim and started directly, without a shell.
1079
+ const clientCall = (client, args) => {
1080
+ const platform = o.platform ?? process.platform;
1081
+ if (linuxRoute) return spawnBrokerClient(o.spawn, linuxRoute.client, args, { env: { ...o.env, ...linuxRoute.env } }, { platform, readFile: o.readFile });
1082
+ return spawnBrokerClient(o.spawn, platform === "win32" ? client : "rustright", args, {}, { platform, readFile: o.readFile });
1083
+ };
1119
1084
  return {
1120
1085
  // `want.ttlMs` (from the scan runner): the lane's own bound, every quiet scan's timeout plus a margin, so the lease cannot
1121
1086
  // expire while a quiet suite still runs (each scan is killed at its timeout first).
1122
1087
  acquire: async (want = {}) => {
1123
1088
  const ttlMs = Number.isFinite(want.ttlMs) && want.ttlMs > baseTtlMs ? want.ttlMs : baseTtlMs;
1124
- // A direct (Linux) host never takes a broker lease, whatever is on PATH (managed-cargo directPlatform).
1125
- if (directPlatform(o.platform ?? process.platform)) { o.out(`${tag} note: direct host (no broker on ${o.platform ?? process.platform}); quiet test scans run without a broker lease`); return null; }
1126
- if (!which("rustright")) { o.out(`${tag} note: no rustright on PATH; quiet test scans run without a broker lease`); return null; }
1089
+ if (o.brokerException) { o.out(`${tag} note: ${o.brokerException.note} (${o.brokerException.exceptionId}); quiet test scans run without a broker lease`); return null; }
1090
+ // A direct (Linux) host never takes a broker lease, whatever is on PATH (managed-cargo directPlatform); a declared Linux broker route does.
1091
+ if (directPlatform(o.platform ?? process.platform) && !linuxRoute) { o.out(`${tag} note: direct host (no broker on ${o.platform ?? process.platform}); quiet test scans run without a broker lease`); return null; }
1092
+ const client = linuxRoute ? linuxRoute.client : which("rustright");
1093
+ if (!client) { o.out(`${tag} note: no rustright on PATH; quiet test scans run without a broker lease`); return null; }
1127
1094
  const started = Date.now();
1128
- const r = o.spawn("rustright", ["quiet", "acquire", "--ttl-ms", String(ttlMs), "--reason", reason, "--wait-drain-ms", String(waitDrainMs), "--holder-pid", String(pid), "--json"]);
1095
+ const r = clientCall(client, ["quiet", "acquire", "--ttl-ms", String(ttlMs), "--reason", reason, "--wait-drain-ms", String(waitDrainMs), "--holder-pid", String(pid), "--json"]);
1129
1096
  if (failed(r)) { o.out(`${tag} note: broker quiet lease refused (${(why(r) || `exit ${r?.status}`).slice(0, 200)}); quiet test scans run without a lease`); return null; }
1130
1097
  let id = null;
1131
1098
  // The CLI prints the broker's QUIET_LEASE event: { type, granted, state, id } when held; a refusal exits non-zero above.
@@ -1135,7 +1102,7 @@ export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-g
1135
1102
  return { id, env: { RUSTRIGHT_QUIET_LEASE: id }, at: Date.now() };
1136
1103
  },
1137
1104
  release: async (id) => {
1138
- const r = o.spawn("rustright", ["quiet", "release", String(id)]);
1105
+ const r = clientCall(linuxRoute ? linuxRoute.client : (which("rustright") ?? "rustright"), ["quiet", "release", String(id)]);
1139
1106
  if (failed(r)) o.out(`${tag} note: broker quiet lease ${id} release failed (${(why(r) || `exit ${r?.status}`).slice(0, 200)}); it expires on its own`);
1140
1107
  else o.out(`${tag} broker quiet lease ${id} released`);
1141
1108
  },
@@ -1239,19 +1206,106 @@ async function installHook(o, root) {
1239
1206
  * apps/coderight-tauri), else the repository root. Every right-release stage runs there, the sealed set is read from there, and the
1240
1207
  * receipt is keyed to it (receiptKey). Returns { appRoot, appDir } with appDir relative to the repository root, "." at the root.
1241
1208
  */
1242
- export function appRootOf({ root, cwd, exists = fs.existsSync }) {
1209
+ export function appRootOf({ root, cwd, exists = fs.existsSync, configDirs = null }) {
1243
1210
  const inside = cwd && (cwd === root || cwd.startsWith(root + path.sep));
1244
- const hasConfig = (d) => ["right-release.config.json", "right-release.config.mjs"].some((f) => exists(path.join(d, f)));
1211
+ // With `configDirs` (the directories whose committed tree holds a release config: `committedConfigDirs`) the scope is the SELECTED COMMIT's, read from
1212
+ // its tree alone; the checkout's files are never asked, so a newer, dirty or edited checkout cannot move an older commit's app. Without it (callers that
1213
+ // hold no commit) the live checkout answers, as before. Either way the directory the gate was run from is the explicit identity.
1214
+ const hasConfig = configDirs
1215
+ ? (d) => configDirs.has(path.relative(root, d).split(path.sep).join("/") || ".")
1216
+ : (d) => ["right-release.config.json", "right-release.config.mjs"].some((f) => exists(path.join(d, f)));
1245
1217
  // The nearest directory at or above cwd (below the root) that carries a release config is the app; a deeper cwd (apps/foo/src) is apps/foo.
1246
1218
  let appRoot = root;
1247
- for (let d = path.resolve(cwd); d.startsWith(root) && d !== root; d = path.dirname(d)) { if (hasConfig(d)) { appRoot = d; break; } }
1219
+ if (inside) for (let d = path.resolve(cwd); d.startsWith(root) && d !== root; d = path.dirname(d)) { if (hasConfig(d)) { appRoot = d; break; } }
1248
1220
  return { appRoot, appDir: path.relative(root, appRoot).split(path.sep).join("/") || "." };
1249
1221
  }
1250
1222
 
1223
+ /**
1224
+ * The directories (posix, relative to the repository root, "." for the root) that hold a release config in `commit`'s tree: `{ dirs }`, or `{ refusal }` when the
1225
+ * tree cannot be listed. Read with `git ls-tree` of the selected commit, so the answer is the commit's, whatever the checkout holds.
1226
+ */
1227
+ export function committedConfigDirs({ git, commit }) {
1228
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
1229
+ if (failed(listed)) return { refusal: `cannot list the files of ${String(commit).slice(0, 12)}: ${why(listed)}` };
1230
+ const dirs = new Set(text(listed).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).map((f) => path.posix.dirname(f)));
1231
+ return { dirs };
1232
+ }
1233
+
1234
+ /** True when `commit` is RightKit's own repository: its tree holds packages/release/package.json named @rightkit/release (the identity `check` uses for RightKit's own rules). Read from the commit, never the checkout. */
1235
+ export function rightkitIdentityAt(git, commit) {
1236
+ const shown = git("show", `${commit}:${RIGHTKIT_IDENTITY_FILE}`);
1237
+ return !failed(shown) && isRightKitIdentity(text(shown));
1238
+ }
1239
+
1240
+ const UNIT_SELECTOR = /^(?:npm:@rightkit\/[a-z0-9-]+|cargo:rightkit-[a-z0-9-]+)$/;
1241
+
1242
+ /**
1243
+ * The unit list of `--units-file <file>`: JSON `{ "units": [ "cargo:rightkit-x", "npm:@rightkit/y", ... ] }` written for this run (not a
1244
+ * committed file: Adrian, 2026-10-31: "a JSON file that is created at that time"). Every entry is a `--unit` selector, once. Throws a usage error.
1245
+ */
1246
+ export function readUnitList(readFile, file) {
1247
+ const usage = (what) => Object.assign(new Error(`RK_UNITS_FILE: ${file} ${what}`), { code: "RK_UNITS_FILE" });
1248
+ let parsed;
1249
+ try { parsed = JSON.parse(readFile(file)); } catch (e) { throw usage(`cannot be read as JSON (${e.message})`); }
1250
+ const units = parsed?.units;
1251
+ if (!Array.isArray(units) || !units.length || !units.every((u) => typeof u === "string" && UNIT_SELECTOR.test(u))) throw usage('must be { "units": [ "cargo:rightkit-<name>" | "npm:@rightkit/<name>", ... ] } with at least one unit');
1252
+ const duplicate = units.find((u, i) => units.indexOf(u) !== i);
1253
+ if (duplicate !== undefined) throw usage(`names ${duplicate} more than once`);
1254
+ return units;
1255
+ }
1256
+
1257
+ /**
1258
+ * RightKit names its unit on every run (#1325, Adrian 2026-10-31: "the unit has to be mentioned. Okay, so either it is one specific unit, or it is
1259
+ * a JSON file that is created at that time, or all with my exception"). A door run with no `--unit` is accepted in RightKit only with
1260
+ * `--units-file` (each listed unit runs as its own unit run, in order, on the one selected commit, stopping at the first that fails) or
1261
+ * `--all-units-exception <id>` (the whole-repository run, by Adrian's entry in the commit's ci/all-units-exceptions.json). Both flags name RightKit
1262
+ * units, so a repository that is not RightKit refuses them. Returns `{ exit }` when the run ends here, or `{}` to go on as a whole door run (the
1263
+ * all-units flag and its id are removed from `args`). An unresolvable commit is runStages' own refusal, so it goes on.
1264
+ */
1265
+ export async function rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList }) {
1266
+ const tag = "rightkit-gate:";
1267
+ const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
1268
+ let pinned;
1269
+ try { pinned = (await import("../snapshot.mjs")).resolveCommit(root, option("--commit") ?? "HEAD"); } catch { return {}; }
1270
+ const inRightKit = rightkitIdentityAt(git, pinned.commit);
1271
+ if (!inRightKit) {
1272
+ if (!unitsFileFlag && !allUnitsFlag) return {};
1273
+ o.err(`${tag} BLOCKED: ${unitsFileFlag ? "--units-file" : "--all-units-exception"} names RightKit units and ${pinned.commit.slice(0, 12)} is not RightKit; every other repository is built whole from the ${CARGO_WORKSPACE_LIST_KEY} list committed in ${CARGO_WORKSPACE_LIST_FILE}, with no unit named; nothing ran`);
1274
+ return { exit: 1 };
1275
+ }
1276
+ if (unitsFileFlag) {
1277
+ const stage = option("--to");
1278
+ const given = args.filter((a, i) => a.startsWith("--") && args[i - 1] !== "--units-file" && a !== "--units-file");
1279
+ if (given.some((a) => a !== "--to" && a !== "--commit")) { o.err(`${tag} a unit list run takes only --to, --units-file and optional --commit (a receipt is written per unit run: run the unit alone with --receipt-out)`); return { exit: 2 }; }
1280
+ for (const [i, selector] of unitList.entries()) {
1281
+ o.out(`${tag} unit list ${i + 1}/${unitList.length}: ${selector} on commit ${pinned.commit.slice(0, 12)}`);
1282
+ const code = await main(["--to", stage, "--unit", selector, "--commit", pinned.commit], io);
1283
+ if (code !== 0) { o.err(`${tag} BLOCKED: unit ${selector} ended with exit ${code}; ${unitList.length - i - 1} unit(s) of the list did not run`); return { exit: code }; }
1284
+ }
1285
+ return { exit: 0 };
1286
+ }
1287
+ if (allUnitsFlag) {
1288
+ const id = option("--all-units-exception");
1289
+ let granted;
1290
+ try {
1291
+ const shown = git("show", `${pinned.commit}:${ALL_UNITS_EXCEPTIONS_FILE}`);
1292
+ granted = authorizeAllUnits({ id, record: failed(shown) ? null : text(shown), now: o.now ? o.now() : Date.now() });
1293
+ } catch (e) {
1294
+ if (!e.code?.startsWith("RK_ALL_UNITS_")) throw e;
1295
+ o.err(`${tag} BLOCKED: ${e.message}; nothing ran`);
1296
+ return { exit: e.code === ALL_UNITS_ERRORS.usage ? 2 : 1 };
1297
+ }
1298
+ args.splice(args.indexOf("--all-units-exception"), 2);
1299
+ o.out(`${tag} ALL UNITS EXCEPTION ${describeAllUnitsException(granted)}`);
1300
+ return {};
1301
+ }
1302
+ o.err(`${tag} BLOCKED: RK_UNIT_NOT_NAMED: RightKit names its unit on every run: --unit <cargo:rightkit-name|npm:@rightkit/name>, --units-file <json written for this run>, or --all-units-exception <id> (Adrian's entry in ${ALL_UNITS_EXCEPTIONS_FILE}); a whole-repository run is never the default; nothing ran`);
1303
+ return { exit: 1 };
1304
+ }
1305
+
1251
1306
  async function runStages(o, args, root, git, io) {
1252
1307
  const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
1253
1308
  const tag = "rightkit-gate:";
1254
- const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, exists: o.exists });
1255
1309
  const to = option("--to");
1256
1310
  // dev and test are door stages like the rest: the same commit-only route (select a commit, sync its snapshot under ownership, class A
1257
1311
  // of the snapshot), then right-release dev|test with the snapshot's app root as its cwd. They have no stage list of their own.
@@ -1272,25 +1326,51 @@ async function runStages(o, args, root, git, io) {
1272
1326
  if (mobile && to === "qualify") { o.err(`${tag} --to qualify has no stage for ${platform}: there is no installed-QA lane for a mobile build; qualification is the store's test track after --to release. Run --to package or --to release.`); return 2; }
1273
1327
  if (mobile) stages = stages.filter((s) => s !== "qualify");
1274
1328
  if (stages.includes("release") && !mobile && !["patch", "update"].includes(tier)) { o.err(`${tag} --to release needs --tier patch|update (chosen explicitly, never defaulted)`); return 2; }
1329
+ // The commit this run is for is selected FIRST (--commit <rev>, default HEAD; the door snapshot's commit inside a door run; HEAD on hosted CI, whose runner
1330
+ // checkout is the commit), and the app scope is read from THAT commit's tree (committedConfigDirs): the checkout's present files never decide it.
1331
+ const hosted = o.env.GITHUB_ACTIONS === "true";
1332
+ let selected = null;
1333
+ if (!o.doorSnapshot && !hosted) {
1334
+ const snapModule = await import("../snapshot.mjs");
1335
+ try { selected = snapModule.resolveCommit(root, option("--commit") ?? "HEAD"); }
1336
+ catch (e) {
1337
+ if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir: "." }); // the probe names the same refusal in its own words
1338
+ o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2;
1339
+ }
1340
+ }
1341
+ const scopeCommit = o.doorSnapshot ? o.doorSnapshot.commit : selected ? selected.commit : text(git("rev-parse", "HEAD")).trim();
1342
+ const inventory = committedConfigDirs({ git, commit: scopeCommit });
1343
+ if (inventory.refusal) { o.err(`${tag} cannot read the app configuration of the selected commit: ${inventory.refusal}`); return 2; }
1344
+ const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
1345
+ // `--route`: which route this stage takes here and whether it refuses, answered without running anything (rightkit#1297).
1346
+ if (o.routeProbe) return await runRouteProbe(o, args, root, git, { to, platform, stages: devTest ? [] : stages, devTest, appDir });
1275
1347
  // Commit-only build (GATE-CONTRACT §1): on every host (not hosted CI, whose runner checkout is the commit) the door selects a commit
1276
1348
  // (--commit <rev>, default HEAD), takes the host's build ownership, syncs the commit's gate-owned snapshot once, and every stage
1277
- // below reads that snapshot; the checkout may hold anything and is never a build source. Mac and Dell compile it through the
1278
- // RustRight snapshot carrier (`deps.snapshotBroker`, item 7 rows 7.20 to 7.24). Interim, until the one cutover commit (decision 2,
1279
- // "Interim"): Mac and Dell keep the checkout route exactly as it was (clean HEAD, today's package, qualify and `runWorktreeCompile`),
1280
- // and take the snapshot route ONLY when the carrier is wired (`deps.snapshotBroker`, today only in tests); no flag or environment
1281
- // variable selects it. Once it is selected any failure refuses; there is no retry on the checkout.
1282
- if (!o.doorSnapshot && o.env.GITHUB_ACTIONS !== "true" && snapshotRouteSelected(o)) {
1283
- // The door never sets TAURI_CONFIG or any externalBin override, and refuses to inherit one: Tauri merges it over the commit's own
1284
- // configuration (tauri-build lib.rs), which would build from a configuration that is not the commit's.
1285
- if (o.env.TAURI_CONFIG !== undefined) { o.err(`${tag} BLOCKED: RK_TAURI_CONFIG_OVERRIDE: TAURI_CONFIG is set in the environment; it would replace the commit's Tauri configuration (externalBin included) for every build below, so a commit-only build refuses it; nothing ran`); return 1; }
1286
- // `--to check` compiles nothing (class A of the snapshot: its Cargo scans go through the broker on their own), so it needs no carrier.
1287
- const refused = to === "check" ? null : snapshotRouteRefusal(o, { rightRelease: devTest });
1288
- if (refused) { o.err(`${tag} BLOCKED: ${refused.code}: ${refused.message}; nothing ran and the checkout is never built`); return 1; }
1349
+ // below reads that snapshot; the checkout may hold anything and is never a build source. Every non-hosted host takes this route: Mac
1350
+ // and Dell compile it through the RustRight snapshot carrier (`deps.snapshotBroker`, wired by `productionEntryDeps`; item 7 rows 7.20
1351
+ // to 7.24, rightkit#1301), right-release's own children included. Once it is selected any failure refuses; there is no retry on the
1352
+ // checkout, and no flag or environment variable selects another route.
1353
+ // The route and every refusal the door can meet before it takes ownership are `routePlan`'s, the one function the `--route` probe
1354
+ // also answers from (rightkit#1297 L1): the probe and the real run cannot drift, and the door-route parity test compares the two.
1355
+ if (!o.doorSnapshot) {
1356
+ const whichRoute = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1357
+ const routeExecutor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which: whichRoute, readFile: o.readFile });
1358
+ let decided;
1359
+ try { decided = routePlan(o, { to, stages: devTest ? [] : stages, devTest, executor: routeExecutor, stagePlatform: platform, capabilities: await brokerCapabilityStateWithHandshake(o, { to, executor: routeExecutor }), appConfig: selected && to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
1360
+ catch (e) { o.err(`${tag} ${e.message}`); return 2; }
1361
+ if (decided.refusal) { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran${decided.route === "snapshot" ? " and the checkout is never built" : ""}`); return 1; }
1362
+ const notPublisher = decided.route === "snapshot" ? releaseRouteRefusal(o, { stages, platform }) : null;
1363
+ if (notPublisher) { o.err(`${tag} BLOCKED: ${notPublisher.code}: ${notPublisher.message}; nothing ran and the checkout is never built`); return 1; }
1364
+ // The declared Linux broker admits snapshots only under its own data root: refused before any ownership or sync when the gate's root is elsewhere.
1365
+ if (decided.route === "snapshot" && !hosted) {
1366
+ const snapSync = await import("../snapshot.mjs");
1367
+ const mismatch = await declaredSnapshotRootRefusal(o, routeExecutor, o.snapshotRoot ?? snapSync.snapshotRoot({ platform: o.platform, env: o.env }));
1368
+ if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; }
1369
+ }
1370
+ }
1371
+ if (!o.doorSnapshot && !hosted) {
1289
1372
  const snap = await import("../snapshot.mjs");
1290
1373
  const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
1291
- let selected;
1292
- try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
1293
- catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
1294
1374
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
1295
1375
  let own;
1296
1376
  try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:${to}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit: selected.commit, platform: o.platform, env: o.env }); }
@@ -1308,12 +1388,7 @@ async function runStages(o, args, root, git, io) {
1308
1388
  if (r?.released === false && !own.joined) o.err(`${tag} build ownership ${own.handle} kept: ${r.why}`);
1309
1389
  }
1310
1390
  }
1311
- if (devTest) {
1312
- // Mac and Dell, until the cutover: the checkout route as it was (class A of the working tree, then right-release on it). Hosted CI's
1313
- // runner checkout is the commit, so it takes the commit route below without a snapshot.
1314
- if (!o.doorSnapshot && o.env.GITHUB_ACTIONS !== "true") return await runWorktreeCompile(o, args, root, git, io, to, appRoot);
1315
- return await runDevTest(o, args, root, git, io, to, appRoot, appDir);
1316
- }
1391
+ if (devTest) return await runDevTest(o, args, root, git, io, to, appRoot, appDir);
1317
1392
  const ds = o.doorSnapshot ?? null;
1318
1393
  const commit = ds ? ds.commit : text(git("rev-parse", "HEAD")).trim();
1319
1394
  const tree = ds ? ds.tree : text(git("rev-parse", "HEAD^{tree}")).trim();
@@ -1334,8 +1409,8 @@ async function runStages(o, args, root, git, io) {
1334
1409
  const shown = git("show", `${commit}:${f}`); if (!failed(shown)) lockFiles[f] = text(shown);
1335
1410
  }
1336
1411
  const binding = bindingOf({
1337
- tree, lockFiles, gateFile: fileURLToPath(import.meta.url), executor: executor.kind,
1338
- toolchain: { classA: { "cargo-deny": version("cargo-deny", ["--version"]), "cargo-audit": version("cargo-audit", ["--version"]), actionlint: version("actionlint", ["-version"]), shellcheck: version("shellcheck", ["--version"]) }, node: process.version, rustc: version(executor.kind === "broker" && executor.cargo[0] === "rustright" ? "rustright" : "rustc", executor.kind === "broker" && executor.cargo[0] === "rustright" ? ["rustc", "-V"] : ["-V"]), pnpm: version("pnpm", ["--version"]) },
1412
+ tree, lockFiles, gateFile: fileURLToPath(import.meta.url), executor: o.brokerException ? `${executor.kind}:exception:${o.brokerException.exceptionId}` : executor.kind,
1413
+ toolchain: { classA: { "cargo-deny": version("cargo-deny", ["--version"]), "cargo-audit": version("cargo-audit", ["--version"]), actionlint: version("actionlint", ["-version"]), shellcheck: version("shellcheck", ["--version"]) }, node: process.version, rustc: version(executor.kind === "broker" && executor.cargo.length > 1 ? executor.cargo[0] : "rustc", executor.kind === "broker" && executor.cargo.length > 1 ? ["rustc", "-V"] : ["-V"]), pnpm: version("pnpm", ["--version"]) },
1339
1414
  });
1340
1415
  // Release writes to R2. The write credential lives only in the trusted publisher: a GitHub Actions job in the protected
1341
1416
  // publication environment, which sets RIGHTKIT_PUBLISHER=github-environment. Hosts and agents never hold it, so the door refuses the
@@ -1351,6 +1426,7 @@ async function runStages(o, args, root, git, io) {
1351
1426
  if (receiptIn !== undefined) {
1352
1427
  let imported = null;
1353
1428
  try { imported = JSON.parse(o.readFile(path.resolve(o.cwd, receiptIn))); } catch { /* refused below */ }
1429
+ if (imported?.brokerException) { o.err(`${tag} BLOCKED: --receipt-in ${receiptIn} was built outside the broker by exception ${imported.brokerException.exceptionId}; it is never taken as a broker-built receipt`); return 1; }
1354
1430
  if (!imported || imported.schema !== RECEIPT_SCHEMA || imported.commit !== commit || !sameSource(imported.binding, binding)) {
1355
1431
  o.err(`${tag} BLOCKED: --receipt-in ${receiptIn} is not a ${RECEIPT_SCHEMA} receipt for ${commit.slice(0, 12)} with this tree, these lockfiles and this gate`);
1356
1432
  return 1;
@@ -1363,6 +1439,8 @@ async function runStages(o, args, root, git, io) {
1363
1439
  const receipt = previous && sameBinding(previous.binding, binding) ? previous
1364
1440
  : previous && sameSource(previous.binding, binding) ? { ...previous, binding, stages: Object.fromEntries(Object.entries(previous.stages ?? {}).filter(([k]) => k !== "check" && k !== "compile")) }
1365
1441
  : { schema: RECEIPT_SCHEMA, commit, app: appDir, binding, stages: {} };
1442
+ // Every run under a broker exception records it in the receipt (broker-exception.mjs): id, Adrian's quote, issue, units and host.
1443
+ if (o.brokerException) receipt.brokerException = o.brokerException;
1366
1444
  const receiptOut = option("--receipt-out");
1367
1445
  const save = () => {
1368
1446
  (o.writeReceipt ?? writeReceipt)({ commonDir, receipt });
@@ -1374,25 +1452,35 @@ async function runStages(o, args, root, git, io) {
1374
1452
  // the door run records the split, so the child can prove which snapshot it may read and which checkout holds its outputs
1375
1453
  // (source-split.mjs). Anywhere else it runs in the app root of the checkout, which hosted CI's runner holds at the commit.
1376
1454
  const sourceOf = ds ? sourceSplitOf({ root, ds, appDir }) : null;
1377
- const rightRelease = (stage, cliArgs, extraEnv = {}) => {
1455
+ const rightRelease = async (stage, cliArgs, extraEnv = {}) => {
1456
+ // A broker host's child reaches the broker only through this stage's carrier (carried-cargo.mjs): its Cargo calls, and its pnpm, node and QA
1457
+ // steps as adapter jobs with the third-party children's compiles nested under them. Every call is in the gate-owned ledger and is verified
1458
+ // again after the child ends.
1459
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
1460
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing ran`); return false; }
1378
1461
  const run = startRun({ commonDir, commit, tree, stage, ...(sourceOf ? { source: sourceOf } : {}) });
1462
+ let childOk = false;
1379
1463
  try {
1380
1464
  o.out(`${tag} [${stage}] right-release ${cliArgs.join(" ")}${sourceOf ? ` (from the snapshot ${ds.path})` : ""}`);
1381
- const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, ...cliArgs], { cwd: sourceOf ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: { ...o.env, ...run.env, ...extraEnv } });
1382
- return r.status === 0;
1465
+ const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, ...cliArgs], { cwd: sourceOf ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: carriedEnv(carrier, { ...o.env, ...run.env }, extraEnv) });
1466
+ childOk = r.status === 0;
1383
1467
  } finally { endRun(run); }
1468
+ const carriedOk = await carrier.finish({ stage });
1469
+ return childOk && carriedOk;
1384
1470
  };
1385
1471
 
1386
1472
  o.out(`${tag} --to ${to} for ${commit.slice(0, 12)}${appDir === "." ? "" : ` (app ${appDir})`}: ${stages.join(" -> ")}; executor ${executor.kind}`);
1387
1473
  // A later stage on the sealed bytes: check, compile and package already passed for them (in this receipt) and the bytes on disk are
1388
1474
  // the ones they sealed, so they are not run again.
1389
1475
  // The release configs of the commit (under this app) that declare `sidecars`: the commit's config decides on the snapshot route, never the checkout's.
1390
- const sidecarConfigsOf = () => {
1391
- const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
1392
- return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
1393
- .filter((f) => { try { return /\bsidecars\b/.test(o.readFile(path.join(ds ? ds.path : root, f))); } catch { return false; } });
1394
- };
1476
+ const sidecarConfigsOf = () => committedSidecarConfigs({ o, git, commit, appDir, base: ds ? ds.path : root });
1395
1477
  const sealedNow = receipt.stages.package?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1478
+ // The protected-receipt verifier pulls in the ownership policy modules (and their dependencies): loaded only for a run that qualifies or releases,
1479
+ // so an installed copy that only checks or compiles never needs them (GB-14).
1480
+ const qualifyReuse = stages.includes("qualify") || stages.includes("release") ? await import("../qualify-reuse.mjs") : null;
1481
+ const installedQaReceiptDigest = (...a) => qualifyReuse.installedQaReceiptDigest(...a);
1482
+ const qualifyReuseProblem = (...a) => qualifyReuse.qualifyReuseProblem(...a);
1483
+ const qualifyRecordProblem = (recorded) => (o.qualifyReuseProblem ?? qualifyReuseProblem)({ recorded, sealedNow, repoRoot: appRoot, platform, ...(o.verifyQa !== undefined ? { verify: o.verifyQa } : (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment" ? { verify: null } : {})) });
1396
1484
  const sealedIntact = Boolean(sealedNow && sealedNow.id === receipt.stages.package.sealed && sealedNow.digest === receipt.stages.package.manifestSha256);
1397
1485
  // Every deterministic scan (full class A) runs before any compile, for every door stage. The app rules run at the tier the stage
1398
1486
  // owes: dev, test and package take the compile-tier rules; qualify and release add the release-only rules (approvals, ratified
@@ -1400,16 +1488,36 @@ async function runStages(o, args, root, git, io) {
1400
1488
  // 2026-10-28 change to release tier everywhere was never his decision and blocked HeardRight's dev and test builds).
1401
1489
  const earlyTier = ["qualify", "release"].includes(to) ? "release" : "compile";
1402
1490
  const tierAtLeast = (have, need) => have === "release" || need === "compile";
1491
+ // GB-12 (GATE 36/44/86, E18): Class A admission runs at every stage boundary a door run crosses; an outer receipt boolean or an intact
1492
+ // sealed set never stands in for it. The deterministic scans reuse through their own complete result-cache key (never this receipt's
1493
+ // coarse binding, which names the entry file only), and the live scans (cargo-deny, cargo-audit, pnpm-audit, pnpm-lockfile,
1494
+ // advisory-exceptions) are never cached, so the day's advisories are re-asked on every reuse. The one exception is the trusted
1495
+ // publisher's upload (`--to release` on a desktop platform): it runs no build environment, only uploads bytes whose earlier stages
1496
+ // (Class A included) another job of the same pipeline recorded in the receipt it imported (--receipt-in).
1497
+ const publisherUploadOnly = to === "release" && !mobile;
1498
+ // GB-15: a nested app's Class A runs its executing scans for that app's dependency closure; an app at the repository root is the repository.
1499
+ const appScope = appDir === "." ? [] : ["--scope", appDir];
1403
1500
  for (const stage of stages) {
1404
- if (stage === "check" && receipt.stages.check?.ok && tierAtLeast(receipt.stages.check.ruleTier ?? "compile", earlyTier)) { o.out(`${tag} [check] already passed (${receipt.stages.check.ruleTier ?? "compile"} tier) for this exact input (commit, tree, lockfiles, gate, toolchain)`); continue; }
1405
- if (stage === "compile" && receipt.stages.compile?.ok) { o.out(`${tag} [${stage}] already passed for this exact input (commit, tree, lockfiles, gate, toolchain)`); continue; }
1406
- if ((stage === "check" || stage === "compile" || stage === "package") && sealedIntact) {
1501
+ if (stage === "check" && publisherUploadOnly && receipt.stages.check?.ok && tierAtLeast(receipt.stages.check.ruleTier ?? "compile", earlyTier)) { o.out(`${tag} [check] already passed (${receipt.stages.check.ruleTier ?? "compile"} tier) for this exact input (commit, tree, lockfiles, gate, toolchain)`); continue; }
1502
+ if (stage === "compile" && receipt.stages.compile?.ok) {
1503
+ // GB-13 (GATE E6/E7/E16): a recorded compile stands only while the outputs it recorded are still on disk and the compile environment is
1504
+ // the one it ran in; otherwise the compile runs again, warm (never a cleared target), unless the sealed bytes below carry it.
1505
+ const outputsProblem = (o.compileReuseProblem ?? compileReuseProblem)(receipt.stages.compile, { env: o.env });
1506
+ if (!outputsProblem) { o.out(`${tag} [${stage}] already passed for this exact input (commit, tree, lockfiles, gate, toolchain) and its outputs are intact`); continue; }
1507
+ if (!sealedIntact) o.out(`${tag} [${stage}] the recorded compile no longer stands: ${outputsProblem}; running it again (warm)`);
1508
+ }
1509
+ if (((stage === "check" && publisherUploadOnly) || stage === "compile" || stage === "package") && sealedIntact) {
1407
1510
  o.out(`${tag} [${stage}] already passed for the sealed set ${sealedNow.id} (manifest ${sealedNow.digest.slice(0, 12)})`); continue;
1408
1511
  }
1409
1512
  // The publisher (another runner, often another OS) cannot repeat installed QA; it takes the qualify result recorded for exactly
1410
1513
  // these sealed bytes. --to qualify itself always runs it.
1514
+ // GB-14: the record is reused only while the protected installed-QA receipt it names still verifies over the current state (the existing
1515
+ // verifier: policy bundle, floor, required checks, source, artifact hashes); the publisher, which holds no receipt key, is held to the
1516
+ // recorded receipt digest and the manifest digest, and the upload admission verifies the receipt itself.
1411
1517
  if (stage === "qualify" && to !== "qualify" && sealedIntact && receipt.stages.qualify?.ok && receipt.stages.qualify.manifestSha256 === sealedNow.digest) {
1412
- o.out(`${tag} [qualify] already passed for the sealed set ${sealedNow.id} (manifest ${sealedNow.digest.slice(0, 12)})`); continue;
1518
+ const problem = qualifyRecordProblem(receipt.stages.qualify);
1519
+ if (!problem) { o.out(`${tag} [qualify] already passed for the sealed set ${sealedNow.id} (manifest ${sealedNow.digest.slice(0, 12)}; protected QA receipt ${receipt.stages.qualify.qaReceiptDigest.slice(0, 12)} verified)`); continue; }
1520
+ o.out(`${tag} [qualify] the recorded result no longer stands: ${problem}; it runs again`);
1413
1521
  }
1414
1522
  // The publisher only uploads. It never checks out a build environment, signs or runs installed QA, so every earlier stage must
1415
1523
  // already stand in its receipt for exactly the sealed bytes on disk; anything else is refused, never rebuilt or requalified here.
@@ -1421,7 +1529,7 @@ async function runStages(o, args, root, git, io) {
1421
1529
  if (ds && !(await openSnapshotStage({ o, ds, root, stage, tag }))) { receipt.stages[stage] = { ok: false, at: new Date().toISOString(), snapshot: { path: ds.path, tree: ds.tree, intactAfter: false }, ownership: ds.own.handle }; save(); o.err(`${tag} --to ${to} stopped before ${stage}: the snapshot is not the commit's bytes plus the ledger; nothing ran`); return 2; }
1422
1530
  let ok = false; const at = new Date().toISOString(); let extra = {}; let directReceipt = null;
1423
1531
  if (stage === "check") {
1424
- ok = (await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", earlyTier], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))(earlyTier)) === 0;
1532
+ ok = (await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", earlyTier], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))(earlyTier)) === 0;
1425
1533
  extra = { ruleTier: earlyTier };
1426
1534
  }
1427
1535
  else if (stage === "compile") {
@@ -1431,15 +1539,17 @@ async function runStages(o, args, root, git, io) {
1431
1539
  ok = true;
1432
1540
  // On the snapshot route the sidecars are staged from the snapshot's own config and sources through right-release's source split
1433
1541
  // (the staged files and receipt land beside the sources the compile reads; the caches and build trees stay the checkout's), before
1434
- // `main --all` compiles. A broker host reaches Cargo here only through the snapshot carrier, which right-release's children do not
1435
- // have yet (item 7 row 7.24): it is refused with a code, never staged from the checkout.
1436
- if (ds && sidecarConfigs.length && executor.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: [compile] ${sidecarConfigs.join(", ")} stage sidecars through right-release, whose Cargo calls reach the RustRight broker only through the snapshot carrier (item 7 row 7.24), which is not wired; nothing compiled`); ok = false; }
1542
+ // `main --all` compiles. A broker host's stage-sidecar child reaches Cargo through the stage's carrier (carried-cargo.mjs).
1437
1543
  // right-release runs in appRoot (nested apps), so the repository-relative config is handed over absolute (Astra, second pass).
1438
1544
  // On the snapshot route sidecars are staged ONCE per door run by the ledger bookkeeping (attribution diff, immutable staged entries).
1439
- if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage });
1440
- else for (const config of ok ? sidecarConfigs : []) if (!rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
1441
- if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; } } : {}) })))()) === 0;
1545
+ if (ok && ds) ok = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli, commonDir, commit, tree, stage, executor });
1546
+ else for (const config of ok ? sidecarConfigs : []) if (!await rightRelease(stage, ["stage-sidecar", "--config", path.join(root, config)])) { ok = false; break; }
1547
+ let compiledDirs = [];
1548
+ const reportOutputs = (dirs) => { compiledDirs = [...dirs]; };
1549
+ if (ok) ok = (await (o.runCompile ?? (() => main(ds ? ["--all", "--commit", commit, "--snapshot", ds.path] : ["--all"], { ...io, cwd: root, readStdin: () => "", ...(ds ? { buildOwnership: ds.own, snapshotLedger: ds.ledger?.entries ?? [], onCompileLedger: (entries) => { ds.compileAdmitted = entries; }, onDirectReceipt: (r) => { directReceipt = r; }, onCompileOutputs: reportOutputs } : {}) })))({ reportOutputs })) === 0;
1442
1550
  if (directReceipt) extra = { ...extra, direct: directReceipt };
1551
+ // What this compile produced, recorded with its result so a later run can prove the outputs are still there (GB-13).
1552
+ if (ok) extra = { ...extra, outputs: compileOutputsRecord({ dirs: compiledDirs, env: o.env }) };
1443
1553
  // The cross-check with `.rightkit/compile-inputs.json`: a declared input that covers ledger entries must carry the digest of those
1444
1554
  // very bytes in the compile receipt (the broker digests declared inputs in place in the snapshot, BC:39).
1445
1555
  if (ok && ds && directReceipt?.declaredInputDigests) {
@@ -1454,29 +1564,61 @@ async function runStages(o, args, root, git, io) {
1454
1564
  // compile was already passed (a separately invoked `--to package` re-stages after the sync, so it never meets a missing externalBin).
1455
1565
  // right-release then takes its identity from the selected commit and the verified snapshot (release-source-identity.mjs, row 7.04),
1456
1566
  // never the checkout's HEAD, status or branch; its outputs, state and the sealed directory stay in the checkout's `.right-release`.
1457
- ok = ds ? await stageSidecarsOnce({ o, ds, root, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor }) : true;
1567
+ ok = ds ? await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigsOf(), appDir, cli, commonDir, commit, tree, stage, executor }) : true;
1458
1568
  // Source QA (right-release qa --stage source) exists for mac and win only; a mobile package is the signed build alone.
1459
1569
  // Row 7.04c (R-T1, 7.09): the snapshot holds no Cargo target. On a direct host the checkout's output directory is resolved here, in the primary
1460
1570
  // checkout, and handed to right-release absolute: it binds every manifest's target for this stage, and the sealed manifest must record it back
1461
1571
  // (the door never trusts the child's value alone). One workspace is pinned as a whole; several are refused, naming them. A broker host's target
1462
- // is the carrier's (rows 7.20 to 7.24), which this gate does not call yet.
1572
+ // is the carrier's (rows 7.20 to 7.24): the carried calls' signed receipts name it (carried-cargo.mjs), and no CARGO_TARGET_DIR is passed.
1463
1573
  let packagePin = { dir: null };
1464
1574
  if (ok && ds && executor.kind !== "broker") {
1465
- packagePin = cargoOutputPin({ o, git, root, commit, executor });
1575
+ packagePin = await cargoOutputPinForScope({ o, git, root, commit, executor, snapshot: ds.path, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1466
1576
  if (packagePin.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packagePin.refusal}; nothing was built`); ok = false; }
1467
1577
  else if (packagePin.dir) o.out(`${tag} [package] output directory for the Cargo workspace: ${packagePin.dir} (the checkout's)`);
1468
1578
  }
1469
- const packageEnv = packagePin.dir ? { CARGO_TARGET_DIR: packagePin.dir } : {};
1470
- ok = ok && (mobile || rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], packageEnv)) && rightRelease(stage, ["build", "--platform", platform], packageEnv);
1579
+ // GB-17: several Cargo workspaces: each of right-release's and its children's Cargo calls is bound to ITS workspace's target through the per-call wrapper.
1580
+ let packageMulti = null;
1581
+ if (ok && packagePin.dirs?.length > 1) {
1582
+ packageMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
1583
+ if (packageMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: [package] ${packageMulti.refusal}; nothing was built`); ok = false; }
1584
+ else if (packageMulti.note) o.out(`${tag} [package] ${packageMulti.note}`);
1585
+ }
1586
+ const packageEnv = packagePin.dir ? { CARGO_TARGET_DIR: packagePin.dir } : (packageMulti?.env ?? {});
1587
+ ok = ok && (mobile || await rightRelease(stage, ["qa", "--stage", "source", "--platform", platform], packageEnv)) && await rightRelease(stage, ["build", "--platform", platform], packageEnv);
1471
1588
  const sealed = ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
1472
1589
  if (ok && !sealed) { o.err(`${tag} [package] the build reported success but sealed nothing for ${commit.slice(0, 8)} on ${platform}`); ok = false; }
1473
1590
  if (sealed) extra = { platform, sealed: sealed.id, manifestSha256: sealed.digest };
1474
1591
  // The cross-check: the Cargo targets the sealed manifest records are the directory this door resolved itself. A different one voids the stage.
1475
- if (ok && sealed && ds && packagePin.dir) {
1592
+ try { packageMulti?.cleanup?.(); } catch { /* the wrapper's map is the gate's own temp directory */ }
1593
+ if (ok && sealed && ds && (packagePin.dir || packagePin.dirs?.length > 1)) {
1594
+ let recorded = null;
1595
+ try { recorded = JSON.parse(String(o.readFile(sealed.manifest))).boundTargets ?? null; } catch { recorded = null; }
1596
+ const { crossCheckBoundTargets, SPLIT_TARGET_DRIFT_CODE } = await import("../release-artifact-path.mjs");
1597
+ const drift = crossCheckBoundTargets({ recorded, expected: packagePin.dirs });
1598
+ if (drift.length) {
1599
+ for (const problem of drift.slice(0, 10)) o.err(`${tag} ${SPLIT_TARGET_DRIFT_CODE}: [package] ${problem}`);
1600
+ const closedDrift = await closeSnapshotStage({ o, ds, root, stage, step: stage, tag });
1601
+ receipt.stages[stage] = { ok: false, at, voided: SPLIT_TARGET_DRIFT_CODE, ...extra, snapshot: { path: ds.path, tree: ds.tree, intactAfter: closedDrift.ok }, ownership: ds.own.handle };
1602
+ save();
1603
+ o.err(`${tag} --to ${to} stopped at ${stage}: ${SPLIT_TARGET_DRIFT_CODE}; the sealed set is void and nothing after it ran`);
1604
+ return 2;
1605
+ }
1606
+ }
1607
+ // A broker host's Cargo target is the carrier's: every target the sealed manifest records is a directory a verified, signed receipt of this
1608
+ // stage named (`outputDir`), never one the child alone reported.
1609
+ if (ok && sealed && ds && executor.kind === "broker") {
1476
1610
  let recorded = null;
1477
1611
  try { recorded = JSON.parse(String(o.readFile(sealed.manifest))).boundTargets ?? null; } catch { recorded = null; }
1478
1612
  const { crossCheckBoundTargets, SPLIT_TARGET_DRIFT_CODE } = await import("../release-artifact-path.mjs");
1479
- const drift = crossCheckBoundTargets({ recorded, expected: [packagePin.dir] });
1613
+ // The commit's workspaces decide whether a bound target is owed; the receipts compared are THIS package stage's own (rightkit#1301 L1).
1614
+ const owed = Object.keys(await snapshotCargoWorkspaces({ git, ds, root })).length > 0;
1615
+ const want = [...(ds.carriedByStage?.[stage] ?? [])];
1616
+ let drift = [];
1617
+ if (owed) {
1618
+ if (!Array.isArray(recorded) || !recorded.length) drift = ["the sealed manifest records no bound Cargo target; a broker host's package stage binds the output directory its carried build's receipt named"];
1619
+ else if (!want.length) drift = ["no verified receipt of this package stage names an output directory, so the recorded target cannot be checked"];
1620
+ else drift = crossCheckBoundTargets({ recorded, expected: want });
1621
+ }
1480
1622
  if (drift.length) {
1481
1623
  for (const problem of drift.slice(0, 10)) o.err(`${tag} ${SPLIT_TARGET_DRIFT_CODE}: [package] ${problem}`);
1482
1624
  const closedDrift = await closeSnapshotStage({ o, ds, root, stage, step: stage, tag });
@@ -1497,21 +1639,23 @@ async function runStages(o, args, root, git, io) {
1497
1639
  // store); the release tier is owed first, as for desktop qualification.
1498
1640
  const releaseTier = receipt.stages.check?.ok && receipt.stages.check.ruleTier === "release"
1499
1641
  ? (o.out(`${tag} [release] the release tier already passed at check for this exact input; not run again`), 0)
1500
- : await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1642
+ : await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1501
1643
  if (releaseTier !== 0) { o.err(`${tag} [release] the release tier (every rule and scan) is red for ${commit.slice(0, 12)}; nothing was uploaded`); ok = false; }
1502
- else ok = rightRelease(stage, ["upload", "--platform", platform, "--release", sealed.sealed, "--apply"]);
1644
+ else ok = await rightRelease(stage, ["upload", "--platform", platform, "--release", sealed.sealed, "--apply"]);
1503
1645
  extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, track: platform === "ios" ? "testflight" : "play-internal", ruleTier: "release" };
1504
1646
  } else if (stage === "qualify") {
1505
1647
  // Qualification owes the release tier: every rule and scan, the ones held out of compile admission included (app-check.mjs TIERS).
1506
1648
  const releaseTier = receipt.stages.check?.ok && receipt.stages.check.ruleTier === "release"
1507
1649
  ? (o.out(`${tag} [qualify] the release tier already passed at check for this exact input; not run again`), 0)
1508
- : await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1650
+ : await (o.runReleaseCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...appScope, "--rule-tier", "release"], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1509
1651
  if (releaseTier !== 0) { o.err(`${tag} [qualify] the release tier (every rule and scan) is red for ${commit.slice(0, 12)}; nothing was qualified`); ok = false; }
1510
- else ok = rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
1511
- extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release" };
1652
+ else ok = await rightRelease(stage, ["qa", "--stage", "installed", "--platform", platform, "--release", sealed.sealed]);
1653
+ // The protected installed-QA receipt this run produced is journaled by digest (GB-14), so the record can be re-verified before any reuse.
1654
+ extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, ruleTier: "release", ...(ok ? { qaReceiptDigest: (o.installedQaReceiptDigest ?? installedQaReceiptDigest)({ repoRoot: appRoot, releaseId: sealed.sealed, platform }) } : {}) };
1512
1655
  } else {
1513
1656
  if (!receipt.stages.qualify?.ok || receipt.stages.qualify.manifestSha256 !== sealed.manifestSha256) { o.err(`${tag} BLOCKED: [release] the sealed set was not qualified (installed QA) in this door run's receipt`); ok = false; }
1514
- else { ok = rightRelease(stage, ["upload", "--release", sealed.sealed, "--platform", platform, "--tier", tier]); extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, tier }; }
1657
+ else if (qualifyRecordProblem(receipt.stages.qualify)) { o.err(`${tag} BLOCKED: [release] the recorded qualification no longer stands: ${qualifyRecordProblem(receipt.stages.qualify)}; nothing was uploaded`); ok = false; }
1658
+ else { ok = await rightRelease(stage, ["upload", "--release", sealed.sealed, "--platform", platform, "--tier", tier]); extra = { sealed: sealed.sealed, manifestSha256: sealed.manifestSha256, tier }; }
1515
1659
  }
1516
1660
  }
1517
1661
  if (ds) {
@@ -1550,7 +1694,9 @@ function stageForPublisher(o, args, root, git) {
1550
1694
  const common = text(git("rev-parse", "--git-common-dir")).trim();
1551
1695
  if (!/^[0-9a-f]{40}$/.test(commit) || !common) { o.err(`${tag} cannot read HEAD and the git directory`); return 2; }
1552
1696
  const commonDir = path.resolve(root, common);
1553
- const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, exists: o.exists });
1697
+ const inventory = committedConfigDirs({ git, commit });
1698
+ if (inventory.refusal) { o.err(`${tag} cannot read the app configuration of ${commit.slice(0, 12)}: ${inventory.refusal}`); return 2; }
1699
+ const { appRoot, appDir } = appRootOf({ root, cwd: o.cwd, configDirs: inventory.dirs });
1554
1700
  const receipt = (o.readReceipt ?? readReceipt)({ commonDir, commit, app: appDir });
1555
1701
  const pack = receipt?.stages?.package; const qual = receipt?.stages?.qualify;
1556
1702
  const sealed = pack?.ok ? (o.sealedFor ?? sealedFor)({ root: appRoot, commit, platform }) : null;
@@ -1565,7 +1711,7 @@ function stageForPublisher(o, args, root, git) {
1565
1711
  const receiptFile = path.join(work, `receipt-${platform}.json`);
1566
1712
  fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
1567
1713
  const archive = path.join(work, `sealed-${platform}.tar.gz`);
1568
- const tar = o.spawn("tar", ["-czf", archive, "-C", appRoot, path.join(".right-release", "sealed", sealed.id, platform)]);
1714
+ const tar = o.spawn("tar", ["-czf", archive, "-C", appRoot, path.join(".right-release", "sealed", sealed.id, platformDirOf(platform))]);
1569
1715
  if (failed(tar)) { o.err(`${tag} could not pack ${sealed.id}/${platform}`); return 1; }
1570
1716
  const gh = (...a) => o.spawn("gh", a, { cwd: root });
1571
1717
  if (failed(gh("release", "view", releaseTag))) {
@@ -1663,30 +1809,41 @@ export async function closeSnapshotStage({ o, ds, root, stage, step = "stage", t
1663
1809
  return { ok: true, entries };
1664
1810
  }
1665
1811
 
1812
+ /** The release configs of `commit` (under `appDir`) that declare `sidecars`, read from `base` (the snapshot on the snapshot route): the commit's config decides, never the checkout's. */
1813
+ export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
1814
+ const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
1815
+ return (failed(listedConfigs) ? [] : text(listedConfigs).split("\0").filter((f) => /(^|\/)right-release\.config\.(json|mjs)$/.test(f)).filter((f) => appDir === "." || f.startsWith(`${appDir}/`)))
1816
+ .filter((f) => { try { return /\bsidecars\b/.test(o.readFile(path.join(base, f))); } catch { return false; } });
1817
+ }
1818
+
1666
1819
  /**
1667
1820
  * Sidecars are staged ONCE per door run, after the sync and before the first stage that builds the Tauri crate. The step is
1668
1821
  * right-release's own `stage-sidecar --json` run inside the snapshot (the split); this gate then walks the snapshot itself and the
1669
1822
  * step's additions must equal the files it reported plus its receipt (anything else is an unattributed write, RK_CHECK_ERROR). The
1670
1823
  * staged files become immutable `staged` ledger entries. Returns true when nothing is owed or staging passed.
1671
1824
  */
1672
- export async function stageSidecarsOnce({ o, ds, root, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
1825
+ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
1673
1826
  if (ds.sidecarsStaged) return true;
1674
1827
  ds.sidecarsStaged = true;
1675
1828
  if (!configs.length) return true;
1676
- if (executor?.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: [${stage}] ${configs.join(", ")} stage sidecars through right-release, whose Cargo calls reach the RustRight broker only through the snapshot carrier (item 7 row 7.24); nothing was staged`); return false; }
1677
1829
  const { attributionProblems, reportedPaths } = await import("../stage-ledger.mjs");
1678
1830
  const source = sourceSplitOf({ root, ds, appDir });
1679
1831
  const { TAURI_CONFIG: inherited, ...cleanEnv } = o.env; void inherited; // never set or pass one (refused at the door's start)
1680
1832
  for (const config of configs) {
1681
1833
  const before = ds.ledger?.entries ?? [];
1834
+ // The stage-sidecar child's Cargo calls (its builds and metadata queries) are carried on a broker host (carried-cargo.mjs).
1835
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
1836
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing was staged`); return false; }
1682
1837
  const run = startRun({ commonDir, commit, tree, stage, source });
1683
1838
  let r;
1684
1839
  try {
1685
1840
  o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
1686
- r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: { ...cleanEnv, ...run.env } });
1841
+ r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
1687
1842
  } finally { endRun(run); }
1843
+ const carriedOk = await carrier.finish({ stage });
1688
1844
  if (r?.stderr) o.err(String(r.stderr).trimEnd());
1689
1845
  if (!r || r.status !== 0) { o.err(`${tag} [${stage}] stage-sidecar for ${config} failed${r?.error ? ` (${r.error.message})` : ""}`); return false; }
1846
+ if (!carriedOk) return false;
1690
1847
  let result;
1691
1848
  try { result = JSON.parse(String(r.stdout ?? "").trim()); } catch { o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar --json for ${config} did not print its result; its writes cannot be attributed`); return false; }
1692
1849
  let walked;
@@ -1702,44 +1859,116 @@ export async function stageSidecarsOnce({ o, ds, root, configs, appDir, cli, com
1702
1859
  return true;
1703
1860
  }
1704
1861
 
1705
- /**
1706
- * The Cargo binding of a unit stage built from a snapshot (row 7.06a; decision 2 Q3, as for any npm unit): a unit's test or pack that
1707
- * reaches Cargo must never write `target/` into the snapshot. Every tracked Cargo.toml of the commit is asked, in the CHECKOUT, for its
1708
- * workspace and `target_directory`. Linux and any POSIX host with a direct executor put the runtime wrapper (cargo-target-shim.mjs)
1709
- * first on PATH and name it in CARGO, so each cargo call binds its own workspace's checkout target or is refused. A broker host
1710
- * (`o.snapshotBroker`) uses the same wrapper in broker mode, one carried request per call with its own receipt. A direct Windows host has
1711
- * no wrapper: one workspace is pinned by CARGO_TARGET_DIR, several are refused. Returns `{ env, note, cleanup }` or `{ refusal }`.
1712
- */
1713
- async function bindUnitCargo({ o, git, root, ds, executor, which }) {
1714
- const listed = git("ls-tree", "-r", "--name-only", "-z", ds.commit);
1715
- if (failed(listed)) return { refusal: `cannot list the files of ${ds.commit.slice(0, 12)}: ${why(listed)}` };
1716
- const manifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
1717
- if (!manifests.length) return { env: {}, note: null, cleanup() {} };
1862
+ /** Hosted CI scans its committed runner checkout. Raw-tree verification defeats filters or a stale index hiding edits. */
1863
+ async function verifyCommittedCheckout({ git, root, commit, tree, when }) {
1864
+ const head = git("rev-parse", "--verify", "HEAD^{commit}");
1865
+ const dirty = git("status", "--porcelain", "--untracked-files=all");
1866
+ if (failed(head) || text(head).trim() !== commit || failed(dirty) || text(dirty).trim()) throw new Error(`hosted CI checkout is not clean commit ${commit} ${when}`);
1867
+ const { commitOf } = await import("../snapshot-check.mjs");
1868
+ const identity = commitOf(root, commit);
1869
+ if (identity.commit !== commit || identity.tree !== tree) throw new Error(`hosted CI commit identity changed ${when}`);
1870
+ const { snapshotTree } = await import("../snapshot.mjs");
1871
+ const actual = snapshotTree({ checkout: root, dir: root, baseTree: identity.tree });
1872
+ if (actual !== tree) throw new Error(`hosted CI checkout is raw tree ${actual}, not tree ${tree} of commit ${commit} ${when}`);
1873
+ }
1874
+
1875
+ /** Select output-binding workspaces from committed source before any Cargo metadata process runs. */
1876
+ async function committedCargoManifests({ o, git, root, commit, scopeDirs }) {
1877
+ const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
1878
+ if (failed(listed)) throw new Error(`cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}`);
1879
+ const files = text(listed).split("\0").filter(Boolean).map(posix);
1880
+ const tracked = (name) => files.filter((f) => f === name || f.endsWith(`/${name}`));
1881
+ const allManifests = tracked("Cargo.toml");
1882
+ if (!scopeDirs?.length) return { manifests: allManifests, hasCargo: allManifests.length > 0 };
1883
+ const fileSet = new Set(files);
1884
+ const readCommitted = (file) => {
1885
+ if (!fileSet.has(file)) throw new Error(`RK_SCOPE_CLOSURE: ${file} is absent from commit ${commit}`);
1886
+ const r = git("show", `${commit}:${file}`);
1887
+ if (failed(r)) throw new Error(`RK_SCOPE_CLOSURE: cannot read ${file} of commit ${commit}: ${why(r)}`);
1888
+ return text(r);
1889
+ };
1890
+ const relative = (file) => posix(path.relative(root, file));
1891
+ const readFile = (file) => readCommitted(relative(file));
1892
+ const exists = (file) => { const rel = relative(file); return fileSet.has(rel) || files.some((f) => f.startsWith(`${rel}/`)); };
1893
+ let fixtures = [];
1894
+ if (fileSet.has(GATE_CONFIG)) {
1895
+ const config = JSON.parse(readCommitted(GATE_CONFIG));
1896
+ if (Array.isArray(config.fixtures)) fixtures = config.fixtures.filter((d) => typeof d === "string");
1897
+ }
1898
+ const tools = await import("../app-closure.mjs");
1899
+ const sourceMetadata = tools.cargoMetadataFromManifests({ manifests: tracked("Cargo.toml").filter((m) => !fixtures.some((d) => m.startsWith(`${d.replace(/\/$/, "")}/`))), readFile: readCommitted });
1900
+ const inventory = discoverRustWorkspaces({ root, tracked, fixtures, readFile, metadata: sourceMetadata });
1901
+ const npm = discoverNpmUnits({ root, tracked, readFile, exists });
1902
+ const closure = tools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
1903
+ const workspaces = [...new Set(inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir))].sort();
1904
+ o.out(`rightkit-gate: Cargo output scope ${scopeDirs.join(", ")}: ${workspaces.length} workspace(s) (${workspaces.join(", ") || "none"}); dependency directories ${closure.dependencyDirs.join(", ")}`);
1905
+ return { manifests: workspaces.map((dir) => path.posix.join(dir, "Cargo.toml")), hasCargo: allManifests.length > 0 };
1906
+ }
1907
+
1908
+ async function cargoOutputPinForScope(options) {
1909
+ try { return cargoOutputPin({ ...options, manifests: (await committedCargoManifests(options)).manifests }); }
1910
+ catch (error) { return { refusal: error.message }; }
1911
+ }
1912
+
1913
+ /** Bind only committed closure workspaces to checkout outputs; runtime Cargo outside that map refuses. */
1914
+ async function bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs }) {
1915
+ let selection;
1916
+ try { selection = await committedCargoManifests({ o, git, root, commit: ds.commit, scopeDirs }); }
1917
+ catch (error) { return { refusal: error.message }; }
1918
+ const { manifests, hasCargo } = selection;
1919
+ if (!hasCargo) return { env: {}, note: null, cleanup() {} };
1920
+ // A scoped npm stage with no declared Rust dependency still gets an empty runtime map: reaching undeclared Cargo refuses.
1718
1921
  let checkoutReal = root; try { checkoutReal = fs.realpathSync.native(root); } catch { /* as given */ }
1719
- const cargo = executor.cargo ?? ["cargo"];
1720
1922
  const targetDirs = {};
1721
- for (const m of manifests) {
1722
- const file = path.join(root, ...m.split("/"));
1723
- if (!o.exists(file)) return { refusal: `the checkout has no ${m}, so its Cargo output directory cannot be resolved` };
1724
- const r = o.spawn(cargo[0], [...cargo.slice(1), "metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", file], { cwd: path.dirname(file), env: o.env });
1725
- let doc = null;
1726
- try { doc = failed(r) ? null : JSON.parse(text(r)); } catch { doc = null; }
1727
- if (!doc || typeof doc.target_directory !== "string" || !path.isAbsolute(doc.target_directory) || typeof doc.workspace_root !== "string") return { refusal: `cargo metadata in the checkout reported no absolute target_directory for ${m}${failed(r) ? ` (${why(r)})` : ""}` };
1728
- const cargoDir = posix(path.relative(checkoutReal, (() => { try { return fs.realpathSync.native(doc.workspace_root); } catch { return doc.workspace_root; } })())) || ".";
1729
- targetDirs[cargoDir] = path.resolve(doc.target_directory);
1923
+ if (executor.kind === "broker") {
1924
+ // A broker host never asks Cargo anything about the checkout: the broker pins every output directory (the receipt's `outputDir`), so
1925
+ // all this binding needs is which directories of the commit are Cargo workspaces, and the commit's own manifests say so (read with the
1926
+ // object database, never the editable checkout: GATE-CONTRACT "Commit-only build"). The same rule the carried seam applies to a call
1927
+ // (cargo-target-shim.mjs locateWorkspaceByWalk): the nearest enclosing manifest that declares `[workspace]`, else the nearest manifest.
1928
+ const tracked = new Set(manifests);
1929
+ const bodies = new Map();
1930
+ const declaresWorkspace = (m) => {
1931
+ if (!bodies.has(m)) { const r = git("show", `${ds.commit}:${m}`); bodies.set(m, failed(r) ? null : text(r)); }
1932
+ return bodies.get(m);
1933
+ };
1934
+ for (const m of manifests) {
1935
+ let dir = path.posix.dirname(m); let nearest = null; let found = null;
1936
+ for (;;) {
1937
+ const candidate = dir === "." ? "Cargo.toml" : `${dir}/Cargo.toml`;
1938
+ if (tracked.has(candidate)) {
1939
+ nearest ??= dir;
1940
+ const body = declaresWorkspace(candidate);
1941
+ if (body === null) return { refusal: `cannot read ${candidate} of ${ds.commit.slice(0, 12)}` };
1942
+ if (/^\s*\[workspace(?:\.[^\]]*)?\]/m.test(body)) { found = dir; break; }
1943
+ }
1944
+ if (dir === ".") break;
1945
+ dir = path.posix.dirname(dir);
1946
+ }
1947
+ targetDirs[found ?? nearest ?? path.posix.dirname(m)] = null;
1948
+ }
1949
+ }
1950
+ // GB-16/GB-17: a direct host's workspaces and target directories are resolved from the SELECTED COMMIT's snapshot (its committed manifests and `.cargo/config.toml`) and mapped
1951
+ // lexically to the checkout's counterpart, exactly as cargoOutputPin does: the live checkout's manifests are never read, so a directory the newer checkout deleted or moved, or a dirty or
1952
+ // untracked manifest there, changes nothing.
1953
+ if (executor.kind !== "broker") {
1954
+ const resolved = cargoOutputPin({ o, git, root, commit: ds.commit, executor, snapshot: ds.path, manifests });
1955
+ if (resolved.refusal) return { refusal: resolved.refusal };
1956
+ const relOf = (w) => { for (const base of [root, checkoutReal]) { const r = path.relative(base, w); if (!r.startsWith("..") && !path.isAbsolute(r)) return posix(r) || "."; } return posix(w); };
1957
+ for (const [workspaceRoot, target] of resolved.workspaces ?? []) targetDirs[relOf(workspaceRoot)] = target;
1730
1958
  }
1731
1959
  const workspaces = Object.keys(targetDirs);
1732
1960
  if (o.platform === "win32" && executor.kind !== "broker") {
1961
+ if (workspaces.length === 0) return { refusal: "the selected closure has no Cargo workspace; a direct Windows host cannot wrap undeclared Cargo calls" };
1733
1962
  if (workspaces.length > 1) return { refusal: `the commit holds ${workspaces.length} Cargo workspaces (${workspaces.join(", ")}) and a direct Windows host has no per-call wrapper to bind each to its own target directory` };
1734
1963
  return { env: { CARGO_TARGET_DIR: Object.values(targetDirs)[0] }, note: `output directory for the Cargo workspace: ${Object.values(targetDirs)[0]} (the checkout's)`, cleanup() {} };
1735
1964
  }
1736
1965
  const shimModule = await import("../cargo-target-shim.mjs");
1737
1966
  let broker = null; let realCargo = null;
1738
1967
  if (executor.kind === "broker") {
1739
- if (!o.snapshotBroker) return { refusal: "a broker host reaches Cargo from a unit stage only through the snapshot carrier (deps.snapshotBroker), which is not wired" };
1740
- const sb = o.snapshotBroker;
1741
- const marked = sb.markBrokerSubmit?.();
1742
- if (marked?.broker?.submitted !== true || marked.handle !== sb.handle) return { refusal: `the build ownership did not record the broker submit${sb.handle ? ` under ${sb.handle}` : ""}` };
1968
+ if (!o.snapshotBroker) return { refusal: "a broker host reaches Cargo from a unit stage only through the snapshot carrier (deps.snapshotBroker), which this gate was started without" };
1969
+ let sb;
1970
+ try { sb = await carrierBroker({ o, root, ds, executor, which }); } catch (error) { return { refusal: `the snapshot carrier cannot be built (${error.code ?? "error"}: ${error.message})` }; }
1971
+ // Installing the carrier map sends nothing: the ownership records each carried request's sub-handle just before it is sent (snapshot-broker.mjs).
1743
1972
  const { brokerSection } = await import("../snapshot-broker.mjs");
1744
1973
  broker = { checkoutDirs: Object.fromEntries(workspaces.map((w) => [w, path.join(checkoutReal, ...(w === "." ? [] : w.split("/")))])), section: (o.brokerSection ?? brokerSection)(sb) };
1745
1974
  } else {
@@ -1751,8 +1980,9 @@ async function bindUnitCargo({ o, git, root, ds, executor, which }) {
1751
1980
  catch (error) { if (dir) fs.rmSync(dir, { recursive: true, force: true }); return { refusal: `the runtime cargo wrapper cannot be installed (${error.message})` }; }
1752
1981
  const scan = shimModule.shimScanEnv(o.env, installed);
1753
1982
  const delta = Object.fromEntries(Object.entries(scan).filter(([k, v]) => o.env[k] !== v));
1983
+ const { CARRIED_CARGO_MAP_ENV } = await import("../carried-cargo.mjs");
1754
1984
  return {
1755
- env: delta, note: `runtime cargo wrapper ${installed.shim} (${installed.mode}): each cargo call a unit stage reaches binds its workspace's checkout target directory or is refused`,
1985
+ env: { ...delta, ...(installed.mode === "broker" ? { [CARRIED_CARGO_MAP_ENV]: installed.mapFile } : {}) }, note: `runtime cargo wrapper ${installed.shim} (${installed.mode}): each cargo call a unit stage reaches binds its workspace's checkout target directory or is refused`,
1756
1986
  // Every call the wrapper decided is in the gate-owned ledger: a refused or unverified call fails the stage even when the test ignored cargo's exit (7.10, decision C).
1757
1987
  check: async () => { const { readLedger, UNSCOPED } = await import("../cargo-ledger.mjs"); return readLedger(installed.ledgerDir, UNSCOPED).problems ?? []; },
1758
1988
  cleanup: () => { fs.rmSync(installed.dir ?? dir, { recursive: true, force: true }); },
@@ -1785,10 +2015,13 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
1785
2015
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1786
2016
  const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
1787
2017
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2018
+ { const decided = routePlan(o, { to: option("--to"), stages: [], devTest: true, executor, capabilities: await brokerCapabilityStateWithHandshake(o, { to: option("--to"), executor }) });
2019
+ if (decided.refusal?.code === "RK_BROKER_CAPABILITY_MISSING") { o.err(`${tag} BLOCKED: ${decided.refusal.code}: ${decided.refusal.message}; nothing ran`); return 1; } }
1788
2020
  if (executor.kind === "broker" && !o.snapshotBroker) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: a broker host builds a unit snapshot only through the snapshot carrier; nothing ran`); return 1; }
1789
2021
  const common = git("rev-parse", "--git-common-dir");
1790
2022
  if (failed(common) || !text(common).trim()) { o.err(`${tag} cannot read the git directory`); return 2; }
1791
2023
  const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2024
+ { const mismatch = await declaredSnapshotRootRefusal(o, executor, snapshotsAt); if (mismatch) { o.err(`${tag} BLOCKED: ${mismatch.code}: ${mismatch.message}; nothing ran`); return 1; } }
1792
2025
  let own;
1793
2026
  try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: `door:unit:${option("--to")}`, snapshot: path.join(snapshotsAt, snap.repositoryOf(root).key), commit, platform: o.platform, env: o.env }); }
1794
2027
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 1; }
@@ -1800,7 +2033,9 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
1800
2033
  o.out(`${tag} snapshot ${synced.path} is commit ${synced.commit} tree ${synced.tree} (written=${synced.written} removed=${synced.removed} unchanged=${synced.unchanged})`);
1801
2034
  const ds = { own, path: synced.path, commit: synced.commit, tree: synced.tree, root: snapshotsAt, ledger: { entries: [] } };
1802
2035
  // A unit's test or pack that reaches Cargo must never write `target/` into the snapshot (npm units included): see bindUnitCargo.
1803
- const bound = await bindUnitCargo({ o, git, root, ds, executor, which });
2036
+ // A check starts no unit command: its own Class A path binds read-only Cargo calls. Resolving every unit's compile output
2037
+ // here would run unrelated metadata before the scoped check even begins.
2038
+ const bound = option("--to") === "check" ? { env: {}, cleanup() {} } : await bindUnitCargo({ o, git, root, ds, executor, which, scopeDirs: [path.posix.dirname(manifest)] });
1804
2039
  if (bound.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${bound.refusal}; nothing ran`); return 1; }
1805
2040
  if (bound.note) o.out(`${tag} ${bound.note}`);
1806
2041
  const envExtra = bound.env;
@@ -1813,14 +2048,15 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
1813
2048
  return await runUnitGate({ selector, to: option("--to"), root: ds.path, commonDir: path.resolve(root, text(common).trim()), commit: ds.commit, tree: ds.tree, executor, env: o.env, platform: o.platform, source, envExtra,
1814
2049
  resolve: (sel, r) => resolveUnit(sel, r, { granted: () => localBuildGrantedAt(source) }),
1815
2050
  ...(o.loadUnitOwner ? { loadOwner: o.loadUnitOwner } : {}),
2051
+ ...(o.unitGateDeps ?? {}), // test seam: fetchReceipt, stageCommands, verifyReceipts, selfGit
1816
2052
  stageGuard: { start: (stage) => openSnapshotStage({ o, ds, root, stage: `unit:${stage}`, tag }), end: async (stage) => {
1817
2053
  if (!(await closeSnapshotStage({ o, ds, root, stage: `unit:${stage}`, step: `unit:${stage}`, tag })).ok) return false;
1818
2054
  const calls = bound.check ? await bound.check() : [];
1819
2055
  for (const p of calls.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [unit:${stage}] a cargo call the unit stage made did not complete: ${typeof p === "string" ? p : JSON.stringify(p)}`);
1820
2056
  return calls.length === 0;
1821
2057
  } },
1822
- check: (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier], { ...io, cwd: root, buildOwnership: own })))(tier), out: o.out, err: o.err,
1823
- ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(report, null, 2)}\n`) } : {}),
2058
+ check: (tier) => (o.runCheck ?? (() => main(["check", "--commit", ds.commit, "--full-a", "--snapshot", ds.path, "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root, buildOwnership: own })))(tier), out: o.out, err: o.err,
2059
+ ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(o.brokerException ? { ...report, brokerException: o.brokerException } : report, null, 2)}\n`) } : {}),
1824
2060
  });
1825
2061
  } finally { bound.cleanup(); }
1826
2062
  } finally {
@@ -1830,52 +2066,12 @@ async function runUnitSnapshot(o, args, root, git, io, option) {
1830
2066
  }
1831
2067
 
1832
2068
  /**
1833
- * Whether the door takes the snapshot route on this host. Linux always does (direct, no broker); a broker host (Mac, Dell) does only
1834
- * when the snapshot carrier is wired (`deps.snapshotBroker`). Until the cutover commit wires it in production, that is tests only;
1835
- * every other Mac or Dell run keeps the checkout route (decision 2 "Interim"). No flag or environment variable selects it.
2069
+ * Whether the door takes the snapshot route on this host: every host but hosted CI, whose runner checkout is the commit. Linux
2070
+ * compiles it directly; Mac and Dell compile it through the RustRight snapshot carrier. There is no other route and no flag or
2071
+ * environment variable that selects one (rightkit#1301: the checkout route is deleted).
1836
2072
  */
1837
2073
  export function snapshotRouteSelected(o) {
1838
- return directPlatform(o.platform) || Boolean(o.snapshotBroker);
1839
- }
1840
-
1841
- /**
1842
- * rightkit-gate --to dev|test [-- <right-release dev|test arguments>] on the CHECKOUT route (Mac and Dell until the cutover commit):
1843
- * class A over the working tree first (nothing compiles while it is red), then `right-release dev|test` runs under a live door run,
1844
- * the only way it runs at all. The executor is the same as every other stage: the broker where there is one (its own admission
1845
- * checks again), plain cargo on an unmanaged host, never around a managed host's broker. The cutover commit deletes this function.
1846
- */
1847
- async function runWorktreeCompile(o, args, root, git, io, to, appRoot = root) {
1848
- const tag = "rightkit-gate:";
1849
- const separator = args.indexOf("--");
1850
- const passthrough = separator >= 0 ? args.slice(separator + 1) : [];
1851
- // `--to dev --platform mac` (the documented form) hands the platform to right-release as `--platform mac`; a platform given after
1852
- // `--` wins (Pulse, 2026-10-27: the flag before `--` was dropped and right-release refused "requires --platform").
1853
- const gateArgs = separator >= 0 ? args.slice(0, separator) : args;
1854
- const platformAt = gateArgs.indexOf("--platform");
1855
- if (platformAt >= 0 && gateArgs[platformAt + 1] && !passthrough.includes("--platform")) passthrough.unshift("--platform", gateArgs[platformAt + 1]);
1856
- const commit = text(git("rev-parse", "HEAD")).trim();
1857
- const tree = text(git("rev-parse", "HEAD^{tree}")).trim();
1858
- const common = text(git("rev-parse", "--git-common-dir")).trim();
1859
- if (!/^[0-9a-f]{40}$/.test(commit) || !common) { o.err(`${tag} cannot read HEAD and the git directory`); return 2; }
1860
- const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1861
- const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
1862
- if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
1863
- o.out(`${tag} --to ${to} on the working tree of ${commit.slice(0, 12)}: check (class A) -> right-release ${to}; executor ${executor.kind}`);
1864
- // Full class A on the working tree, the checks that execute repository code included, before right-release compiles anything.
1865
- const checked = await (o.runCheck ?? (() => main(["check", "--worktree", "--full-a"], { ...io, cwd: root })))();
1866
- if (checked !== 0) { o.err(`${tag} --to ${to} stopped at check: nothing compiled`); return 1; }
1867
- const commonDir = path.resolve(root, common);
1868
- const run = startRun({ commonDir, commit, tree, stage: to });
1869
- try {
1870
- const cli = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
1871
- // right-release reads its config from its cwd: the resolved app root (appRootOf), not the directory the command was typed in
1872
- // (apps/foo/src is apps/foo; Astra, third pass).
1873
- // The door's own RightKit runs every sub-step: a dev target's `right-release web-build|stage-sidecar` steps resolve `right-release`
1874
- // on PATH, which found the app's node_modules copy (HeardRight on the Dell, 2026-10-27: the app's 0.6.8 guard refused what the
1875
- // door's 0.6.9 admitted). An installed gate (node_modules/@rightkit/release/bin) has its package's bin shims two levels up.
1876
- const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: appRoot, env: { ...o.env, ...run.env, ...doorPath(o) } });
1877
- return r.status === 0 ? 0 : 1;
1878
- } finally { endRun(run); }
2074
+ return o.env?.GITHUB_ACTIONS !== "true";
1879
2075
  }
1880
2076
 
1881
2077
  /** The app's root inside the snapshot: `<snapshot>/<appDir>`, the snapshot itself for an app at the repository root. */
@@ -1893,43 +2089,430 @@ function sourceSplitOf({ root, ds, appDir }) {
1893
2089
  /**
1894
2090
  * Why the snapshot route cannot start on this host, or null. The route is the executor's: `direct` hosts (Linux always, any host
1895
2091
  * with no broker) take it as they are. A broker host compiles the snapshot only through the RustRight snapshot carrier, which the
1896
- * gate reaches through `deps.snapshotBroker` (item 7 rows 7.20 to 7.24, wired in production by the carrier lane); without it the
1897
- * host is refused (RK_SNAPSHOT_CARRIER_NOT_WIRED), and a broker that cannot be reached is refused too (RK_SNAPSHOT_BROKER_UNAVAILABLE).
1898
- * Neither ever falls back to compiling the checkout.
2092
+ * gate reaches through `deps.snapshotBroker` (production wires it on Mac and Dell: `productionEntryDeps`); a gate started without
2093
+ * it is refused (RK_SNAPSHOT_CARRIER_NOT_WIRED: tests only, never production), and a broker that cannot be reached is refused too
2094
+ * (RK_SNAPSHOT_BROKER_UNAVAILABLE). Neither ever falls back to compiling the checkout.
1899
2095
  */
1900
- export function snapshotRouteRefusal(o, { rightRelease = false } = {}) {
2096
+ export function snapshotRouteRefusal(o) {
1901
2097
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1902
2098
  const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
1903
2099
  if (executor.kind === "refuse") return { code: "RK_SNAPSHOT_BROKER_UNAVAILABLE", message: executor.problem };
1904
- // right-release's own children (dev, test) reach Cargo with no carrier of their own on a broker host (row 7.24): refused even when the gate has one.
1905
- if (executor.kind === "broker" && rightRelease) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `right-release dev and test reach Cargo on a ${o.platform} broker host only through the snapshot carrier's per-call wrapper (item 7 row 7.24), which is not wired` };
1906
- if (executor.kind === "broker" && !o.snapshotBroker) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `a ${o.platform} host compiles a commit snapshot only through the RustRight snapshot carrier (item 7 rows 7.20 to 7.24), which this gate does not carry yet` };
2100
+ if (executor.kind === "broker" && !o.snapshotBroker) return { code: "RK_SNAPSHOT_CARRIER_NOT_WIRED", message: `a ${o.platform} host compiles a commit snapshot only through the RustRight snapshot carrier (item 7 rows 7.20 to 7.24), and this gate was started without it` };
1907
2101
  return null;
1908
2102
  }
1909
2103
 
2104
+ /** The refusal codes a route probe can answer with: the codes the stages themselves print (`BLOCKED: <code>: ...`). */
2105
+ export const ROUTE_REFUSAL_CODES = Object.freeze(["RK_TAURI_CONFIG_OVERRIDE", "RK_STAGE_HOST_MISMATCH", "RK_SNAPSHOT_BROKER_UNAVAILABLE", "RK_SNAPSHOT_CARRIER_NOT_WIRED", "RK_BROKER_CAPABILITY_MISSING", "RK_BROKER_UNAVAILABLE",
2106
+ // One signing identity per product (rightkit#1303): the Mac and the Dell refuse an unsigned, ad hoc or re-identified test or dev build.
2107
+ ...SIGNING_REFUSAL_CODES]);
2108
+ /** The stages `--route` can answer for (`release` writes to the publisher and has no route of its own on a host). */
2109
+ export const ROUTE_PROBE_STAGES = Object.freeze(["check", "compile", "package", "qualify", "dev", "test"]);
2110
+ export const ROUTE_SCHEMA = "rightkit-gate.route/1";
2111
+
2112
+ /**
2113
+ * The capabilities a broker must advertise for the carried stages (rightkit#1301 B2). RustRight reports them in `rustright doctor --json`
2114
+ * as `.broker.capabilities` (the HANDSHAKE's `capabilities`: `journal-v1`, `snapshot-readonly-v1`, `snapshot-adapter-v1`, ...; constants
2115
+ * in rustright-protocol response.rs). The two required strings ship with RustRight's upcoming change: today's broker (78eda7e) reports no
2116
+ * `.broker.capabilities`, so this release refuses every stage that builds until that broker is installed. `snapshot-adapter-v1`
2117
+ * (rustright#190, rightkit#1301 B10) is the broker's acceptance of a snapshot-bound adapter job (pnpm, node, QA) inside a gate snapshot,
2118
+ * under which a Tauri CLI's compiles run nested: without it every pnpm step of a snapshot stage (an app's dev and package steps, an npm
2119
+ * unit's compile and package) is refused by the broker, so the gate refuses first. `stage-grant-v1` is no longer required: the gate mints
2120
+ * no stage grant (one model: every compile of a snapshot stage is a carried request or nested under one).
2121
+ */
2122
+ export const REQUIRED_BROKER_CAPABILITIES = Object.freeze(["snapshot-readonly-v1", "snapshot-adapter-v1"]);
2123
+
2124
+ /**
2125
+ * The broker's capabilities, asked without a build through `rustright doctor --json`: `{ ok: true, capabilities }` or `{ ok: false, problem }`.
2126
+ * A missing client, a client that cannot run, a hung (20 s), unparsable or field-less answer is `ok: false`, which the route refuses (fail
2127
+ * closed). A doctor that reports an unhealthy host still prints its JSON: the field decides, not the exit status.
2128
+ */
2129
+ export function queryBrokerCapabilities(o, { executor = null } = {}) {
2130
+ const client = brokerClientOf(o, executor);
2131
+ if (!client) return { ok: false, problem: "the RustRight client is not on PATH" };
2132
+ const r = spawnBrokerClient(o.spawn, client, ["doctor", "--json"], { env: brokerEnvOf(o, executor), encoding: "utf8", timeout: 20_000, windowsHide: true }, { platform: o.platform ?? process.platform, readFile: o.readFile });
2133
+ if (r?.error || r?.status === null || r?.status === undefined) return { ok: false, problem: `\`rustright doctor --json\` did not finish: ${why(r)}` };
2134
+ let doc;
2135
+ try { doc = JSON.parse(text(r)); } catch { return { ok: false, problem: `\`rustright doctor --json\` printed no JSON${r.status ? ` (exit ${r.status})` : ""}` }; }
2136
+ const caps = doc?.broker?.capabilities;
2137
+ if (!Array.isArray(caps) || !caps.every((c) => typeof c === "string")) return { ok: false, problem: "`rustright doctor --json` reports no `.broker.capabilities` (a broker that predates snapshot-readonly-v1 and snapshot-adapter-v1)" };
2138
+ // The list is the broker's HANDSHAKE.capabilities only when the client received that handshake: an unreachable broker (doctor reports an empty list for it) advertises nothing, which is a
2139
+ // different statement from a broker that advertises few capabilities, so it is a problem naming why, never a list. Each name is advertised once.
2140
+ if (doc.broker.reachable !== true) return { ok: false, problem: `the broker is not reachable, so its HANDSHAKE.capabilities were not received (\`rustright doctor --json\` .broker.reachable is ${JSON.stringify(doc.broker.reachable ?? null)}${typeof doc.broker.error === "string" && doc.broker.error ? `: ${doc.broker.error}` : ""})` };
2141
+ if (new Set(caps).size !== caps.length) return { ok: false, problem: "`rustright doctor --json` lists a capability twice: the broker's HANDSHAKE.capabilities are not read from an answer that repeats itself" };
2142
+ return { ok: true, capabilities: caps };
2143
+ }
2144
+
2145
+ /**
2146
+ * What a route decision needs of the broker: `null` when nothing is asked (not a broker executor, hosted CI, `--to check`, or a gate
2147
+ * whose broker is an injected fixture without a declared capability query), else the answer. The production carrier asks the real
2148
+ * client; a test's injected broker declares its own through `o.brokerCapabilities`.
2149
+ */
2150
+ export function brokerCapabilityState(o, { to, executor }) {
2151
+ if (executor?.kind !== "broker" || to === "check" || o.env.GITHUB_ACTIONS === "true") return null;
2152
+ if (typeof o.brokerCapabilities === "function") return o.brokerCapabilities(o);
2153
+ return typeof o.snapshotBroker?.create === "function" ? queryBrokerCapabilities(o, { executor }) : null;
2154
+ }
2155
+
2156
+ /**
2157
+ * GB-51 / GB-62: the capability state a route plans from, with the broker's HANDSHAKE received by the gate itself (broker-handshake.mjs) beside `rustright doctor --json`: both are asked, they must agree,
2158
+ * and a plan sees only names advertised by what answered. Not asked (null) exactly when `brokerCapabilityState` asks nothing; an injected fixture broker that declares its own capabilities
2159
+ * (`o.brokerCapabilities`) keeps them unless it also declares a handshake (`o.brokerHandshake`, a function returning a parsed handshake or `{ ok: false, problem }`).
2160
+ */
2161
+ export async function brokerCapabilityStateWithHandshake(o, { to, executor }) {
2162
+ const doctor = brokerCapabilityState(o, { to, executor });
2163
+ if (doctor === null) return null;
2164
+ const injected = typeof o.brokerHandshake === "function";
2165
+ if (!injected && typeof o.brokerCapabilities === "function") return doctor;
2166
+ let handshake;
2167
+ try {
2168
+ handshake = injected ? await o.brokerHandshake(o) : await queryBrokerHandshake({ ...o, env: brokerEnvOf(o, executor) }, { client: brokerClientOf(o, executor) });
2169
+ } catch (error) { handshake = { ok: false, problem: `the broker's HANDSHAKE could not be read (${error?.message ?? error})` }; }
2170
+ return mergeCapabilityViews({ doctor, handshake });
2171
+ }
2172
+
2173
+ /**
2174
+ * The refusal of `--to release` on a host that is not the trusted publisher, or null: a release uploads sealed bytes and runs only in the release workflow's
2175
+ * protected publication job (GITHUB_ACTIONS with RIGHTKIT_PUBLISHER=github-environment); a mobile platform's store upload has its own tier-less route. Asked by a real
2176
+ * run before it takes ownership or syncs the snapshot (E2E L3-03); the probe has no release route, so this is not a probe code.
2177
+ */
2178
+ export function releaseRouteRefusal(o, { stages, platform }) {
2179
+ if (!stages.includes("release") || MOBILE_PLATFORMS.has(platform)) return null;
2180
+ if (o.env.GITHUB_ACTIONS === "true" && o.env.RIGHTKIT_PUBLISHER === "github-environment") return null;
2181
+ return { code: "RK_RELEASE_PUBLISHER_ONLY", message: "--to release runs only in the trusted publisher (the release workflow's protected publication job). Run --to qualify on the native host, then dispatch the release workflow for the commit" };
2182
+ }
2183
+
2184
+ /**
2185
+ * Which route this host's door takes for `--to <stage>` and whether it refuses, WITHOUT running anything: the decisions of `runStages`
2186
+ * in the order it makes them. `runStages` takes its own pre-ownership route and refusal from this very function (rightkit#1297 L1), so
2187
+ * the probe and a real run cannot drift (the door-route parity test compares them over the carrier, executor, platform and Tauri
2188
+ * combinations):
2189
+ * 1. hosted CI (GITHUB_ACTIONS) builds the runner's checkout, which is the commit: no snapshot route;
2190
+ * 2. every other host takes the snapshot route (Linux direct; Mac and Dell through the carrier; there is no checkout route);
2191
+ * 3. the snapshot route refuses an inherited TAURI_CONFIG;
2192
+ * 4. then `snapshotRouteRefusal` (skipped for `--to check`, which compiles nothing): an unreachable broker, a gate without its carrier;
2193
+ * 5. then an executor that cannot start (a broker-managed host whose broker is unreachable), on any route.
2194
+ * `executor` is a resolved executor. Returns { route, refusal } with
2195
+ * refusal null or { code, stage, message }. Reads no checkout, takes no ownership, syncs no snapshot and spawns nothing itself.
2196
+ */
2197
+ export function routePlan(o, { to, stages, devTest, executor, capabilities = null, appConfig = null, stagePlatform = null }) {
2198
+ const refuse = (code, stage, message) => ({ code, stage, message });
2199
+ const hosted = o.env.GITHUB_ACTIONS === "true";
2200
+ const snapshot = !hosted && snapshotRouteSelected(o);
2201
+ const route = hosted ? "hosted" : "snapshot";
2202
+ if (snapshot) {
2203
+ if (o.env.TAURI_CONFIG !== undefined) return { route, refusal: refuse("RK_TAURI_CONFIG_OVERRIDE", to, "TAURI_CONFIG is set in the environment; it would replace the commit's Tauri configuration (externalBin included) for every build below, so a commit-only build refuses it") };
2204
+ // A stage that packages or qualifies a platform's artifact runs on that platform's own host: Mac and Windows artifacts are constructed on their
2205
+ // native platform and never cross-built (GATE-CONTRACT s1 "Native build target"; PLATFORM-MATRIX: packages only on the matching native host).
2206
+ // Refused here, before ownership, a snapshot or any process, so `--route` and a real run say the same thing (E2E L3-03).
2207
+ const hostNeeded = STAGE_NATIVE_HOST[stagePlatform];
2208
+ if (stagePlatform && (stages.includes("package") || stages.includes("qualify")) && hostNeeded && o.platform !== hostNeeded) {
2209
+ return { route, refusal: refuse("RK_STAGE_HOST_MISMATCH", to, `--to ${to} --platform ${stagePlatform} builds the ${stagePlatform} artifact, which is constructed only on a ${hostNeeded === "darwin" ? "Mac" : "Windows"} host (this host is ${o.platform}); a Mac or Windows artifact is never cross-built from another platform, so nothing was started`) };
2210
+ }
2211
+ const refused = to === "check" ? null : snapshotRouteRefusal(o);
2212
+ if (refused) return { route, refusal: refuse(refused.code, to, refused.message) };
2213
+ // The broker must accept what the carried stages ask of it (rightkit#1301 B2): a missing or failing capability query refuses.
2214
+ if (capabilities && executor?.kind === "broker" && to !== "check") {
2215
+ const missing = capabilities.ok ? REQUIRED_BROKER_CAPABILITIES.filter((c) => !capabilities.capabilities.includes(c)) : REQUIRED_BROKER_CAPABILITIES;
2216
+ if (!capabilities.ok || missing.length) return { route, refusal: refuse("RK_BROKER_CAPABILITY_MISSING", to, `the broker ${capabilities.ok ? `does not advertise ${missing.join(", ")}` : `cannot be asked for its capabilities (${capabilities.problem})`} (\`rustright doctor --json\` \`.broker.capabilities\`); this release builds only against a broker that accepts snapshot-bound read-only Cargo and snapshot-bound adapter jobs (rightkit#1301, rustright#188, rustright#190)`) };
2217
+ }
2218
+ }
2219
+ // GB-51: a declared native build tool with no carried adapter (xcodegen, xcodebuild, Gradle, CMake) refuses here, at planning, naming the missing
2220
+ // capability, instead of after the earlier stages built (the door's launch-time refusal remains the backstop).
2221
+ if (snapshot && executor?.kind === "broker" && appConfig?.config && to !== "check") {
2222
+ const refusalText = nativeToolRefusal(nativeToolUses({ config: appConfig.config, to, devPlatform: appConfig.nativePlatform ?? appConfig.devPlatform ?? null, platform: appConfig.nativePlatform ?? null }), { capabilities, readFile: appConfig.readFile ?? null, listFiles: appConfig.listFiles ?? null });
2223
+ if (refusalText) return { route, refusal: refuse("RK_BROKER_CAPABILITY_MISSING", to, refusalText) };
2224
+ }
2225
+ // One signing identity per product, every build (rightkit#1303): on the Mac and the Dell a test or dev build that would run unsigned, ad hoc or under
2226
+ // another identifier is refused here, from the commit's own config, so `--route` and a real run decide alike. Linux and hosted CI are exempt.
2227
+ if (snapshot && appConfig?.config && (devTest || to === "test" || to === "dev")) {
2228
+ const [first] = suiteSigningRefusals({ config: appConfig.config, platform: o.platform, env: o.env, to, devPlatform: appConfig.devPlatform ?? null, readFile: appConfig.readFile, listFiles: appConfig.listFiles ?? null });
2229
+ if (first) return { route, refusal: refuse(first.code, to, first.message) };
2230
+ }
2231
+ if (executor?.kind === "refuse") return { route, refusal: refuse("RK_BROKER_UNAVAILABLE", to, executor.problem) };
2232
+ return { route, refusal: null };
2233
+ }
2234
+
2235
+ /**
2236
+ * The app config of the selected commit for the route's signing refusals, read from the object database (never the checkout): `{ config, readFile }`,
2237
+ * `readFile(rel)` being an app-relative file of the commit (text or null). Null when the commit has no readable JSON config (class A judges that).
2238
+ * The probe covers JSON configs only: a legacy right-release.config.mjs is not evaluated here (that would run app code in the probe), so `--route` is
2239
+ * silent about it. The same policy (suiteSigningRefusals) is enforced at run time on whatever config loaded: test-command.mjs before any suite runs and
2240
+ * development.mjs before a dev target builds, so a .mjs config's unsigned product build is refused there, on the Mac and the Dell.
2241
+ */
2242
+ export function routeAppConfig({ git, commit, appDir = ".", platform = null }) {
2243
+ if (!commit) return null;
2244
+ const show = (rel) => { const r = git("show", `${commit}:${path.posix.normalize(appDir === "." ? rel : `${appDir}/${rel}`)}`); return failed(r) ? null : text(r); };
2245
+ const raw = show("right-release.config.json");
2246
+ if (raw === null) return null;
2247
+ let config;
2248
+ try { config = JSON.parse(raw); } catch { return null; }
2249
+ const list = (c, rel) => { const r = git("ls-tree", "-r", "--name-only", c, "--", rel); return failed(r) ? null : text(r).split(/\r?\n/).filter(Boolean); };
2250
+ return { config, readFile: show, listFiles: gitListRustFiles(list, commit, appDir), devPlatform: platform === "mac" || platform === "win" ? platform : null, nativePlatform: platform ?? null };
2251
+ }
2252
+
2253
+ /** The `--platform` a dev or test run was given: before `--`, or in the arguments after it (a platform after `--` wins, as runDevTest hands it on). */
2254
+ export function devTestPlatform(args) {
2255
+ const at = args.indexOf("--");
2256
+ const find = (list) => { const i = list.lastIndexOf("--platform"); return i < 0 ? null : list[i + 1] ?? null; };
2257
+ return (at >= 0 ? find(args.slice(at + 1)) : null) ?? find(at >= 0 ? args.slice(0, at) : args);
2258
+ }
2259
+
2260
+ /**
2261
+ * `rightkit-gate --to <stage> --route [--platform p] [--commit rev]`: prints one JSON line (`rightkit-gate.route/1`) naming the route
2262
+ * the stage would take and the refusal it would meet, and runs nothing: no ownership, no snapshot sync, no check, no run record, no
2263
+ * receipt, no right-release. Exit 0 when it answered (a refusal is an answer), 2 when it could not compute one.
2264
+ */
2265
+ async function runRouteProbe(o, args, root, git, { to, platform, stages, devTest, appDir }) {
2266
+ const tag = "rightkit-gate: --route:";
2267
+ const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
2268
+ const snap = await import("../snapshot.mjs");
2269
+ let selected;
2270
+ try { selected = snap.resolveCommit(root, option("--commit") ?? "HEAD"); }
2271
+ catch (e) { o.err(`${tag} cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2272
+ const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2273
+ const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile });
2274
+ let plan;
2275
+ const capabilities = await brokerCapabilityStateWithHandshake(o, { to, executor });
2276
+ try { plan = routePlan(o, { to, stages, devTest, executor, stagePlatform: platform, capabilities, appConfig: to !== "check" ? routeAppConfig({ git, commit: selected.commit, appDir, platform: devTestPlatform(args) }) : null }); }
2277
+ catch (e) { o.err(`${tag} ${e.message}`); return 2; }
2278
+ // The declared Linux broker's snapshot root: the probe reports the refusal a real run would meet (read-only: one status probe of the broker).
2279
+ if (!plan.refusal && plan.route === "snapshot") {
2280
+ let at = null; try { at = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env }); } catch { at = null; }
2281
+ const mismatch = at ? await declaredSnapshotRootRefusal(o, executor, at) : null;
2282
+ if (mismatch) plan = { ...plan, refusal: { code: mismatch.code, stage: to, message: mismatch.message } };
2283
+ }
2284
+ // Read-only: GIT_OPTIONAL_LOCKS=0, so the probe's `git status` never refreshes the index or contends with a writer (rightkit#1297 L4).
2285
+ const dirty = o.spawn("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: root, env: { ...o.env, GIT_OPTIONAL_LOCKS: "0" } });
2286
+ const version = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
2287
+ o.out(JSON.stringify({
2288
+ schema: ROUTE_SCHEMA, gate: version, to, platform: platform ?? null, host: o.platform, app: appDir, commit: selected.commit,
2289
+ executor: executor.kind, carrier: o.snapshotBroker ? (o.snapshotBroker.kind ?? "injected") : null, route: plan.route,
2290
+ brokerCapabilities: capabilities?.ok ? capabilities.capabilities : null, stages: devTest ? ["check", to] : stages, refusal: plan.refusal, checkoutClean: failed(dirty) ? null : !text(dirty).trim(),
2291
+ }));
2292
+ return 0;
2293
+ }
2294
+
2295
+ // ---- the carrier of a snapshot stage's right-release children (carried-cargo.mjs; rightkit#1301) --------------------------------------
2296
+
2297
+ /**
2298
+ * The environment of a carried stage's child: the base, the stage's carrier and the caller's extras, without an output directory.
2299
+ * The broker pins every Cargo output, so the Tauri CLI and every other child must not look for bundles in a directory the environment
2300
+ * names (rightkit#1301 F5): CARGO_TARGET_DIR and CARGO_BUILD_TARGET_DIR are dropped, in any case.
2301
+ */
2302
+ export function carriedEnv(carrier, base, extra = {}) {
2303
+ if (!carrier.carried) return { ...base, ...carrier.env, ...extra };
2304
+ const strip = (env) => {
2305
+ const out = { ...env };
2306
+ for (const key of Object.keys(out)) {
2307
+ if (/^(CARGO_TARGET_DIR|CARGO_BUILD_TARGET_DIR)$/i.test(key)) delete out[key];
2308
+ }
2309
+ return out;
2310
+ };
2311
+ return { ...strip(base), ...carrier.env, ...strip(extra) };
2312
+ }
2313
+
2314
+ // ---- the declared Linux broker's snapshot root --------------------------------------------------------------------------------------------------
2315
+ // The broker admits a snapshot only under `<its data root>/snapshots`. On Linux the gate's snapshot root is `~/.config/rightkit/gate.json` `snapshotRoot`, else
2316
+ // `~/.local/state/rightkit/snapshots` (snapshot.mjs snapshotRoot); a host whose broker keeps its data elsewhere would sync thousands of files, take build ownership and submit,
2317
+ // only to be refused at admission with the ownership left unknown. So the declared route compares the two BEFORE ownership or a sync, and refuses typed.
2318
+ export const LINUX_SNAPSHOT_ROOT_MISMATCH = "RK_LINUX_BROKER_SNAPSHOT_ROOT_MISMATCH";
2319
+ const ANSWERING_ROOTS_TIMEOUT_MS = 10_000;
2320
+
2321
+ /**
2322
+ * What the answering broker says its roots are: the health probe's `STATUS` carries `roots` (`rustright.answering-roots.v1`: dataRoot, controlRoot, ...), after the HANDSHAKE the first event
2323
+ * is. Reads lines until a STATUS with roots (or any other end) and resolves `{ ok: true, dataRoot }` or `{ ok: false, problem }`; never rejects. `connect` is the transport (tests, `net.connect`).
2324
+ */
2325
+ export function readAnsweringRoots({ endpoint, identity, connect = (target) => net.connect({ path: target }), timeoutMs = ANSWERING_ROOTS_TIMEOUT_MS }) {
2326
+ return new Promise((resolve) => {
2327
+ let settled = false; let conn = null; let timer = null; let buffer = "";
2328
+ const finish = (result) => { if (settled) return; settled = true; clearTimeout(timer); try { conn?.destroy(); } catch { /* gone */ } resolve(result); };
2329
+ const problem = (why) => finish({ ok: false, problem: why });
2330
+ try { conn = connect(endpoint); } catch (e) { problem(`cannot connect to the broker at ${endpoint} (${e?.code ?? e?.message ?? e})`); return; }
2331
+ timer = setTimeout(() => problem(`the broker at ${endpoint} reported no roots within ${timeoutMs} ms`), timeoutMs);
2332
+ conn.on("connect", () => { try { conn.write(`${JSON.stringify({ type: "STATUS", protocol: BUILD_CONTROL_PROTOCOL, runtime: identity, id: HANDSHAKE_PROBE_ID })}\n`); } catch (e) { problem(`cannot send the probe to the broker at ${endpoint} (${e?.message ?? e})`); } });
2333
+ conn.on("data", (chunk) => {
2334
+ buffer += String(chunk);
2335
+ if (buffer.length > (1 << 21)) { problem("the broker's reply is too large"); return; }
2336
+ for (let at = buffer.indexOf("\n"); at >= 0; at = buffer.indexOf("\n")) {
2337
+ const line = buffer.slice(0, at); buffer = buffer.slice(at + 1);
2338
+ let event; try { event = JSON.parse(line); } catch { problem("the broker's reply is not JSON"); return; }
2339
+ if (event?.type === "STATUS" && event.roots && typeof event.roots === "object") {
2340
+ const root = event.roots.dataRoot;
2341
+ if (event.roots.schema !== "rustright.answering-roots.v1" || typeof root !== "string" || !path.isAbsolute(root)) { problem("the broker's STATUS roots are not rustright.answering-roots.v1 with an absolute dataRoot"); return; }
2342
+ finish({ ok: true, dataRoot: root }); return;
2343
+ }
2344
+ if (event?.type !== "HANDSHAKE" && event?.type !== "STATUS") { problem(`the broker answered ${event?.type ?? "something"} to the roots probe`); return; }
2345
+ }
2346
+ });
2347
+ const closed = () => problem(`the broker at ${endpoint} closed without reporting its roots (a broker that predates the answering roots)`);
2348
+ conn.on("error", (e) => problem(`the broker connection at ${endpoint} failed (${e?.code ?? e?.message ?? e})`));
2349
+ conn.on("close", closed); conn.on("end", closed);
2350
+ });
2351
+ }
2352
+
2353
+ /** The declared Linux broker's data root: what it reports on the wire (the validated client, its runtime manifest, the declared endpoint), `{ ok, dataRoot }` or `{ ok: false, problem }`. */
2354
+ export async function queryBrokerDataRoot(o, executor) {
2355
+ try {
2356
+ const exe = resolveBrokerClient(executor.client, { platform: o.platform, readFile: o.readFile }).file;
2357
+ let real = exe; try { real = fs.realpathSync.native(exe); } catch { real = exe; }
2358
+ const identity = parseRuntimeIdentity(fs.readFileSync(path.join(path.dirname(real), RUNTIME_MANIFEST_FILE), "utf8"));
2359
+ if (!identity.ok) return { ok: false, problem: identity.problem };
2360
+ return await readAnsweringRoots({ endpoint: executor.socket, identity: identity.identity, ...(o.connectBroker ? { connect: o.connectBroker } : {}) });
2361
+ } catch (e) { return { ok: false, problem: e.message }; }
2362
+ }
2363
+
2364
+ /**
2365
+ * null, or `{ code, message }` when the declared Linux broker would refuse this gate's snapshots. Only the declared route is asked (a Linux host with no declared broker stays direct and
2366
+ * has no broker root). The broker's own report decides; when it reports none (a broker that predates it, or an unreadable answer) the DECLARED data root (RUSTRIGHT_DATA_ROOT, else
2367
+ * RIGHTKIT_DATA_ROOT, MANAGED_ROOT or EXTERNAL_ROOT) is the comparison, and the message says it is only a declaration; with neither nothing is known and nothing is refused here.
2368
+ * `snapshotsAt` is the gate's snapshot root.
2369
+ */
2370
+ export async function declaredSnapshotRootRefusal(o, executor, snapshotsAt) {
2371
+ if (executor?.kind !== "broker" || executor.route !== LINUX_BROKER_ROUTE) return null;
2372
+ const answered = await (o.queryBrokerDataRoot ?? queryBrokerDataRoot)(o, executor);
2373
+ let dataRoot; let basis;
2374
+ if (answered?.ok) { dataRoot = answered.dataRoot; basis = "the broker reports its data root"; }
2375
+ else {
2376
+ dataRoot = envNamed(o.env, "DATA_ROOT") || envNamed(o.env, "MANAGED_ROOT") || envNamed(o.env, "EXTERNAL_ROOT");
2377
+ basis = `the broker did not report its roots (${answered?.problem ?? "no answer"}), so the DECLARED data root from the environment is the comparison (a declaration, not the broker's word)`;
2378
+ }
2379
+ if (!dataRoot) return null;
2380
+ const real = (p) => { try { return fs.realpathSync.native(p); } catch { return path.resolve(p); } };
2381
+ const expected = path.join(dataRoot, "snapshots");
2382
+ if (real(expected) === real(snapshotsAt)) return null;
2383
+ return { code: LINUX_SNAPSHOT_ROOT_MISMATCH, message: `the gate's snapshot root is ${snapshotsAt}, but the declared Linux broker admits snapshots only under ${expected} (${basis}); set "snapshotRoot": ${JSON.stringify(expected)} in ~/.config/rightkit/gate.json (the gate host configuration), then run again. Nothing was synced and no build ownership was taken` };
2384
+ }
2385
+
2386
+ /** The RustRight client this executor drives: the declared Linux route's validated client, else the absolute `rustright` on PATH (Mac, Dell). */
2387
+ const brokerClientOf = (o, executor) => (executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor.client : whichIn(o)("rustright").find((c) => path.isAbsolute(c)));
2388
+ /** The environment the client runs in: the gate's, plus the endpoint the executor declared (the declared Linux route's socket). */
2389
+ const brokerEnvOf = (o, executor) => (executor?.route === LINUX_BROKER_ROUTE && executor.env ? { ...o.env, ...executor.env } : o.env);
2390
+ const whichIn = (o) => (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
2391
+
2392
+ /**
2393
+ * The gate's snapshot broker for this door run: `o.snapshotBroker` is the production carrier statement (`{ kind, create }`, which
2394
+ * builds the broker once: the host's enrolled pins, the declared inputs read from the snapshot, the build ownership and the RustRight
2395
+ * client) or, in a test, a ready broker object. Built once per door run.
2396
+ */
2397
+ async function carrierBroker({ o, root, ds, executor = null, which = whichIn(o) }) {
2398
+ if (ds.carrierBroker) return ds.carrierBroker;
2399
+ const carrier = o.snapshotBroker;
2400
+ let sb = carrier;
2401
+ if (typeof carrier?.create === "function") {
2402
+ const client = executor?.route === LINUX_BROKER_ROUTE && executor.kind === "broker" ? executor.client : which("rustright").find((c) => path.isAbsolute(c));
2403
+ const { brokerLeaseState } = await import("../build-ownership.mjs");
2404
+ sb = carrier.create({ lease: brokerLeaseState, env: brokerEnvOf(o, executor), platform: o.platform, checkout: root, commit: { sha: ds.commit, tree: ds.tree }, snapshot: { path: ds.path, expectedTree: ds.tree }, ownership: ds.own, client, gateFile: callingGatePath(), spawn: o.spawn });
2405
+ }
2406
+ ds.carrierBroker = sb;
2407
+ return sb;
2408
+ }
2409
+
2410
+ /** The commit's Cargo workspaces in the snapshot, each with its counterpart in the primary checkout: `{ [cargoDir]: checkout path }`, from the files alone. */
2411
+ async function snapshotCargoWorkspaces({ git, ds, root }) {
2412
+ const listed = git("ls-tree", "-r", "--name-only", "-z", ds.commit);
2413
+ if (failed(listed)) throw new Error(`cannot list the files of ${ds.commit.slice(0, 12)}: ${why(listed)}`);
2414
+ const manifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
2415
+ const { locateWorkspaceByWalk } = await import("../cargo-target-shim.mjs");
2416
+ let checkout = root; try { checkout = fs.realpathSync.native(root); } catch { /* as given */ }
2417
+ const dirs = {};
2418
+ for (const m of manifests) {
2419
+ const file = path.join(ds.path, ...m.split("/"));
2420
+ let rel;
2421
+ try { rel = posix(path.relative(ds.path, path.dirname(locateWorkspaceByWalk(file, path.dirname(file))))) || "."; } catch { rel = posix(path.dirname(m)); }
2422
+ if (rel === ".." || rel.startsWith("../")) rel = posix(path.dirname(m)) || ".";
2423
+ dirs[rel] = path.join(checkout, ...(rel === "." ? [] : rel.split("/")));
2424
+ }
2425
+ return dirs;
2426
+ }
2427
+
2428
+ /**
2429
+ * The carrier of one snapshot stage's right-release child on a broker host: the stage's workspaces, the broker section, one map and
2430
+ * ledger. Returns `{ env, refusal, finish({ stage }), cleanup }`: `env` is added to the
2431
+ * child's environment; `finish` runs after the child ended, fails on any call that was refused, unverified or unfinished and on any
2432
+ * receipt the gate cannot verify again itself, and removes the carrier. A direct executor, or no snapshot, has nothing to carry.
2433
+ */
2434
+ async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:" }) {
2435
+ const none = { env: {}, carried: false, refusal: null, async finish() { return true; }, cleanup() {} };
2436
+ if (!ds || executor?.kind !== "broker") return none;
2437
+ let base = null;
2438
+ try {
2439
+ if (!git) throw new Error("the gate's git reader is missing");
2440
+ const carried = await import("../carried-cargo.mjs");
2441
+ const { brokerSection } = await import("../snapshot-broker.mjs");
2442
+ const sb = await carrierBroker({ o, root, ds, executor });
2443
+ // Installing the carrier map sends nothing, so it records nothing: the child's own uncarried probes (`rustc -vV`, `--version`) never
2444
+ // reach the broker under a snapshot, and each carried request records its sub-handle just before it is sent (snapshot-broker.mjs).
2445
+ const checkoutDirs = await snapshotCargoWorkspaces({ git, ds, root });
2446
+ base = fs.mkdtempSync(path.join(os.tmpdir(), "rk-carrier-"));
2447
+ // The carrier is the map and the ledger on every platform: no PATH wrapper and no stage grant. The child's pnpm, node and QA steps are carried
2448
+ // adapter jobs and its Cargo calls carried requests; whatever the Tauri CLI compiles runs nested under the adapter job that started it, and the
2449
+ // broker's signed list of those children is what the gate verifies and reads the bundle root from (carried-cargo.mjs).
2450
+ const installed = (o.installCarrier ?? carried.installCarrier)({ base, snapshotRoot: ds.path, checkoutDirs, section: (o.brokerSection ?? brokerSection)(sb), known: Object.fromEntries(ds.carriedOutputs ?? []), platform: o.platform, env: o.env });
2451
+ o.out(`${tag} [${stage}] carrier (${installed.mode}): ${Object.keys(checkoutDirs).length} workspace(s); each right-release cargo call and each pnpm, node or QA step is one carried request with its own signed receipt, and a compile a step's child makes is nested under it`);
2452
+ let finished = false;
2453
+ const cleanup = () => { try { installed.cleanup?.(); } catch { /* removed below */ } fs.rmSync(base, { recursive: true, force: true }); };
2454
+ return {
2455
+ env: installed.env, carried: true, refusal: null, cleanup,
2456
+ async finish({ stage: at = stage } = {}) {
2457
+ if (finished) return true;
2458
+ finished = true;
2459
+ let problems;
2460
+ try {
2461
+ problems = await installed.problems(sb);
2462
+ // The output directories the stage's verified receipts name: the package stage's sealed manifest must record only these.
2463
+ for (const dir of installed.outputDirs?.() ?? []) ((ds.carriedByStage ??= {})[at] ??= new Set()).add(dir);
2464
+ // And by workspace, for the next stage's children: a receipt already names the output directory, so none asks Cargo for it.
2465
+ for (const [root, dir] of installed.outputsByWorkspace?.() ?? []) (ds.carriedOutputs ??= new Map()).set(root, dir);
2466
+ } catch (error) { problems = [`the cargo ledger cannot be read (${error.message})`]; } finally { cleanup(); }
2467
+ for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${at}] a broker call the right-release child made did not complete: ${typeof p === "string" ? p : JSON.stringify(p)}`);
2468
+ return problems.length === 0;
2469
+ },
2470
+ };
2471
+ } catch (error) {
2472
+ if (base) fs.rmSync(base, { recursive: true, force: true });
2473
+ return { ...none, refusal: `${error.code ?? "RK_CARGO_CARRIER"}: the stage's cargo carrier cannot be installed (${error.message})` };
2474
+ }
2475
+ }
2476
+
1910
2477
  /**
1911
2478
  * The checkout's Cargo output directory for the snapshot of `commit`: `{ dir }` (null when the commit has no Cargo manifest) or
1912
- * `{ refusal }`. Each tracked Cargo.toml of the commit is asked, in the CHECKOUT with the stage's environment, for its
1913
- * `target_directory` (which honours CARGO_TARGET_DIR and `.cargo/config.toml` build.target-dir) and its workspace root; one workspace
1914
- * yields its directory, several are refused rather than pointed at one shared directory (GATE-CONTRACT: mixed workspaces never share a target).
2479
+ * `{ refusal }`. Each Cargo.toml the COMMIT tracks (git ls-tree of the selected commit, never the checkout's files) is asked, in the
2480
+ * commit's SNAPSHOT with the stage's environment, for its `target_directory` (which honours CARGO_TARGET_DIR and the committed
2481
+ * `.cargo/config.toml` build.target-dir) and its workspace root. A path the snapshot holds is mapped lexically to the same
2482
+ * snapshot-relative path under the checkout (the output directory is the checkout's, GATE-CONTRACT "Output directory & cache"), so a
2483
+ * directory the newer checkout deleted or moved, or a dirty or untracked manifest there, changes nothing: the checkout is not read. A
2484
+ * path outside the snapshot (an absolute CARGO_TARGET_DIR) stands as reported. Optional manifests restrict discovery to a resolved
2485
+ * committed closure. Several workspaces retain independent output directories for the runtime wrapper; they never share one target.
1915
2486
  */
1916
- function cargoOutputPin({ o, git, root, commit, executor }) {
2487
+ export function cargoOutputPin({ o, git, root, commit, executor, snapshot, manifests: selectedManifests }) {
1917
2488
  const listed = git("ls-tree", "-r", "--name-only", "-z", commit);
1918
2489
  if (failed(listed)) return { refusal: `cannot list the files of ${commit.slice(0, 12)}: ${why(listed)}` };
1919
- const manifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
2490
+ const trackedManifests = text(listed).split("\0").filter((f) => f === "Cargo.toml" || f.endsWith("/Cargo.toml"));
2491
+ if (selectedManifests && selectedManifests.some((m) => !trackedManifests.includes(m))) return { refusal: "the scoped Cargo output map names a manifest outside the selected commit" };
2492
+ const manifests = selectedManifests ?? trackedManifests;
2493
+ if (manifests.length && (typeof snapshot !== "string" || !path.isAbsolute(snapshot))) return { refusal: "the output directory is resolved in the commit's snapshot and none was given" };
1920
2494
  const cargo = executor.cargo ?? ["cargo"];
2495
+ // The snapshot as the tool reports it may be a realpath of the spelling the gate holds (a symlinked temp root): both spellings map.
2496
+ const snapshots = [path.resolve(snapshot ?? "/")];
2497
+ try { snapshots.push(fs.realpathSync.native(snapshot)); } catch { /* an absent snapshot fails at the metadata call */ }
2498
+ const toCheckout = (p) => {
2499
+ for (const base of snapshots) { const rel = path.relative(base, p); if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) return path.join(root, rel); }
2500
+ return p;
2501
+ };
1921
2502
  const workspaces = new Map();
1922
2503
  for (const m of manifests) {
1923
- const file = path.join(root, ...m.split("/"));
1924
- if (!o.exists(file)) return { refusal: `the checkout has no ${m}, so its Cargo output directory cannot be resolved` };
2504
+ const file = path.join(snapshot, ...m.split("/"));
1925
2505
  const r = o.spawn(cargo[0], [...cargo.slice(1), "metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", file], { cwd: path.dirname(file), env: o.env });
1926
2506
  let doc = null;
1927
2507
  try { doc = failed(r) ? null : JSON.parse(text(r)); } catch { doc = null; }
1928
- if (!doc || typeof doc.target_directory !== "string" || !path.isAbsolute(doc.target_directory) || typeof doc.workspace_root !== "string") return { refusal: `cargo metadata in the checkout reported no absolute target_directory for ${m}${failed(r) ? ` (${why(r)})` : ""}` };
1929
- workspaces.set(path.resolve(doc.workspace_root), path.resolve(doc.target_directory));
2508
+ if (!doc || typeof doc.target_directory !== "string" || !path.isAbsolute(doc.target_directory) || typeof doc.workspace_root !== "string") return { refusal: `cargo metadata in the snapshot reported no absolute target_directory for ${m}${failed(r) ? ` (${why(r)})` : ""}` };
2509
+ workspaces.set(path.resolve(toCheckout(path.resolve(doc.workspace_root))), path.resolve(toCheckout(path.resolve(doc.target_directory))));
1930
2510
  }
1931
- if (workspaces.size > 1) return { refusal: `the commit holds ${workspaces.size} Cargo workspaces (${[...workspaces.keys()].map((w) => posix(path.relative(root, w)) || ".").join(", ")}); one directory cannot serve them all, and right-release's own Cargo calls carry no per-workspace wrapper` };
1932
- return { dir: workspaces.size ? [...workspaces.values()][0] : null };
2511
+ // GB-17: several Cargo workspaces are independent components with their own target caches (GATE-CONTRACT, mixed workspaces never share a target): the per-workspace mapping
2512
+ // (workspace root of the checkout -> its own target directory, all resolved from the commit's snapshot) is returned, never one shared directory and no refusal. The caller binds
2513
+ // each of right-release's and its children's Cargo calls to its own workspace through the per-call wrapper (bindUnitCargo).
2514
+ if (workspaces.size > 1) return { dir: null, workspaces, dirs: [...workspaces.values()] };
2515
+ return { dir: workspaces.size ? [...workspaces.values()][0] : null, workspaces, dirs: [...workspaces.values()] };
1933
2516
  }
1934
2517
 
1935
2518
  /**
@@ -1963,10 +2546,9 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
1963
2546
  const which = (cmd) => { const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]); return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean); };
1964
2547
  const executor = (o.resolveExecutor ?? resolveExecutor)({ env: o.env, platform: o.platform, exists: o.exists, which, readFile: o.readFile }); applyExecutorEnv(o, executor);
1965
2548
  if (executor.kind === "refuse") { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
1966
- if (ds && executor.kind === "broker") { o.err(`${tag} BLOCKED: RK_SNAPSHOT_CARRIER_NOT_WIRED: right-release ${to} reaches Cargo on a broker host only through the snapshot carrier (item 7 row 7.24), which is not wired; nothing ran and the checkout is never built`); return 1; }
1967
2549
  o.out(`${tag} --to ${to} for ${commit.slice(0, 12)}${appDir === "." ? "" : ` (app ${appDir})`}: check (class A) -> right-release ${to}; executor ${executor.kind}${ds ? `; snapshot ${ds.path}` : ""}`);
1968
2550
  // Full class A of the commit before right-release compiles anything: of the snapshot, the checks that execute repository code included.
1969
- const checked = await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : [])], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
2551
+ const checked = await (o.runCheck ?? (() => main(["check", "--commit", ds ? commit : "HEAD", "--full-a", ...(ds ? ["--snapshot", ds.path] : []), ...(appDir === "." ? [] : ["--scope", appDir])], { ...io, cwd: root, ...(ds ? { buildOwnership: ds.own } : {}) })))();
1970
2552
  if (checked !== 0) { o.err(`${tag} --to ${to} stopped at check: nothing compiled`); return 1; }
1971
2553
  // The snapshot is exactly tree + dependencies + the ledger before right-release starts; what it wrote may remain only as ledger
1972
2554
  // entries (a generated `gen/schemas`, a dist), recorded by this gate's own walk (stage-ledger.mjs, decision 2 Q1).
@@ -1974,11 +2556,30 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
1974
2556
  // Cargo would resolve `target/` (or a relative build.target-dir) against the snapshot: its output directory is resolved in the checkout
1975
2557
  // and passed absolute, so the warm outputs stay the checkout's and the snapshot holds no target/ (docs/GATE-CONTRACT.md "Output
1976
2558
  // directory & cache"). One Cargo workspace is pinned as a whole; several would share one directory (refused, naming them).
1977
- const pin = ds ? cargoOutputPin({ o, git, root, commit, executor }) : { dir: null };
2559
+ // A broker host's output is the broker's (the receipts' `outputDir`); only a direct host pins the checkout's target directory.
2560
+ const pin = ds && executor.kind !== "broker" ? await cargoOutputPinForScope({ o, git, root, commit, executor, snapshot: ds.path, ...(appDir === "." ? {} : { scopeDirs: [appDir] }) }) : { dir: null };
1978
2561
  if (pin.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${pin.refusal}; nothing compiled`); return 1; }
1979
2562
  if (pin.dir) o.out(`${tag} output directory for the Cargo workspace: ${pin.dir} (the checkout's)`);
2563
+ // GB-17: several workspaces, each with its own target (never one shared directory): the per-call wrapper binds every Cargo call to its workspace's target, from the snapshot's mapping.
2564
+ let pinMulti = null;
2565
+ if (pin.dirs?.length > 1) {
2566
+ pinMulti = await bindUnitCargo({ o, git, root, ds, executor, which: whichIn(o), ...(appDir === "." ? {} : { scopeDirs: [appDir] }) });
2567
+ if (pinMulti.refusal) { o.err(`${tag} BLOCKED: RK_SNAPSHOT_OUTPUT_UNPINNED: ${pinMulti.refusal}; nothing compiled`); return 1; }
2568
+ if (pinMulti.note) o.out(`${tag} ${pinMulti.note}`);
2569
+ }
1980
2570
  const commonDir = path.resolve(root, common);
1981
2571
  const source = ds ? sourceSplitOf({ root, ds, appDir }) : null;
2572
+ // GB-58 (GATE dev/test exact stage; E19): the selected app's declared sidecars are staged ONCE under this snapshot's ownership before the dependent
2573
+ // build, through the same planner and ledger as the compile and package stages (a Tauri crate with externalBin refuses to build without them). The
2574
+ // app's own prepare step may stage them too; the ledger's attribution check, not an environment marker, is what keeps that single and exact.
2575
+ if (ds) {
2576
+ const sidecarConfigs = committedSidecarConfigs({ o, git, commit, appDir, base: ds.path });
2577
+ const staged = await stageSidecarsOnce({ o, ds, root, git, configs: sidecarConfigs, appDir, cli: fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url)), commonDir, commit, tree, stage: to, executor, tag });
2578
+ if (!staged) { o.err(`${tag} --to ${to} stopped: the declared sidecars could not be staged; nothing compiled`); return 1; }
2579
+ }
2580
+ // right-release dev and test, and the Tauri CLI and every other child they start, reach Cargo only through the stage's carrier on a broker host.
2581
+ const carrier = await carrierForChildren({ o, ds, root, git, executor, stage: to, tag });
2582
+ if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing compiled`); return 1; }
1982
2583
  const run = startRun({ commonDir, commit, tree, stage: to, ...(source ? { source } : {}) });
1983
2584
  try {
1984
2585
  const cli = fileURLToPath(new URL("../cli/right-release.mjs", import.meta.url));
@@ -1987,15 +2588,19 @@ async function runDevTest(o, args, root, git, io, to, appRoot, appDir) {
1987
2588
  // The door's own RightKit runs every sub-step: a dev target's `right-release web-build|stage-sidecar` steps resolve `right-release`
1988
2589
  // on PATH, which found the app's node_modules copy (HeardRight on the Dell, 2026-10-27: the app's 0.6.8 guard refused what the
1989
2590
  // door's 0.6.9 admitted). An installed gate (node_modules/@rightkit/release/bin) has its package's bin shims two levels up.
1990
- const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: source ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: { ...o.env, ...run.env, ...doorPath(o), ...(pin.dir ? { CARGO_TARGET_DIR: pin.dir } : {}) } });
2591
+ const r = (o.spawnInherit ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, stdio: "inherit" })))(process.execPath, [cli, to, ...passthrough], { cwd: source ? snapshotAppRootOf(ds.path, appDir) : appRoot, env: carriedEnv(carrier, { ...o.env, ...run.env }, { ...doorPath({ ...o, env: { ...o.env, ...carrier.env } }), ...(pin.dir ? { CARGO_TARGET_DIR: pin.dir } : (pinMulti?.env ?? {})) }) });
2592
+ const carriedOk = await carrier.finish({ stage: to });
1991
2593
  if (ds && !(await closeSnapshotStage({ o, ds, root, stage: to, step: to, tag })).ok) { o.err(`${tag} --to ${to} stopped: the result covers no known bytes`); return 2; }
1992
- return r.status === 0 ? 0 : 1;
1993
- } finally { endRun(run); }
2594
+ return r.status === 0 && carriedOk ? 0 : 1;
2595
+ } finally { endRun(run); carrier.cleanup(); try { pinMulti?.cleanup?.(); } catch { /* the wrapper map is the gate's own temp directory */ } }
1994
2596
  }
1995
2597
 
1996
2598
  /** A direct (Linux) executor's environment (PATH without RustRight shim directories) for every child this run starts. */
1997
2599
  function applyExecutorEnv(o, executor) {
1998
2600
  if (executor?.env && o.env) Object.assign(o.env, executor.env);
2601
+ // Outside the broker by Adrian's recorded exception: every child's `rustright cargo|rustc|rustdoc` runs direct, never through the installed
2602
+ // client (managed-cargo.mjs NO_BROKER_ENV; it holds only inside this live door run). Without it a child on the Mac or the Dell still reaches the broker.
2603
+ if (o.brokerException && o.env && executor?.kind === "direct") o.env[NO_BROKER_ENV] = o.brokerException.exceptionId;
1999
2604
  if (executor?.note && o.out) o.out(`rightkit-gate: ${executor.note}`);
2000
2605
  }
2001
2606
 
@@ -2016,6 +2621,32 @@ export function doorPath(o, gateFile = o.gateFile ?? fileURLToPath(import.meta.u
2016
2621
  * readStdin, out, err, cwd, env, platform, abort (an AbortController), runScans and findRustfmt (the scan runner and the
2017
2622
  * lookup of a usable rustfmt, which the deferred Rust gate needs).
2018
2623
  */
2624
+ /** The gate flags a `--to <stage>` invocation takes (before a `--` separator, after which the arguments belong to right-release), and whether each takes a value. */
2625
+ const STAGE_VALUE_FLAGS = Object.freeze(["--to", "--platform", "--tier", "--commit", "--receipt-in", "--receipt-out", "--unit", "--units-file", "--all-units-exception"]);
2626
+ const STAGE_BOOLEAN_FLAGS = Object.freeze(["--route"]);
2627
+
2628
+ /**
2629
+ * Why `args` (a `--to <stage>` invocation) is not a well-formed request, or null. Every value flag needs a value that is not another flag and may be
2630
+ * given once; `--flag=value`, unknown flags and stray words are refused. The arguments after a `--` separator are right-release's and are not read.
2631
+ */
2632
+ export function stageArgsProblem(args) {
2633
+ const at = args.indexOf("--");
2634
+ const gate = at < 0 ? args : args.slice(0, at);
2635
+ const seen = new Set();
2636
+ for (let i = 0; i < gate.length; i += 1) {
2637
+ const a = gate[i];
2638
+ if (STAGE_BOOLEAN_FLAGS.includes(a)) continue;
2639
+ if (STAGE_VALUE_FLAGS.includes(a)) {
2640
+ const value = gate[i + 1];
2641
+ if (value === undefined || value.startsWith("--")) return a === "--to" ? `--to needs a stage (${[...STAGES, "dev", "test"].join("|")})` : `${a} needs a value`;
2642
+ if (seen.has(a)) return `${a} was given twice`;
2643
+ seen.add(a); i += 1; continue;
2644
+ }
2645
+ return a.startsWith("-") ? `unknown option ${JSON.stringify(a)} (a value follows its flag as the next word: \`--commit <rev>\`, not \`--commit=<rev>\`)` : `unexpected argument ${JSON.stringify(a)}`;
2646
+ }
2647
+ return null;
2648
+ }
2649
+
2019
2650
  export async function main(argv, io = {}) {
2020
2651
  const o = {
2021
2652
  spawn: defaultSpawn, exists: fs.existsSync, readFile: (f) => fs.readFileSync(f, "utf8"), writeFile: fs.writeFileSync, mkdir: fs.mkdirSync, chmod: fs.chmodSync, readdir: fs.readdirSync,
@@ -2028,6 +2659,8 @@ export async function main(argv, io = {}) {
2028
2659
  const option = (name) => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
2029
2660
  // `clippy`: the Clippy route (../clippy-route.mjs; docs/GATE-CONTRACT.md §1 "Clippy route & batching"). Dispatched before the global
2030
2661
  // flags below because everything after `clippy` is cargo's own argv, verbatim (`-v`, `-h` and `--version` are cargo's).
2662
+ // `candidate-receipt` (GB-73): the candidate build receipt the RustRight installer takes in place of a clean primary checkout.
2663
+ if (args[0] === "candidate-receipt") return (await import("../candidate-receipt-command.mjs")).candidateReceiptCommand(o, args.slice(1));
2031
2664
  if (args[0] === "clippy") return (await import("../clippy-route.mjs")).clippyCommand(o, args.slice(1), { main, io, absoluteEnv: SNAPSHOT_ABSOLUTE_ENV });
2032
2665
  if (args.includes("--version") || args.includes("-v")) {
2033
2666
  o.out(JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
@@ -2052,6 +2685,42 @@ export async function main(argv, io = {}) {
2052
2685
  // class A scan (lockfiles, cargo-deny, cargo-audit, secrets, licences, manifests, workflow lint, syntax...) over the whole tree of the
2053
2686
  // commit, never a diff. Nothing compiles. The RustRight broker runs exactly this before it admits any compile. `--list`,
2054
2687
  // `--print-baseline` and `--only` stay app-check-only views.
2688
+ // `check --commit <sha>` without `--snapshot` reads the gate's one warm snapshot of that commit, selected the way
2689
+ // every `--to` stage selects it: host-wide build ownership first, then one sync of the commit into the gate-owned source directory, then the
2690
+ // check of that snapshot below. There is no export and no copy of the source (docs/GATE-CONTRACT.md, 2026-10-29); the checkout's own files are
2691
+ // never read, so an uncommitted edit in it is ignored. Hosted CI has no warm snapshot: its runner checkout is the commit, scanned in place.
2692
+ // `check --commit <sha> --full-a` takes the same route (GB-48, GATE 34/36/44, E17): the selected commit's snapshot, never a conversion to
2693
+ // `--worktree` that reads the live checkout after a momentary cleanliness check. Only hosted CI keeps the in-place full check.
2694
+ const commitCheck = sub === "check"
2695
+ && !["--snapshot", "--worktree", "--plan", "--list", "--print-baseline", "--only"].some((f) => args.includes(f)) && o.classA !== false && snapshotRouteSelected(o);
2696
+ if (commitCheck) {
2697
+ const sc = await import("../snapshot-check.mjs");
2698
+ const snap = await import("../snapshot.mjs");
2699
+ const { acquireBuildOwnership, ownershipFile } = await import("../build-ownership.mjs");
2700
+ const at = option("--repo") ? path.resolve(o.cwd, option("--repo")) : o.cwd;
2701
+ const top0 = o.spawn("git", ["rev-parse", "--show-toplevel"], { cwd: at });
2702
+ if (failed(top0) || !text(top0).trim()) { o.err("rightkit-gate: not inside a git repository"); return 2; }
2703
+ let own = o.buildOwnership ?? null; let ownHere = false;
2704
+ try {
2705
+ const checkout = fs.realpathSync.native(path.resolve(text(top0).trim()));
2706
+ const selected = sc.commitOf(checkout, option("--commit") ?? "HEAD", o.spawn);
2707
+ const snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
2708
+ if (!own) {
2709
+ try { own = (o.acquireOwnership ?? acquireBuildOwnership)({ file: ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: "check", snapshot: path.join(snapshotsAt, snap.repositoryOf(checkout).key), commit: selected.commit, platform: o.platform, env: o.env }); ownHere = true; }
2710
+ catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`rightkit-gate: RK_CHECK_ERROR: ${e.message}; nothing ran${e.code === "refused" ? ", nothing created" : ""}`); return 2; }
2711
+ o.out(`rightkit-gate: build ownership ${own.joined ? "joined" : "held"} ${own.handle}${own.reclaimed ? ` (reclaimed from ${own.reclaimed.from}: ${own.reclaimed.proofs.join("; ")})` : ""}`);
2712
+ }
2713
+ let synced;
2714
+ try { synced = (o.syncSnapshot ?? snap.syncSnapshot)({ checkout, rev: selected.commit, root: snapshotsAt }); }
2715
+ catch (e) { o.err(`rightkit-gate: ${e.code === "error" ? sc.CHECK_ERROR : "BLOCKED"}: snapshot ${e.message}; nothing ran`); return e.code === "error" ? 2 : 1; }
2716
+ o.out(`rightkit-gate: snapshot ${synced.path} is commit ${synced.commit} tree ${synced.tree} (written=${synced.written} removed=${synced.removed} unchanged=${synced.unchanged})`);
2717
+ const next = [...argv]; const ci = next.indexOf("--commit");
2718
+ if (ci >= 0) next.splice(ci, 2, "--commit", synced.commit); else next.push("--commit", synced.commit);
2719
+ next.push("--snapshot", synced.path);
2720
+ return await main(next, { ...io, buildOwnership: own });
2721
+ } catch (e) { if (e instanceof sc.CheckError) { o.err(`rightkit-gate: ${sc.CHECK_ERROR}: ${e.message}`); return 2; } throw e; }
2722
+ finally { if (ownHere) { const r = own.release(); if (r?.released === false && !own.joined) o.err(`rightkit-gate: build ownership ${own.handle} kept: ${r.why}`); } }
2723
+ }
2055
2724
  if ((sub === "check" || (!sub && args.includes("--all"))) && args.includes("--snapshot") && !o.buildOwnership) {
2056
2725
  // Host-wide build ownership (../build-ownership.mjs; GATE-CONTRACT §1): held from before the first snapshot read through the
2057
2726
  // end of the check, so no other request can sync the snapshot while class A runs in it. A nested gate whose
@@ -2091,9 +2760,11 @@ export async function main(argv, io = {}) {
2091
2760
  // and package compile on the compile-tier rules, qualify and release name the release tier (rightkit#1296).
2092
2761
  const fullAdmission = args.includes("--full-a") && !args.includes("--list") && !args.includes("--print-baseline") && !args.includes("--only");
2093
2762
  const stripSnapshot = (list) => { const i = list.indexOf("--snapshot"); return i < 0 ? list : [...list.slice(0, i), ...list.slice(i + 2)]; };
2094
- const appArgs = stripSnapshot(args.slice(1).filter((a) => a !== "--full-a"));
2763
+ const scopeForward = args.flatMap((a, i) => a === "--scope" ? [a, args[i + 1]] : []);
2764
+ const withoutScope = args.slice(1).filter((a, i, list) => a !== "--scope" && list[i - 1] !== "--scope");
2765
+ const appArgs = stripSnapshot(withoutScope.filter((a) => a !== "--full-a"));
2095
2766
  if (fullAdmission && !appArgs.includes("--rule-tier")) appArgs.push("--rule-tier", requestedTier);
2096
- const appCheck = await (o.appCheck ?? (await import("../app-check.mjs")).main)(appArgs, { out: o.out, err: o.err, label: "check", cwd: o.cwd });
2767
+ const appCheck = await (o.appCheck ?? (await import("../app-check.mjs")).main)(appArgs, { out: o.out, err: o.err, label: "check", cwd: o.cwd, ...(snapshotDir && !args.includes("--worktree") ? { snapshot: snapshotDir } : {}) });
2097
2768
  if (args.includes("--list") || args.includes("--print-baseline") || args.includes("--only") || o.classA === false) {
2098
2769
  // A view of the app rules, never a class A verdict: say so, so no caller can take its exit 0 for a full check.
2099
2770
  // A green partial view exits PARTIAL_EXIT (3), never 0, so a caller that admits only exit 0 rejects it by status alone.
@@ -2109,7 +2780,7 @@ export async function main(argv, io = {}) {
2109
2780
  const checkCwd = repoArg ? path.resolve(o.cwd, repoArg) : o.cwd;
2110
2781
  // The app rules' tier is the stage's (rightkit#1296); class A's deterministic scans are the same at either tier.
2111
2782
  const ruleTier = requestedTier;
2112
- const classA = await main(["--class-a", "--rule-tier", ruleTier, ...(args.includes("--full-a") ? ["--full-a"] : []), ...(args.includes("--worktree") ? ["--worktree"] : ["--commit", snapshotCommit?.commit ?? option("--commit") ?? "HEAD"]), ...(snapshotDir ? ["--snapshot", snapshotDir] : [])], { ...io, cwd: checkCwd });
2783
+ const classA = await main(["--class-a", "--rule-tier", ruleTier, ...(args.includes("--full-a") ? ["--full-a"] : []), ...(args.includes("--worktree") ? ["--worktree"] : ["--commit", snapshotCommit?.commit ?? option("--commit") ?? "HEAD"]), ...(snapshotDir ? ["--snapshot", snapshotDir] : []), ...scopeForward], { ...io, cwd: checkCwd });
2113
2784
  // RightKit checking itself: its own rules (line endings, consumer recipes, npm surface, version copies and the version feed, dispatcher
2114
2785
  // pins, consumer locks, generated docs, temp paths: scripts/preflight.mjs) and its exports' import safety are class A for the RightKit repository.
2115
2786
  let selfRules = 0;
@@ -2123,7 +2794,7 @@ export async function main(argv, io = {}) {
2123
2794
  // In snapshot mode RightKit's own scripts are the snapshot's (the commit's code), never the live checkout's.
2124
2795
  const selfRoot = snapshotDir ?? root;
2125
2796
  const rightkitRepository = root && (() => {
2126
- try { return JSON.parse(o.readFile(path.join(selfRoot, "packages", "release", "package.json"))).name === "@rightkit/release"; }
2797
+ try { return isRightKitIdentity(o.readFile(path.join(selfRoot, ...RIGHTKIT_IDENTITY_FILE.split("/")))); }
2127
2798
  catch { return false; }
2128
2799
  })();
2129
2800
  if (rightkitRepository && (!o.exists(path.join(selfRoot, "scripts", "preflight.mjs")) || !o.exists(path.join(selfRoot, "scripts", "ci", "exports-import-effects.mjs")))) {
@@ -2137,7 +2808,7 @@ export async function main(argv, io = {}) {
2137
2808
  const r = launch(process.execPath, [path.join(snapshotDir, "scripts", "preflight.mjs"), "--repo", root, "--commit", snapshotCommit.commit], { cwd: root, env: o.env });
2138
2809
  selfRules = r.status === 0 ? 0 : 1;
2139
2810
  const effects = path.join(snapshotDir, "scripts", "ci", "exports-import-effects.mjs");
2140
- if (selfRules === 0 && o.exists(effects)) {
2811
+ if (selfRules === 0 && o.exists(effects) && scopeForward.length === 0) {
2141
2812
  const common = o.spawn("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd: root });
2142
2813
  const report = path.join(failed(common) ? path.join(root, ".git") : text(common).trim(), "rightkit-gate", "import-effects", `snapshot-${snapshotCommit.commit}.json`);
2143
2814
  const e = launch(process.execPath, [effects, "--root", snapshotDir, "--no-export", "--report", report], { cwd: snapshotDir, env: o.env });
@@ -2206,6 +2877,7 @@ export async function main(argv, io = {}) {
2206
2877
  // directory. A direct host only, until the broker's snapshot carrier lands (step 4d).
2207
2878
  const snapshotB = !classAOnly && args.includes("--all") ? (option("--snapshot") ?? null) : null;
2208
2879
  if (snapshotB && !option("--commit")) { o.err(`${tag} --all --snapshot <dir> needs --commit <sha>`); return 2; }
2880
+ let hostedCommitA = null;
2209
2881
  if (fullA && !args.includes("--worktree") && !snapshotA) {
2210
2882
  // A commit is checked in full only where its bytes are the live checkout: it is HEAD and nothing tracked or untracked differs
2211
2883
  // (the unit and app stages already refuse a dirty tree). Then the working tree is that commit, and the checks run there.
@@ -2214,16 +2886,32 @@ export async function main(argv, io = {}) {
2214
2886
  const dirty = at("status", "--porcelain", "--untracked-files=all");
2215
2887
  if (failed(want) || failed(head) || text(want).trim() !== text(head).trim()) { o.err("rightkit-gate: --full-a checks a commit only when it is HEAD of this checkout: the repository's checks run on the live tree"); return 2; }
2216
2888
  if (failed(dirty) || text(dirty).trim()) { o.err("rightkit-gate: --full-a on a commit needs a clean checkout (the live tree must be exactly that commit); commit or remove the changes"); return 2; }
2217
- const i = args.indexOf("--commit"); if (i >= 0) args.splice(i, 2);
2218
- args.push("--worktree");
2889
+ if (!snapshotRouteSelected(o)) {
2890
+ const tree = at("rev-parse", "--verify", `${text(want).trim()}^{tree}`);
2891
+ if (failed(tree) || !text(tree).trim()) { o.err("rightkit-gate: RK_CHECK_ERROR: cannot identify hosted CI commit tree"); return 2; }
2892
+ hostedCommitA = { commit: text(want).trim(), tree: text(tree).trim() };
2893
+ const i = args.indexOf("--commit"); if (i >= 0) args.splice(i, 2, "--commit", hostedCommitA.commit); else args.push("--commit", hostedCommitA.commit);
2894
+ } else {
2895
+ const i = args.indexOf("--commit"); if (i >= 0) args.splice(i, 2);
2896
+ args.push("--worktree");
2897
+ }
2219
2898
  }
2220
2899
  const classACommit = classAOnly && !args.includes("--worktree") ? (option("--commit") ?? "HEAD") : null;
2900
+ const scopeDirs = args.flatMap((a, i) => a === "--scope" ? [args[i + 1]] : []);
2901
+ if (scopeDirs.length) {
2902
+ const bad = scopeDirs.find((d) => typeof d !== "string" || !d || d.startsWith("--") || path.isAbsolute(d) || /^[A-Za-z]:/.test(d) || /(^|[\\/])\.\.([\\/]|$)/.test(d));
2903
+ if (!fullA || (!snapshotA && !hostedCommitA) || bad !== undefined || scopeDirs.some((d) => d === undefined)) { o.err("rightkit-gate: --scope needs full Class A of a snapshot or committed hosted CI checkout & repository-relative directories"); return 2; }
2904
+ }
2221
2905
  if (args.includes("--base") && (!option("--base") || option("--base").startsWith("--"))) { o.err("rightkit-gate: --base needs a ref"); return 2; }
2222
2906
 
2223
2907
  const top = o.spawn("git", ["rev-parse", "--show-toplevel"], { cwd: o.cwd });
2224
2908
  if (failed(top) || !text(top).trim()) { o.err("rightkit-gate: not inside a git repository"); return 2; }
2225
2909
  const root = path.resolve(text(top).trim());
2226
2910
  const git = (...a) => o.spawn("git", a, { cwd: root });
2911
+ if (hostedCommitA) {
2912
+ try { await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "before class A discovery" }); }
2913
+ catch (error) { o.err(`rightkit-gate: RK_CHECK_ERROR: ${error.message}; nothing scanned`); return 2; }
2914
+ }
2227
2915
  if (sub === "install-hook") return await installHook(o, root);
2228
2916
  // `env-digest`: only the environment identity a full class A verdict depends on (classAEnvironmentDigest), no scans. The broker
2229
2917
  // recomputes it when a queued compile starts and re-runs the check if it changed since the verdict.
@@ -2253,16 +2941,81 @@ export async function main(argv, io = {}) {
2253
2941
  // `--unit` is a gate flag only before a `--` separator: after it the arguments belong to right-release (an app's `--to dev -- …`).
2254
2942
  const separator = args.indexOf("--");
2255
2943
  const unitFlag = args.includes("--unit") && (separator < 0 || args.indexOf("--unit") < separator);
2944
+ // RightKit names its unit on every run (rightkitDoorRule): `--unit <selector>`, `--units-file <json written for this run>` or
2945
+ // `--all-units-exception <id>`; exactly one. The unit list is read here, before anything runs, so a bad file is a usage error (exit 2).
2946
+ const gateFlag = (name) => args.includes(name) && (separator < 0 || args.indexOf(name) < separator);
2947
+ const unitsFileFlag = gateFlag("--units-file"); const allUnitsFlag = gateFlag("--all-units-exception");
2948
+ let unitList = null;
2949
+ if (unitsFileFlag || allUnitsFlag) {
2950
+ const flag = unitsFileFlag ? "--units-file" : "--all-units-exception";
2951
+ if (option("--to") === undefined) { o.err(`rightkit-gate: ${flag} applies to a --to stage\n${USAGE}`); return 2; }
2952
+ if ([unitFlag, unitsFileFlag, allUnitsFlag].filter(Boolean).length > 1) { o.err("rightkit-gate: name the unit once: --unit, --units-file or --all-units-exception, not several"); return 2; }
2953
+ if (!option(flag) || option(flag).startsWith("--")) { o.err(`rightkit-gate: ${flag} needs a value\n${USAGE}`); return 2; }
2954
+ if (unitsFileFlag) {
2955
+ try { unitList = readUnitList((f) => o.readFile(path.resolve(o.cwd, f)), option("--units-file")); }
2956
+ catch (e) { o.err(`rightkit-gate: ${e.message}`); return 2; }
2957
+ }
2958
+ }
2959
+ // `--no-broker <exception-id>`: the one documented exception to the mandatory broker on Mac and Dell (broker-exception.mjs). The record is
2960
+ // read from the selected commit, never the working tree; every refusal is typed and nothing runs. The run then takes the same stages
2961
+ // with the direct executor, and prints and records the exception.
2962
+ const noBrokerAt = args.indexOf("--no-broker");
2963
+ if (noBrokerAt >= 0 && (separator < 0 || noBrokerAt < separator)) {
2964
+ const brokerId = args[noBrokerAt + 1];
2965
+ if (option("--to") === undefined) { o.err(`rightkit-gate: ${BROKER_EXCEPTION_ERRORS.usage}: --no-broker <exception-id> applies to a --to stage\n${USAGE}`); return 2; }
2966
+ args.splice(noBrokerAt, typeof brokerId === "string" && !brokerId.startsWith("--") ? 2 : 1);
2967
+ let granted;
2968
+ try {
2969
+ let record = null; let inRightKit = true;
2970
+ if (typeof brokerId === "string" && !brokerId.startsWith("--")) {
2971
+ const pinned = (await import("../snapshot.mjs")).resolveCommit(root, option("--commit") ?? "HEAD");
2972
+ const shown = git("show", `${pinned.commit}:${BROKER_EXCEPTIONS_FILE}`);
2973
+ record = failed(shown) ? null : text(shown);
2974
+ // The repository identity of the commit decides what an exception must name: RightKit's units, or (every other repository, built whole) nothing.
2975
+ inRightKit = rightkitIdentityAt(git, pinned.commit);
2976
+ }
2977
+ // A unit list run covers every listed unit; a run that names no unit (the whole repository) selects every unit.
2978
+ const selected = unitFlag ? unitsOfSelector(option("--unit")) : unitList ? unitList.flatMap((u) => unitsOfSelector(u)) : unitsOfSelector(undefined);
2979
+ granted = authorizeNoBroker({ id: brokerId, record, platform: o.platform, units: selected, now: o.now ? o.now() : Date.now(), inRightKit });
2980
+ } catch (e) {
2981
+ if (!e.code?.startsWith("RK_BROKER_EXCEPTION_")) { o.err(`rightkit-gate: cannot resolve ${option("--commit") ?? "HEAD"} to a commit: ${e.message}`); return 2; }
2982
+ o.err(`rightkit-gate: BLOCKED: ${e.message}; nothing ran`); return e.code === BROKER_EXCEPTION_ERRORS.usage ? 2 : 1;
2983
+ }
2984
+ const brokerResolver = o.resolveExecutor ?? resolveExecutor;
2985
+ o.resolveExecutor = (a) => brokerResolver({ ...a, forceDirect: true });
2986
+ o.brokerException = granted;
2987
+ io = { ...io, resolveExecutor: o.resolveExecutor, brokerException: granted };
2988
+ o.out(`rightkit-gate: BROKER EXCEPTION ${describeBrokerException(granted)}`);
2989
+ }
2990
+ // The door's stage arguments are read strictly (E2E L3-02): a flag with no value, `--flag=value`, an unknown flag, a repeated value flag or a stray
2991
+ // word is refused before git, ownership or any process, never ignored (`--commit=<old sha>` used to build HEAD, a trailing `--to` ran the default gate).
2992
+ // A unit run (`--unit`) validates its own, narrower vocabulary below ("unit stages take only ...").
2993
+ if (!sub && !unitFlag && args.some((a) => a === "--to" || a.startsWith("--to="))) {
2994
+ const problem = stageArgsProblem(args);
2995
+ if (problem) { o.err(`rightkit-gate: ${problem}\n${USAGE}`); return 2; }
2996
+ }
2997
+ // `--route` is a gate flag only before a `--` separator (after it the arguments belong to right-release). It answers which route a
2998
+ // stage takes and runs nothing, so it takes no unit, receipt or release stage (rightkit#1297).
2999
+ const gateArgs = separator < 0 ? args : args.slice(0, separator);
3000
+ if (gateArgs.includes("--route")) {
3001
+ const routeTo = option("--to");
3002
+ const clash = ["--unit", "--units-file", "--all-units-exception", "--receipt-in", "--receipt-out"].filter((f) => gateArgs.includes(f));
3003
+ if (routeTo === undefined) { o.err(`rightkit-gate: --route needs --to ${ROUTE_PROBE_STAGES.join("|")}\n${USAGE}`); return 2; }
3004
+ if (routeTo === "release") { o.err("rightkit-gate: --route answers for --to check|compile|package|qualify|dev|test; release uploads and has no route of its own"); return 2; }
3005
+ if (clash.length) { o.err(`rightkit-gate: --route runs nothing and takes no ${clash.join(" or ")}`); return 2; }
3006
+ o.routeProbe = true;
3007
+ }
2256
3008
  if (option("--to") !== undefined && unitFlag) {
2257
3009
  const selector = option("--unit");
2258
3010
  const validArgs = args.length % 2 === 0 && args.every((a, i) => i % 2 ? !a.startsWith("--") : ["--to", "--unit", "--receipt-out", "--commit"].includes(a)) && ["--to", "--unit", "--receipt-out", "--commit"].every((flag) => args.filter((a) => a === flag).length <= 1);
2259
3011
  if (!validArgs || !selector) {
2260
3012
  o.err("rightkit-gate: unit stages take only --to, --unit and optional --receipt-out and --commit"); return 2;
2261
3013
  }
2262
- // Row 7.06a: on the snapshot route (Linux now; Mac and Dell at the cutover) a unit stage builds the selected commit's snapshot.
2263
- if (!o.doorSnapshot && o.env.GITHUB_ACTIONS !== "true" && snapshotRouteSelected(o)) return await runUnitSnapshot(o, args, root, git, io, option);
2264
- if (option("--commit") !== undefined) { o.err("rightkit-gate: BLOCKED: --commit selects a commit for the snapshot route; on this host a unit stage takes HEAD of a clean checkout until the cutover"); return 2; }
2265
- const head = git("rev-parse", "HEAD"), treeResult = git("rev-parse", "HEAD^{tree}"), common = git("rev-parse", "--git-common-dir"), dirty = git("status", "--porcelain", "--untracked-files=no");
3014
+ // Row 7.06a: on every host but hosted CI a unit stage builds the selected commit's snapshot (Linux direct, Mac and Dell through the carrier).
3015
+ if (!o.doorSnapshot && snapshotRouteSelected(o)) return await runUnitSnapshot(o, args, root, git, io, option);
3016
+ // Hosted CI only: the runner's checkout is the commit.
3017
+ if (option("--commit") !== undefined) { o.err("rightkit-gate: BLOCKED: --commit selects a commit for the snapshot route; hosted CI builds the runner's checkout, which is the commit"); return 2; }
3018
+ const head = git("rev-parse", "HEAD"), treeResult = git("rev-parse", "HEAD^{tree}"), common = git("rev-parse", "--git-common-dir"), dirty = git("status", "--porcelain", "--untracked-files=all");
2266
3019
  const commit = text(head).trim(), tree = text(treeResult).trim();
2267
3020
  if (failed(head) || failed(treeResult) || failed(common) || !text(common).trim() || !/^[0-9a-f]{40}$/.test(commit) || !/^[0-9a-f]{40}$/.test(tree) || failed(dirty) || text(dirty).trim()) { o.err("rightkit-gate: BLOCKED: unit stages need clean committed HEAD and tree"); return 1; }
2268
3021
  const match = /^(?:npm:@rightkit\/([a-z0-9-]+)|cargo:(rightkit-[a-z0-9-]+))$/.exec(selector);
@@ -2276,12 +3029,22 @@ export async function main(argv, io = {}) {
2276
3029
  // node_modules does not carry; the app paths of this gate must keep loading there (app-check.test: the installed copy).
2277
3030
  const { runUnitGate } = await import("../unit-gate.mjs");
2278
3031
  return await runUnitGate({ selector, to: option("--to"), root, commonDir: path.resolve(root, text(common).trim()), commit, tree, executor, env: o.env, platform: o.platform,
2279
- check: (tier) => main(["check", "--commit", "HEAD", "--full-a", "--rule-tier", tier], { ...io, cwd: root }), out: o.out, err: o.err,
3032
+ check: async (tier) => {
3033
+ await verifyCommittedCheckout({ git, root, commit, tree, when: "before unit:check" });
3034
+ const result = await main(["check", "--commit", commit, "--full-a", "--rule-tier", tier, "--scope", path.posix.dirname(manifest)], { ...io, cwd: root });
3035
+ await verifyCommittedCheckout({ git, root, commit, tree, when: "after unit:check" });
3036
+ return result;
3037
+ }, out: o.out, err: o.err,
2280
3038
  ...(reportOut ? { writeReport: (report) => o.writeFile(path.resolve(o.cwd, reportOut), `${JSON.stringify(report, null, 2)}\n`) } : {}),
2281
3039
  });
2282
3040
  }
2283
3041
  if (unitFlag) { o.err("rightkit-gate: --unit requires --to"); return 2; }
2284
- if (option("--to") !== undefined) return await runStages(o, args, root, git, io);
3042
+ if (option("--to") !== undefined) {
3043
+ // A door run that names no unit: RightKit refuses it unless Adrian's exception or a unit list names what runs (rightkitDoorRule).
3044
+ const door = await rightkitDoorRule(o, args, root, git, io, { unitsFileFlag, allUnitsFlag, unitList });
3045
+ if (door.exit !== undefined) return door.exit;
3046
+ return await runStages(o, args, root, git, io);
3047
+ }
2285
3048
  if (sub === "stage") return stageForPublisher(o, args, root, git);
2286
3049
 
2287
3050
  const planOnly = args.includes("--plan");
@@ -2323,6 +3086,16 @@ export async function main(argv, io = {}) {
2323
3086
  // A broker-managed host whose broker is unreachable refuses the moment a Cargo manifest is in the snapshot (below): a repository
2324
3087
  // with no Rust owes nothing to the broker (a git hook's minimal PATH, a JS-only app), and the class A scans never compile.
2325
3088
  const cargoCommand = executor.cargo ?? ["cargo"];
3089
+ // The declared Linux broker route's cargo is `[<absolute validated client>, "cargo"]`, which the scan planner (planScans: `["rustright","cargo"]` or
3090
+ // `["cargo"]`) refuses, and a PATH `rustright` is never the client. So the scans are PLANNED with the logical `["rustright","cargo"]` and every spawn the
3091
+ // gate hands the scans, the discovery of the snapshot's workspaces and the tool probes resolves that one name to the executor's validated client,
3092
+ // with the endpoint the executor declared (`withDeclaredClient`); nothing starts a `rustright` found on PATH. The carried requests
3093
+ // (snapshotCargoSpawn's carrySpawn, the snapshot carrier) name the client by absolute path already. Every other route plans `cargoCommand` itself.
3094
+ const declaredClient = executor.kind === "broker" && executor.route === LINUX_BROKER_ROUTE && typeof executor.client === "string" ? executor.client : null;
3095
+ const planCargoCommand = declaredClient ? ["rustright", ...cargoCommand.slice(1)] : cargoCommand;
3096
+ const withDeclaredClient = (spawn) => (declaredClient
3097
+ ? (command, args, opts = {}) => (command === "rustright" ? spawn(declaredClient, args, { ...opts, env: { ...(opts.env ?? o.env), ...(executor.env ?? {}) } }) : spawn(command, args, opts))
3098
+ : spawn);
2326
3099
  // A broker host (Mac, Dell) compiles a commit snapshot through the RustRight snapshot carrier: `deps.snapshotBroker`, built in the
2327
3100
  // snapshot-compile region below (item 7 rows 7.20 to 7.24). A broker-managed host whose broker cannot be reached is refused with
2328
3101
  // the executor's own problem, never compiled around.
@@ -2339,11 +3112,9 @@ export async function main(argv, io = {}) {
2339
3112
  }
2340
3113
 
2341
3114
  const ac = o.abort ?? new AbortController();
2342
- let exportRoot = null;
2343
3115
  let snapshotGitConfig = null;
2344
3116
  let cargoShimDir = null;
2345
3117
  const cleanup = () => {
2346
- if (exportRoot) fs.rmSync(exportRoot, { recursive: true, force: true });
2347
3118
  if (cargoShimDir) fs.rmSync(cargoShimDir, { recursive: true, force: true });
2348
3119
  if (snapshotGitConfig) { fs.rmSync(snapshotGitConfig, { force: true }); try { fs.rmdirSync(path.dirname(snapshotGitConfig)); } catch { /* not empty: leave it, the next exact sync prunes it */ } }
2349
3120
  };
@@ -2351,6 +3122,14 @@ export async function main(argv, io = {}) {
2351
3122
  try {
2352
3123
  let repo = root;
2353
3124
  let snapshotBCommit = null;
3125
+ // On a broker host the gate's snapshot is the broker's managed one, where RustRight refuses a root request ("a root request cannot run inside a
3126
+ // managed snapshot", rightkit#1311): the gate's own Cargo reads there (unit discovery, workspace members) are carried read-only requests,
3127
+ // with the broker's signed receipt verified (snapshot-metadata.mjs). The checkout, git and a direct host keep the plain spawn.
3128
+ let snapshotSpawn = withDeclaredClient((c, a, opts) => o.spawn(c, a, opts));
3129
+ let discoveryBroker = null;
3130
+ // On a broker host, the spawn every Class A Cargo scan of the snapshot runs through: a Cargo call inside the snapshot is a carried read-only
3131
+ // request of one of the exact admitted shapes (snapshot-metadata.mjs), anything else passes through (node, git, the client's probe).
3132
+ let scanSpawn = null;
2354
3133
  if (snapshot && snapshotB) {
2355
3134
  // Direct host: no broker grant to satisfy, so no config-only .git; the snapshot is scanned and compiled in place.
2356
3135
  repo = snapshotB;
@@ -2368,27 +3147,14 @@ export async function main(argv, io = {}) {
2368
3147
  o.writeFile(snapshotGitConfig, `[remote "origin"]\n\turl = ${originUrl}\n`);
2369
3148
  }
2370
3149
  } else if (snapshot) {
2371
- exportRoot = fs.mkdtempSync(path.join(exportTmpRoot({ env: o.env, platform: o.platform, exists: o.exists }), "rk-gate-"));
2372
- repo = path.join(exportRoot, "repo");
2373
- // Every file of the commit or tree: read-tree into a private index, then checkout-index. `git archive` would leave out files
2374
- // marked export-ignore in .gitattributes, so they would never be scanned; this export has no such exclusion.
2375
- const indexEnv = { ...o.env, GIT_INDEX_FILE: path.join(exportRoot, "index") };
2376
- const made = o.spawn("git", ["read-tree", snapshot], { cwd: root, env: indexEnv });
2377
- // git builds each path as <prefix><index path> with "/" separators and creates only the directories below the prefix, so the
2378
- // export root must exist and the prefix use "/": a "C:\\...\\repo\\" prefix made checkout-index fail on Windows
2379
- // (publish run 40282215043: "cannot create directory at ...\\repo\\.github").
2380
- fs.mkdirSync(repo, { recursive: true });
2381
- const x = failed(made) ? made : o.spawn("git", ["checkout-index", "--all", "--force", `--prefix=${repo.split(path.sep).join("/")}/`], { cwd: root, env: indexEnv });
2382
- if (failed(made) || failed(x)) { o.err(`${tag} cannot export ${snapshot}: ${why(made) || why(x)}`); return 2; }
2383
- // rustright binds the tracked .rightkit-local-build.json grant to the origin URL in .git/config (rustright-policy local_build.rs),
2384
- // and the export has no .git, so cargo metadata in it was refused on every broker host and no push passed the hook (rightkit#1215).
2385
- // A config-only .git carries exactly the source checkout's origin and is not a repository to git (no HEAD, no objects), so nothing
2386
- // can run git against the export. No origin (a checkout without a remote): nothing is written and rustright decides as before.
2387
- const origin = o.spawn("git", ["config", "--get", "remote.origin.url"], { cwd: root });
2388
- const originUrl = failed(origin) ? "" : text(origin).trim();
2389
- if (originUrl) {
2390
- o.mkdir(path.join(repo, ".git"), { recursive: true });
2391
- o.writeFile(path.join(repo, ".git", "config"), `[remote "origin"]\n\turl = ${originUrl}\n`);
3150
+ // No export, no copy of the source (docs/GATE-CONTRACT.md, 2026-10-29): a host that has the gate's warm snapshot reads the commit
3151
+ // through it (`check --commit` selects it above, `--to` stages hand it over). What reaches here is a run whose checkout IS the
3152
+ // commit: hosted CI's runner checkout, or HEAD for the legacy gate run. It is scanned in place, only after proving it is that commit.
3153
+ if (classAOnly && snapshotRouteSelected(o)) { o.err(`${tag} BLOCKED: class A of a commit reads the gate's warm snapshot of it, never the checkout; run \`rightkit-gate check --commit <sha>\` (it selects the snapshot) or pass \`--snapshot <dir>\``); return 2; }
3154
+ const want = git("rev-parse", "--verify", `${snapshot}^{commit}`); const head = git("rev-parse", "--verify", "HEAD^{commit}");
3155
+ if (snapshot !== "HEAD" && (failed(want) || failed(head) || text(want).trim() !== text(head).trim())) {
3156
+ o.err(`${tag} BLOCKED: ${snapshot} is not HEAD of this checkout, and this run has no gate-owned snapshot of it: only the commit the checkout already is can be scanned in place (hosted CI's runner checkout); check another commit with \`check --commit <sha>\` on a host with the gate's warm snapshot`);
3157
+ return 2;
2392
3158
  }
2393
3159
  }
2394
3160
  // Every file of the snapshot, once: units, manifests, lockfiles and the changed set are all read from what is scanned, never from
@@ -2399,7 +3165,7 @@ export async function main(argv, io = {}) {
2399
3165
  const tracked = (name) => files.filter((f) => f === name || f.endsWith(`/${name}`));
2400
3166
  const readSnap = (f) => o.readFile(f);
2401
3167
 
2402
- let rust; let npm; let npmRoots = []; let manifests = [];
3168
+ let rust; let npm; let npmRoots = []; let installRoots = []; let manifests = []; let closure = null; let closureTools = null;
2403
3169
  try {
2404
3170
  manifests = tracked("Cargo.toml");
2405
3171
  if (executor.kind === "refuse" && manifests.length) { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
@@ -2413,11 +3179,63 @@ export async function main(argv, io = {}) {
2413
3179
  const r = git("ls-files", "-z", "--others", "--exclude-standard", "--", ":(glob)**/Cargo.toml");
2414
3180
  return failed(r) ? [] : text(r).split("\0").filter(Boolean).map(posix);
2415
3181
  };
2416
- rust = discoverRustWorkspaces({ root: repo, spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, tracked, untracked, fixtures, readFile: readSnap });
3182
+ if (executor.kind === "broker" && snapshot && (snapshotA || snapshotB) && manifests.length) {
3183
+ const { snapshotCargoSpawn } = await import("../snapshot-metadata.mjs");
3184
+ const { createSnapshotBroker } = await import("../snapshot-broker.mjs");
3185
+ const snapshotCheck = await import("../snapshot-check.mjs");
3186
+ const snapshotCommitOf = () => snapshotCheck.commitOf(fs.realpathSync.native(root), String(snapshot), o.spawn);
3187
+ // The broker is built before anything is read: a missing enrollment, ownership or client refuses here, naming its code (as the compile does).
3188
+ try {
3189
+ if (o.snapshotBroker && typeof o.snapshotBroker.create !== "function") discoveryBroker = o.snapshotBroker; // a fixture broker (tests)
3190
+ else {
3191
+ const sc = snapshotCommitOf();
3192
+ discoveryBroker = createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: sc.commit, tree: sc.tree }, snapshot: { path: repo, expectedTree: sc.tree }, ownership: o.buildOwnership, client: executor.route === LINUX_BROKER_ROUTE ? executor.client : which("rustright").find((c) => path.isAbsolute(c)), gateFile: callingGatePath(), spawn: o.spawn });
3193
+ }
3194
+ } catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
3195
+ snapshotSpawn = snapshotCargoSpawn({ spawn: withDeclaredClient((c, a, opts) => o.spawn(c, a, opts)), cargoCommand: planCargoCommand, snapshotRoot: repo, env: o.env, err: o.err, broker: () => discoveryBroker });
3196
+ scanSpawn = snapshotCargoSpawn({ spawn: withDeclaredClient(defaultSpawnAsync), carrySpawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand: planCargoCommand, snapshotRoot: repo, env: o.env, err: o.err, broker: () => discoveryBroker });
3197
+ }
2417
3198
  npm = discoverNpmUnits({ root: repo, tracked, readFile: readSnap, exists: o.exists });
2418
3199
  npmRoots = discoverPnpmRoots({ root: repo, tracked, exists: o.exists });
3200
+ let metadata = null;
3201
+ if (scopeDirs.length) {
3202
+ closureTools = await import("../app-closure.mjs");
3203
+ const readCommitted = (file) => {
3204
+ if (!files.includes(file)) throw new Error(`${file} is absent from the committed tree`);
3205
+ return readSnap(path.join(repo, file));
3206
+ };
3207
+ const sourceMetadata = closureTools.cargoMetadataFromManifests({ manifests: manifests.filter((m) => !fixtures.some((d) => m.startsWith(`${d.replace(/\/$/, "")}/`))), readFile: readCommitted });
3208
+ const inventory = discoverRustWorkspaces({ root: repo, tracked, fixtures, readFile: readSnap, metadata: sourceMetadata });
3209
+ closure = closureTools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: inventory.units, readFile: readCommitted });
3210
+ const selectedWorkspaces = new Set(inventory.units.filter((u) => closure.rust.has(u.name)).map((u) => u.cargoDir));
3211
+ metadata = (dir) => {
3212
+ const source = sourceMetadata(dir);
3213
+ return selectedWorkspaces.has(source.cargoDir) ? workspaceMetadata({ root: repo, spawn: snapshotSpawn, cargoCommand: planCargoCommand, dir, realpath: fs.realpathSync }) : source;
3214
+ };
3215
+ }
3216
+ rust = discoverRustWorkspaces({ root: repo, spawn: snapshotSpawn, cargoCommand: planCargoCommand, tracked, untracked, fixtures, readFile: readSnap, ...(metadata ? { metadata } : {}) });
3217
+ if (closure) {
3218
+ // Confirm membership discovered by Cargo; its closure must resolve without unrelated execution.
3219
+ closure = closureTools.closureOf({ scopeDirs, npmUnits: npm, rustUnits: rust.units, readFile: (file) => { if (!files.includes(file)) throw new Error(`${file} is absent from the committed tree`); return readSnap(path.join(repo, file)); } });
3220
+ installRoots = [...new Set(npm.filter((u) => closure.npm.has(u.name)).map((u) => u.root ?? "."))].sort();
3221
+ } else installRoots = npmRoots;
2419
3222
  } catch (e) { o.err(`${tag} BLOCKED: cannot discover the repository's units: ${e.message}`); return 1; }
2420
3223
  for (const k of rust.skipped) o.out(`${tag} note: ${k.manifest} is skipped, not scanned: ${k.why}`);
3224
+ // Every repository but RightKit commits one fixed build definition that lists every Cargo workspace; a run builds all of them and nothing is
3225
+ // chosen per run (#1325, Adrian 2026-10-31). The workspaces this commit holds and the committed list must be the same set, missing or extra,
3226
+ // or nothing runs. RightKit is judged per unit instead (rightkitDoorRule). `--plan` builds nothing, so it only says what a run would meet.
3227
+ {
3228
+ let rightkitHere = false;
3229
+ if (files.includes(RIGHTKIT_IDENTITY_FILE)) { try { rightkitHere = isRightKitIdentity(readSnap(path.join(repo, ...RIGHTKIT_IDENTITY_FILE.split("/")))); } catch { rightkitHere = false; } }
3230
+ if (!rightkitHere) {
3231
+ try { checkCargoWorkspaceList({ discovered: rust.workspaces.map((w) => w.cargoDir), record: files.includes(CARGO_WORKSPACE_LIST_FILE) ? readSnap(path.join(repo, CARGO_WORKSPACE_LIST_FILE)) : null }); }
3232
+ catch (e) {
3233
+ if (!e.code?.startsWith("RK_CARGO_WORKSPACE_LIST_")) throw e;
3234
+ if (args.includes("--plan")) o.out(`${tag} note: a run would be refused: ${e.message}`);
3235
+ else { o.err(`${tag} BLOCKED: ${e.message}; nothing ran`); return 1; }
3236
+ }
3237
+ }
3238
+ }
2421
3239
 
2422
3240
  // The snapshot compile: verified as the commit's tree before anything runs, dependencies installed into it offline from the
2423
3241
  // checkout's store, and every Cargo workspace's output pinned to the target directory cargo resolves for it IN THE CHECKOUT
@@ -2449,7 +3267,7 @@ export async function main(argv, io = {}) {
2449
3267
  directDeclaredNotes = judged.findings.map((f) => `declared compile input: ${f}`);
2450
3268
  }
2451
3269
  o.out(`${tag} snapshot ${snapshotB} is commit ${snapshotBCommit.commit} tree ${snapshotBCommit.tree} (verified before the compile)`);
2452
- for (const r of sc.installSnapshotDependencies({ checkout, snapshot: snapshotB, roots: npmRoots, env: o.env, spawn: o.spawn })) o.out(`${tag} ${sc.describeInstall(r)}`);
3270
+ for (const r of sc.installSnapshotDependencies({ checkout, snapshot: snapshotB, ...snapshotBCommit, ...(o.snapshotLedger !== undefined ? { ledger: o.snapshotLedger } : {}), roots: installRoots, ...(closure ? { packageDirs: npm.filter((u) => closure.npm.has(u.name)).map((u) => u.dir) } : {}), env: o.env, spawn: o.spawn })) o.out(`${tag} ${sc.describeInstall(r)}`);
2453
3271
  } catch (error) { o.err(`${tag} ${error.code === "RK_CHECK_ERROR" ? "RK_CHECK_ERROR" : "BLOCKED"}: ${error.message}`); return error.code === "RK_CHECK_ERROR" ? 2 : 1; }
2454
3272
  // A broker host (item 7 rows 7.20 to 7.24): the compile goes through the RustRight snapshot carrier. The broker pins the output
2455
3273
  // directory (its managed generation target), so no checkout target is resolved here; the gate's broker is built once, with the
@@ -2458,8 +3276,8 @@ export async function main(argv, io = {}) {
2458
3276
  if (executor.kind === "broker" && manifests.length) {
2459
3277
  try {
2460
3278
  const { createSnapshotBroker } = await import("../snapshot-broker.mjs");
2461
- const client = which("rustright").find((c) => path.isAbsolute(c));
2462
- snapshotBroker = createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: snapshotBCommit.commit, tree: snapshotBCommit.tree }, snapshot: { path: snapshotB, expectedTree: snapshotBCommit.tree }, ownership: o.buildOwnership, client, gateFile: callingGatePath(), spawn: o.spawn });
3279
+ const client = executor.route === LINUX_BROKER_ROUTE ? executor.client : which("rustright").find((c) => path.isAbsolute(c));
3280
+ snapshotBroker = discoveryBroker ?? createSnapshotBroker({ env: o.env, platform: o.platform, checkout: root, commit: { sha: snapshotBCommit.commit, tree: snapshotBCommit.tree }, snapshot: { path: snapshotB, expectedTree: snapshotBCommit.tree }, ownership: o.buildOwnership, client, gateFile: callingGatePath(), spawn: o.spawn });
2463
3281
  } catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SNAPSHOT_BROKER"}: ${error.message}; nothing compiled`); return 1; }
2464
3282
  o.out(`${tag} broker snapshot compile: pins ${snapshotBroker.pins.keyId.slice(0, 12)} from ${snapshotBroker.pinsSource}, ${snapshotBroker.declaredInputs.length} declared input(s) read from the snapshot, ownership ${snapshotBroker.handle}`);
2465
3283
  }
@@ -2491,11 +3309,9 @@ export async function main(argv, io = {}) {
2491
3309
  let broker = null;
2492
3310
  if (snapshotBroker) {
2493
3311
  // Broker mode (the Mac, decision C): every cargo a test reaches is its own carried request. The gate's own snapshot-to-checkout
2494
- // map is the only source of a request's primaryCwd, and broker work may exist from here on: the ownership records it first.
3312
+ // map is the only source of a request's primaryCwd; each carried request records its sub-handle in the ownership just before it is sent.
2495
3313
  const checkoutRoot = fs.realpathSync.native(root);
2496
3314
  const checkoutDirs = Object.fromEntries(rust.workspaces.map((ws) => [ws.cargoDir, path.join(checkoutRoot, ...(ws.cargoDir === "." ? [] : ws.cargoDir.split("/")))]));
2497
- const marked = snapshotBroker.markBrokerSubmit();
2498
- if (marked?.broker?.submitted !== true || marked.handle !== snapshotBroker.handle) throw new Error(`the build ownership did not record the broker submit under ${snapshotBroker.handle}`);
2499
3315
  const { brokerSection } = await import("../snapshot-broker.mjs");
2500
3316
  broker = { checkoutDirs, section: brokerSection(snapshotBroker) };
2501
3317
  }
@@ -2515,7 +3331,7 @@ export async function main(argv, io = {}) {
2515
3331
  // (its directory is an input above, which holds its Cargo.toml, Cargo.lock, deny.toml, .cargo and .rightkit).
2516
3332
  ...(rust.workspaces.some((w) => w.cargoDir === ".") ? ["Cargo.toml", "Cargo.lock", "deny.toml"] : []),
2517
3333
  ".cargo", "rust-toolchain.toml", "rust-toolchain", "clippy.toml", "rustfmt.toml", ...npmRootFiles, ...npmRoots.filter((r) => r !== ".").flatMap((r) => npmRootFiles.map((f) => `${r}/${f}`))])];
2518
- // Batched like cargoInputsDiffer: one argv with every input path overflows Windows' command line in a large repository.
3334
+ // Batched (pathspecBatches): one argv with every input path overflows Windows' command line in a large repository.
2519
3335
  const dirtyRuns = pathspecBatches(inputs).map((batch) => git("status", "--porcelain", "--untracked-files=all", "--", ...batch));
2520
3336
  const dirtyFailed = dirtyRuns.find((r) => failed(r));
2521
3337
  const dirtyFiles = dirtyFailed ? [`git status failed: ${why(dirtyFailed)}`] : dirtyRuns.flatMap((r) => text(r).split(/\r?\n/)).filter(Boolean).filter((l) => !l.includes("node_modules"));
@@ -2552,38 +3368,12 @@ export async function main(argv, io = {}) {
2552
3368
  }
2553
3369
  const rustScope = restricted ? scopeNames : null;
2554
3370
 
2555
- // A broker host: rustright refuses every cargo command but a locked metadata outside the app's primary checkout
2556
- // (RK_NOT_PRIMARY_CHECKOUT), so class A's cargo scans cannot read the export there. They run in the primary checkout, allowed only
2557
- // when every Cargo input in it is blob-identical to the snapshot's; a live edit to a manifest, lock, deny.toml or .cargo/ refuses
2558
- // the check with the paths named. A direct host (cloud, CI) keeps reading the export. (rightkit#1215, third cause)
3371
+ // A broker host: the class A Cargo scans of the snapshot (metadata, fetch, cargo-deny, cargo-audit) are carried read-only requests that run IN
3372
+ // THE SNAPSHOT (scanSpawn above), so an uncommitted edit in the checkout is ignored, never refused (docs/GATE-CONTRACT.md: no dirty-tree
3373
+ // condition; class A reads the snapshot). The checkout is only where the RustRight client starts (it refuses to start anywhere else) and
3374
+ // where class B's carried compiles name their working directory; `cargoLiveRoot` is that directory, never a source.
2559
3375
  let cargoLiveRoot = null;
2560
- if (classAOnly && snapshot && executor.kind === "broker" && manifests.length) {
2561
- const differing = cargoInputsDiffer({ root, snapshot, manifests, spawn: (c, a, opts) => o.spawn(c, a, opts) });
2562
- if (differing.length) {
2563
- const resolved = git("rev-parse", "--verify", String(snapshot));
2564
- o.err(`${tag} BLOCKED: the broker scans Cargo inputs in the primary checkout, and these differ from ${(failed(resolved) ? String(snapshot) : text(resolved).trim()).slice(0, 12)}: ${differing.join(", ")}; commit or restore them, then run the check again`);
2565
- return 1;
2566
- }
2567
- // A snapshot check's cargo scans then read the checkout's WORKSPACE, which can hold more than its tracked inputs: an untracked
2568
- // member, or a stray directory a members glob (crates/*) matches. Cargo's own member list there must equal the commit's (from
2569
- // the snapshot), or the verdict would not be the commit's (Oracle review of 79c4427b). A checkout cargo cannot read refuses too.
2570
- if (snapshotA) {
2571
- const memberDiff = [];
2572
- const membersOf = (base, dir) => workspaceMetadata({ root: base, spawn: (c, a, opts) => o.spawn(c, a, opts), cargoCommand, dir, realpath: fs.realpathSync }).members.map((m) => m.manifest).sort();
2573
- for (const ws of rust.workspaces) {
2574
- let inCommit; let inCheckout;
2575
- try { inCommit = membersOf(repo, ws.cargoDir); } catch (e) { memberDiff.push(`${ws.cargoDir} (commit): ${e.message}`); continue; }
2576
- try { inCheckout = membersOf(root, ws.cargoDir); } catch (e) { memberDiff.push(`${ws.cargoDir} (checkout): ${e.message}`); continue; }
2577
- const extra = inCheckout.filter((m) => !inCommit.includes(m)); const missing = inCommit.filter((m) => !inCheckout.includes(m));
2578
- if (extra.length || missing.length) memberDiff.push(`${ws.cargoDir}: checkout-only ${extra.join(", ") || "none"}; commit-only ${missing.join(", ") || "none"}`);
2579
- }
2580
- if (memberDiff.length) {
2581
- o.err(`${tag} BLOCKED: the broker scans Cargo in the primary checkout, and cargo's workspace there is not the commit's: ${memberDiff.join("; ")}; remove or commit what differs, then run the check again`);
2582
- return 1;
2583
- }
2584
- }
2585
- cargoLiveRoot = root;
2586
- }
3376
+ if (executor.kind === "broker" && manifests.length && snapshot && (snapshotA || snapshotB)) cargoLiveRoot = root;
2587
3377
  // One Cargo workspace and lock per crate (rightkit#1285): the reach between crates is the manifests' path dependencies, read from the
2588
3378
  // gated snapshot. A crate that is selected selects its transitive dependents (checked, not re-linted); a vendored source it patches
2589
3379
  // in selects the crate. Neither is derivable from a single lock, so a layout that cannot read them is refused, never planned narrower.
@@ -2600,14 +3390,11 @@ export async function main(argv, io = {}) {
2600
3390
  externalInputs = vendorOwners(crateUnits, (u) => o.readFile(path.join(repo, u.dir, "Cargo.toml")));
2601
3391
  } catch (e) { o.err(`${tag} BLOCKED: cannot read the path dependencies between the crate workspaces (${e.message}); their reverse dependents would go unchecked, so nothing is planned`); return 1; }
2602
3392
  }
2603
- // A broker snapshot compile: its cargo scans are carried requests that name the commit's snapshot and run from the primary checkout
2604
- // (rustright refuses cargo anywhere else); the checkout is the request's working directory, never its source.
2605
- if (snapshotBroker && manifests.length) cargoLiveRoot = root;
2606
3393
  let fullPlan;
2607
3394
  fullPlan = planScans({
2608
- // Class A of a commit or tree reads only that snapshot: every scan, cargo's included, runs in the export, never in the live
3395
+ // Class A of a commit or tree reads only that snapshot: every scan, cargo's included, runs in the snapshot, never in the live
2609
3396
  // checkout (which may hold other bytes). Compiles (class B) use the live checkout, whose tree the dirty check pinned to HEAD.
2610
- changed, contentRoot: repo, liveRoot: (classAOnly && snapshot) || snapshotB ? repo : root, trustedRoot: snapshotA ?? snapshotB ?? root, includeTests: true, rustScope, cargoCommand, targetDirs, cargoShim: Boolean(cargoShim),
3397
+ changed, contentRoot: repo, liveRoot: (classAOnly && snapshot) || snapshotB ? repo : root, trustedRoot: snapshotA ?? snapshotB ?? root, includeTests: true, rustScope, cargoCommand: planCargoCommand, targetDirs, cargoShim: Boolean(cargoShim),
2611
3398
  tier: fullA ? "release" : (option("--rule-tier") ?? "compile"), cargoLiveRoot,
2612
3399
  // The quiet tests (port, pipe, desktop and wall-clock bound) are proven by the CI shards, one crate per runner, unless
2613
3400
  // RK_GATE_QUIET_TESTS=local asks for the serial quiet lane here (no CI, or a CI outage).
@@ -2626,6 +3413,21 @@ export async function main(argv, io = {}) {
2626
3413
  // broker is unreachable runs neither; the gate stops rather than skip them.
2627
3414
  if (executor.kind === "refuse" && fullPlan.scans.some((x) => x.class === "B" || (x.kind === "test" && (!classAOnly || fullA)))) { o.err(`${tag} BLOCKED: ${executor.problem}`); return 1; }
2628
3415
  let plan = fullPlan;
3416
+ if (closure) {
3417
+ let kept;
3418
+ try {
3419
+ kept = closureTools.scopeScans(fullPlan.scans, closure, { rustUnits: rust.units });
3420
+ plan = closureTools.scopedPlan(fullPlan, kept.scans, closure);
3421
+ } catch (error) { o.err(`${tag} BLOCKED: ${error.message}`); return 1; }
3422
+ plan.notes = plan.notes.filter((n) => !n.includes("every Rust unit is in scope"));
3423
+ const classA = plan.scans.filter((s) => s.class === "A");
3424
+ // What actually executes: tests, executing rows and every Cargo scan (metadata, package list, deny, audit run cargo, through the broker
3425
+ // on a broker host), never only the rows that carry `executes`. The Cargo counts are named so a scoped run shows how much cargo it starts.
3426
+ const executing = classA.filter((s) => closureTools.isExecutingScan(s)).length;
3427
+ const cargoRun = classA.filter((s) => closureTools.isCargoScan(s)).length;
3428
+ const cargoExcluded = kept.dropped.filter((s) => closureTools.isCargoScan(s)).length;
3429
+ o.out(`${tag} scope ${closure.dirs.join(", ")}: dependency directories ${closure.dependencyDirs.join(", ")}; ${closure.npm.size} npm & ${closure.rust.size} Rust unit(s); Class A ${executing} executing scan(s) (${cargoRun} Cargo), ${classA.length - executing} repository file-only scan(s); ${kept.dropped.length} unrelated executing scan(s) excluded (${cargoExcluded} Cargo); ${plan.scans.length - classA.length} closure Class B scan(s) do not run in this check; install roots ${installRoots.join(", ") || "none"}; scope digest ${plan.scopeDigest}`);
3430
+ }
2629
3431
  const jobs = Math.max(1, Number(o.env.RK_GATE_JOBS ?? 4) || 4);
2630
3432
  const testJobs = Math.max(1, Number(o.env.RK_GATE_TEST_JOBS ?? jobs) || jobs);
2631
3433
  const classBJobs = Math.max(1, Number(o.env.RK_GATE_CLASS_B_JOBS ?? 3) || 3);
@@ -2664,7 +3466,8 @@ export async function main(argv, io = {}) {
2664
3466
  // Full class A of a snapshot binds like the live tree does: its bytes are the commit's (blob ids from the object database), its
2665
3467
  // environment includes the dependencies installed into it below.
2666
3468
  const snapFullA = fullA && Boolean(snapshotA);
2667
- const boundFullA = liveFullA || snapFullA;
3469
+ const hostedFullA = fullA && Boolean(hostedCommitA);
3470
+ const boundFullA = liveFullA || snapFullA || hostedFullA;
2668
3471
  const liveBase = snapFullA ? snapshotA : root;
2669
3472
  // The live bytes full class A judges: every tracked and untracked (not ignored) file, hashed raw. Computed whether or not the
2670
3473
  // cache is used; a failure to compute it blocks (the verdict must name the bytes it covers).
@@ -2687,6 +3490,12 @@ export async function main(argv, io = {}) {
2687
3490
  if (liveFullA) {
2688
3491
  try { live = liveTree(); } catch (error) { o.err(`${tag} BLOCKED: cannot bind full class A to the working tree's bytes (${error.message})`); return 1; }
2689
3492
  }
3493
+ if (hostedFullA) {
3494
+ try {
3495
+ await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "before class A" });
3496
+ live = (await import("../snapshot-check.mjs")).commitFiles(root, hostedCommitA.commit, o.spawn);
3497
+ } catch (error) { o.err(`${tag} RK_CHECK_ERROR: ${error.message}; nothing scanned`); return 2; }
3498
+ }
2690
3499
  let snapshotCommitId = null;
2691
3500
  if (snapFullA) {
2692
3501
  const sc = await import("../snapshot-check.mjs");
@@ -2698,7 +3507,7 @@ export async function main(argv, io = {}) {
2698
3507
  live = sc.commitFiles(checkout, ct.commit, o.spawn);
2699
3508
  // Dependencies for the checks that execute repository code: installed into the snapshot from the commit's lockfile and the
2700
3509
  // primary checkout's existing store (coordinator decision A, rightkit#1257). A store miss stops here, naming the package.
2701
- const installed = sc.installSnapshotDependencies({ checkout, snapshot: snapshotA, roots: npmRoots, env: o.env, spawn: o.spawn });
3510
+ const installed = sc.installSnapshotDependencies({ checkout, snapshot: snapshotA, ...ct, roots: installRoots, ...(closure ? { packageDirs: npm.filter((u) => closure.npm.has(u.name)).map((u) => u.dir) } : {}), env: o.env, spawn: o.spawn });
2702
3511
  for (const r of installed) o.out(`${tag} ${sc.describeInstall(r)}`);
2703
3512
  } catch (error) { o.err(`${tag} ${error.code === "RK_CHECK_ERROR" ? "RK_CHECK_ERROR" : "BLOCKED"}: ${error.message}`); return error.code === "RK_CHECK_ERROR" ? 2 : 1; }
2704
3513
  }
@@ -2727,7 +3536,8 @@ export async function main(argv, io = {}) {
2727
3536
  // The verdict also depends on what is not in the tree: the gate's own code and advisory data, the node and pnpm that run
2728
3537
  // the checks, the installed dependencies (pnpm's install state) and the platform. Any change there is a cache miss.
2729
3538
  const environment = classAEnvironmentDigest({ root: liveBase, gatePackage: o.gatePackage, files, spawn: o.spawn });
2730
- cache = { ...store, digest: `${buildCacheDigest({ objectId, headTree: digestOf(files), rustUnits: rust.units, npmUnits: npm, cargoDir: ".", rustLocks, crateFeatures, listFiles, readBlob })}:${environment}` };
3539
+ const digest = buildCacheDigest({ objectId, headTree: digestOf(files), rustUnits: rust.units, npmUnits: npm, cargoDir: ".", rustLocks, crateFeatures, listFiles, readBlob });
3540
+ cache = { ...store, digest: (scan) => { const key = digest(scan); return key === null ? null : `${key}:${environment}:${plan.scopeDigest ?? "repository"}:${plan.scanSetDigest}`; } };
2731
3541
  } catch { cache = null; }
2732
3542
  }
2733
3543
  if (!classAOnly && !args.includes("--no-cache") && o.env.RK_GATE_CACHE !== "0") {
@@ -2757,6 +3567,18 @@ export async function main(argv, io = {}) {
2757
3567
  }
2758
3568
  } catch { cache = null; }
2759
3569
  }
3570
+ // The scan-tool policy (RA-18 / L9-03): the analyzers class A accepts (shellcheck, actionlint, cargo-deny, cargo-audit) run only from an enrolled
3571
+ // executable. It comes from this host's enrolment record under the managed data root, else (RightKit's own checkout only) from the RUNNING
3572
+ // gate's revision of ci/rk-pipeline.json `tools` (scan-tool-enrolment.mjs), never the checked repository, the snapshot or an environment variable.
3573
+ // An unreadable or malformed record refuses the run: a scan tool is never judged against a policy that could not be read.
3574
+ let scanTools;
3575
+ try {
3576
+ scanTools = await (o.loadScanToolPolicy ?? (async (q) => (await import("../scan-tool-enrolment.mjs")).loadScanToolPolicy(q)))({
3577
+ dataRoot: resolveManagedRoots({ env: o.env, platform: o.platform }).dataRoot, platform: o.platform, gateFile: callingGatePath(), checkout: root, fromPipeline: scanToolPolicyFromPipeline, // reads the gate's own repository with real git, never the run's (possibly injected) spawn
3578
+ });
3579
+ } catch (error) { o.err(`${tag} BLOCKED: ${error.code ?? "RK_SCAN_TOOLS"}: ${error.message}; no scan ran`); return 1; }
3580
+ const enrolledTools = Object.keys(scanTools?.policy ?? {});
3581
+ if (enrolledTools.length) o.out(`${tag} scan-tool policy: ${enrolledTools.map((name) => `${name} (${scanTools.sources?.[name] ?? "enrolled"})`).join(", ")}`);
2760
3582
  // The direct compile's freshness census (row 7.19): the fingerprint units before the scans, and the fingerprint log of each cargo
2761
3583
  // process that compiles (CARGO_LOG on that process only, read from its own standard error). A broker host's census is the broker's.
2762
3584
  const census = snapshotB && !snapshotBroker ? createFreshnessCensus({ targetDirs }) : null;
@@ -2765,6 +3587,20 @@ export async function main(argv, io = {}) {
2765
3587
  const scanResult = await (o.runScans ?? runScans)(plan, {
2766
3588
  classes: classAOnly ? ["A"] : ["A", "B"], jobs, testJobs, classBJobs, signal: ac.signal, ...(cache ? { cache } : {}),
2767
3589
  ...(census ? { spawn: census.wrapSpawn(defaultSpawnAsync) } : {}),
3590
+ // The declared Linux broker route: the scans are planned with the logical `rustright`, which this spawn (and scanSpawn's pass-through below) starts as the
3591
+ // executor's validated client, never a PATH lookup (withDeclaredClient).
3592
+ ...(declaredClient && !scanSpawn ? { spawn: withDeclaredClient(census ? census.wrapSpawn(defaultSpawnAsync) : defaultSpawnAsync) } : {}),
3593
+ toolPolicy: scanTools?.policy ?? {},
3594
+ // The class A cargo reads (deny, audit, metadata, fetch) are carried requests the broker executes (scanSpawn): cargo-deny and cargo-audit are then the
3595
+ // broker's registered tools, in the check stage too (which has no snapshotBroker). Only when the reads really go to the broker.
3596
+ ...(executor.kind === "broker" && scanSpawn ? { cargoReadsCarried: true } : {}),
3597
+ // A broker host: every Cargo call a class A scan makes inside the snapshot is a carried read-only request; the gate's own temporary files
3598
+ // (cargo-deny's metadata and config) live in a gate-owned directory the broker can read, outside the checkout.
3599
+ ...(scanSpawn ? { spawn: scanSpawn, tmpRoot: gateTmpRoot({ env: o.env, platform: o.platform, exists: o.exists }) } : {}),
3600
+ // A host with no broker runs its Class A Cargo scans itself, in the snapshot: cargo would write target/.rustc_info.json beside each manifest
3601
+ // it reads and the snapshot would no longer be the commit's tree. Each such scan gets an output directory of its own under the gate's
3602
+ // scratch root, removed when it ends (deterministic-scans.mjs cargoReadTargetDir). A broker host carries these reads and the broker owns the output.
3603
+ ...(!scanSpawn ? { cargoReadTargetRoot: gateTmpRoot({ env: o.env, platform: o.platform, exists: o.exists }) } : {}),
2768
3604
  // The runtime cargo wrapper is first on PATH and named in CARGO for every scan of a snapshot compile; CARGO (a path named by the
2769
3605
  // wrapper's identity) is in every scan's cache key, so no result cached without the wrapper or with another map is reused.
2770
3606
  ...(cargoShim ? { env: cargoShim.scanEnv, ...(cargoShim.sentinel ? { cargoSentinel: cargoShim.sentinel } : {}), cargoLedger: { dir: cargoShim.ledgerDir } } : {}),
@@ -2775,14 +3611,14 @@ export async function main(argv, io = {}) {
2775
3611
  // `executes`) run in the full gate (--all, the push hook, the door's compile stage), after the content checks and before any compile.
2776
3612
  ...(classAOnly && !fullA ? { exclude: (s) => s.executes === true } : {}),
2777
3613
  // A snapshot check names its commit; it never reads the checkout's HEAD.
2778
- quietLane: quietLeaseLane(o, { tag, reason: `rightkit-gate-quiet-tests@${snapshotCommitId ? snapshotCommitId.commit.slice(0, 12) : snapshotBCommit ? snapshotBCommit.commit.slice(0, 12) : text(git("rev-parse", "--short", "HEAD")).trim()}` }),
3614
+ quietLane: quietLeaseLane(o, { tag, executor, reason: `rightkit-gate-quiet-tests@${snapshotCommitId ? snapshotCommitId.commit.slice(0, 12) : snapshotBCommit ? snapshotBCommit.commit.slice(0, 12) : text(git("rev-parse", "--short", "HEAD")).trim()}` }),
2779
3615
  // On a broker-managed host the quiet suites need the lease or they are refused (never run unleashed).
2780
3616
  requireQuietLease: executor.kind === "broker",
2781
3617
  // Failing scans' complete output: never inside the repository, on the host's scratch root (RIGHTKIT_SCRATCH_ROOT, else the
2782
3618
  // managed data root's runs/), in a folder unique to this run so evidence is never overwritten.
2783
3619
  scanLogDir: (() => {
2784
3620
  try {
2785
- const base = o.env.RIGHTKIT_SCRATCH_ROOT || path.join(resolveManagedRoots({ env: o.env }).dataRoot, "runs");
3621
+ const base = gateRunsRoot({ env: o.env, platform: o.platform });
2786
3622
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
2787
3623
  return path.join(base, "rightkit-gate-scan-logs", `${path.basename(root)}-${stamp}-${process.pid}`);
2788
3624
  } catch { return null; }
@@ -2807,6 +3643,10 @@ export async function main(argv, io = {}) {
2807
3643
  // in (gate, tools, installed dependencies, tool caches), unchanged too. A verdict whose binding moved covers nothing: a green one is
2808
3644
  // refused and a red one names no reusable deterministic cause (rightkit#1257). Returns { env, notes } or { prefix, problem, exit }.
2809
3645
  const judgedBinding = async () => {
3646
+ if (hostedFullA) {
3647
+ try { await verifyCommittedCheckout({ git, root, ...hostedCommitA, when: "after class A" }); }
3648
+ catch (error) { return { prefix: "RK_CHECK_ERROR", exit: 2, problem: `${error.message}; the verdict covers no known bytes` }; }
3649
+ }
2810
3650
  if (liveFullA) {
2811
3651
  let digestNow = null;
2812
3652
  try { digestNow = liveTree().digest; } catch { digestNow = null; }
@@ -2844,6 +3684,8 @@ export async function main(argv, io = {}) {
2844
3684
  o.out(`${tag} snapshot ${snapshotB} is still tree ${snapshotBCommit.tree} after the compile`);
2845
3685
  // A broker host's evidence is the broker's own signed receipts (each carried scan's row.snapshotEvidence, verified above); the
2846
3686
  // gate writes no receipt that says `executor: direct` for it.
3687
+ // The output directories this compile's evidence names (GB-13): the broker's signed receipts' `outputDir`, or the direct host's resolved targets.
3688
+ o.onCompileOutputs?.(snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir).filter(Boolean) : Object.values(targetDirs ?? {}).filter(Boolean));
2847
3689
  if (snapshotBroker) o.out(`${tag} broker compile: ${scanResult.scans.filter((r) => r.snapshotEvidence).length} carried request(s) with verified signed receipts; no direct receipt on a broker host`);
2848
3690
  else {
2849
3691
  // The direct host's own receipt (GATE-CONTRACT "Broker interface", row 7.18): the contract's returned-evidence names minus the
@@ -2854,7 +3696,7 @@ export async function main(argv, io = {}) {
2854
3696
  const requestId = randomUUID();
2855
3697
  const exitCode = failures.length ? 1 : 0;
2856
3698
  const diagnosticsDir = (() => {
2857
- try { return path.join(o.env.RIGHTKIT_SCRATCH_ROOT || path.join(resolveManagedRoots({ env: o.env }).dataRoot, "runs"), "rightkit-gate-direct-diagnostics"); } catch { return path.join(os.tmpdir(), "rightkit-gate-direct-diagnostics"); }
3699
+ try { return path.join(gateRunsRoot({ env: o.env, platform: o.platform }), "rightkit-gate-direct-diagnostics"); } catch { return path.join(os.tmpdir(), "rightkit-gate-direct-diagnostics"); }
2858
3700
  })();
2859
3701
  let directReceipt;
2860
3702
  try {
@@ -2888,7 +3730,7 @@ export async function main(argv, io = {}) {
2888
3730
  if (binding.exit === 2) return 2;
2889
3731
  } else {
2890
3732
  for (const n of binding.notes) o.out(`${tag} ${n}`);
2891
- const marker = (await import("../snapshot-check.mjs")).deterministicMarker(scanResult, binding.env);
3733
+ const marker = (await import("../snapshot-check.mjs")).deterministicMarker(scanResult, plan.scopeDigest ? createHash("sha256").update(`${binding.env}:${plan.scopeDigest}:${plan.scanSetDigest}`).digest("hex") : binding.env);
2892
3734
  if (marker) o.out(marker);
2893
3735
  }
2894
3736
  }
@@ -2905,12 +3747,12 @@ export async function main(argv, io = {}) {
2905
3747
  // raw bytes>\n" lines (git hash-object --no-filters), so the broker can recompute it over its snapshot without this gate.
2906
3748
  if (live) {
2907
3749
  // A snapshot holds only the commit's files: every one is tracked.
2908
- const trackedList = snapFullA ? null : git("ls-files", "-z", "--cached");
3750
+ const trackedList = snapFullA || hostedFullA ? null : git("ls-files", "-z", "--cached");
2909
3751
  if (trackedList && failed(trackedList)) { o.err(`${tag} BLOCKED: cannot list tracked files to name the checked bytes`); return 1; }
2910
- const tracked = new Set(snapFullA ? live.files : text(trackedList).split("\0").filter(Boolean).map(posix));
2911
- o.out(`${tag} class A bytes tracked=${live.digestOf(live.files.filter((f) => tracked.has(f)))} all=${live.digest}${envStartWithCaches ? ` env-at-start=${envStartWithCaches}` : ""} env=${binding.env}`);
3752
+ const tracked = new Set(snapFullA || hostedFullA ? live.files : text(trackedList).split("\0").filter(Boolean).map(posix));
3753
+ o.out(`${tag} class A bytes tracked=${live.digestOf(live.files.filter((f) => tracked.has(f)))} all=${live.digest}${envStartWithCaches ? ` env-at-start=${envStartWithCaches}` : ""} env=${plan.scopeDigest ? createHash("sha256").update(`${binding.env}:${plan.scopeDigest}:${plan.scanSetDigest}`).digest("hex") : binding.env}${plan.scopeDigest ? ` scope=${plan.scopeDigest} scans=${plan.scanSetDigest}` : ""}`);
2912
3754
  }
2913
- o.out(`${tag} class A full pass: ${scanResult.scans.length} deterministic check(s), the ones that execute repository code included, over ${changed.length} file(s) of ${base}`);
3755
+ o.out(`${tag} class A ${plan.scopeDigest ? "scoped" : "full"} pass: ${scanResult.scans.length} deterministic check(s), the ones that execute repository code included, over ${changed.length} file(s) of ${base}`);
2914
3756
  return 0;
2915
3757
  }
2916
3758
  if (classAOnly) {
@@ -2932,18 +3774,32 @@ export async function main(argv, io = {}) {
2932
3774
  // The inherited locations a snapshot compile requires as absolute paths (GATE-CONTRACT "Output directory & cache").
2933
3775
  const SNAPSHOT_ABSOLUTE_ENV = ["CARGO_HOME", "RUSTUP_HOME", "SCCACHE_DIR", "TMPDIR", "TMP", "TEMP"];
2934
3776
 
3777
+ /**
3778
+ * What the production entry (`node rightkit-gate.mjs ...`) hands `main` as its injected dependencies, and what `--route` reports: the
3779
+ * snapshot carrier on Mac and Dell (rightkit#1301: right-release's own Cargo calls, the Tauri CLI's and cargo-about's are carried, so
3780
+ * every stage compiles the commit's snapshot through the broker); nothing on Linux, which compiles direct with no broker. Tests inject
3781
+ * their own carrier through `main`; the contract test (door-route.test.mjs) runs the real entry. `platform` is the host whose deps are
3782
+ * asked for (the real entry passes none).
3783
+ */
3784
+ export async function productionEntryDeps({ platform = process.platform, env = process.env } = {}) {
3785
+ // A direct Linux host carries nothing. A Linux host that DECLARED the broker route (managed-cargo.mjs linuxBrokerDeclared) gets the same carrier as the
3786
+ // Mac and the Dell, valid declaration or not: an invalid one is refused by the executor (kind "refuse"), never left to compile direct without a carrier.
3787
+ if (directPlatform(platform) && !linuxBrokerDeclared(env, platform)) return {};
3788
+ const { PRODUCTION_SNAPSHOT_CARRIER } = await import("../snapshot-broker.mjs");
3789
+ return { snapshotBroker: PRODUCTION_SNAPSHOT_CARRIER };
3790
+ }
3791
+
2935
3792
  const invokedDirectly = (() => {
2936
3793
  try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; }
2937
3794
  })();
2938
3795
  /** This gate's own resolved entry file: the broker admits a compile only when the request names the pinned trusted gate by this
2939
- * path (rustright reads it from RIGHTKIT_CALLING_GATE; a missing or different path is refused). */
3796
+ * path (rustright reads it from RUSTRIGHT_CALLING_GATE; a missing or different path is refused). */
2940
3797
  export const callingGatePath = () => fs.realpathSync(fileURLToPath(import.meta.url));
2941
3798
  if (invokedDirectly) {
2942
3799
  // Every child this run starts that can reach rustright (scans, unit stages, right-release dev/test, their builds) inherits it.
2943
- process.env.RIGHTKIT_CALLING_GATE = callingGatePath();
2944
- // Interim (decision 2): production carries no snapshot carrier until every child that reaches Cargo has one (item 7 row 7.24,
2945
- // right-release dev and test, is not wired). With it wired early, 0.6.12 refused every dev, test and sidecar package build on the
2946
- // Mac and the Dell (rightkit#1296, HeardRight 2026-10-30); without it they keep the checkout route, as in 0.6.10. Linux takes the
2947
- // snapshot route regardless (direct, no carrier). Tests inject a carrier. The cutover commit wires PRODUCTION_SNAPSHOT_CARRIER here.
2948
- Promise.resolve(main(process.argv.slice(2))).then((code) => process.exit(code), (e) => { console.error(`rightkit-gate: ${e?.stack ?? e}`); process.exit(2); });
3800
+ process.env.RUSTRIGHT_CALLING_GATE = callingGatePath();
3801
+ delete process.env.RIGHTKIT_CALLING_GATE; // one spelling, never both
3802
+ // Production carries the snapshot carrier on Mac and Dell (rightkit#1301): every child that reaches Cargo is carried, so every stage
3803
+ // compiles the commit's snapshot. Linux takes the snapshot route direct, with no carrier. Tests inject theirs.
3804
+ productionEntryDeps().then((deps) => main(process.argv.slice(2), deps)).then((code) => process.exit(code), (e) => { console.error(`rightkit-gate: ${e?.stack ?? e}`); process.exit(2); });
2949
3805
  }