@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
package/unit-gate.mjs CHANGED
@@ -2,13 +2,20 @@
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { locateBrokerClient, spawnBrokerClient } from "./broker-client.mjs";
7
+ import { publicationRowProblem } from "./publication-outcomes.mjs";
6
8
  import { localBuildGranted } from "./execution-policy.mjs";
7
9
  import { readCargoPackage } from "./crate-graph.mjs";
8
10
  import { spawnSync as nodeSpawnSync } from "./background-process.mjs";
9
11
  import { endRun, startRun, stagesTo } from "./gate-stages.mjs";
10
- import { qualificationNonce } from "./provenance-verify.mjs";
12
+ import { RIGHTKIT_REPOSITORY, buildDirectOrigin, directStageEvidence, verifyDirectOrigin } from "./direct-origin.mjs";
13
+ import { exclusionsFor } from "./pack-digest.mjs";
14
+ import { ATTEST_STAGE_NONCE_ENV, qualificationNonce } from "./provenance-verify.mjs";
15
+ import { verifyQualificationReceipts } from "./qualification-verify.mjs";
11
16
  import { assignJobs, describeCommand, expectedStageCommands } from "./stage-commands.mjs";
17
+ import { resolveManagedRoots } from "./managed-roots.mjs";
18
+ import { controlPlaneProblem } from "./control-plane.mjs";
12
19
 
13
20
  const GH = process.platform === "win32" ? "C:\\Program Files\\GitHub CLI\\gh.exe" : "/opt/homebrew/bin/gh";
14
21
  const defaultRun = (cmd, args, options = {}) => nodeSpawnSync(cmd, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, ...options });
@@ -40,8 +47,10 @@ export function defaultStageCommands({ unit, root, platform, readJson = defaultR
40
47
  return expectedStageCommands({ kind: unit.kind, unit: unit.name, config: readJson(path.join(root, "ci", "gate.config.json")), manifest, platform });
41
48
  }
42
49
 
43
- export function readBrokerReceipt(id, { run = defaultRun } = {}) {
44
- const r = run("rustright", ["status", id], { encoding: "utf8", timeout: 60_000 });
50
+ export function readBrokerReceipt(id, { run = defaultRun, platform = process.platform, readFile, env = process.env, installed } = {}) {
51
+ // The client starts the one way every gate child starts it (broker-client.mjs): on Windows `rustright` is rustright.cmd, which node cannot
52
+ // spawn, so its target .exe is read from the shim and started directly.
53
+ const r = spawnBrokerClient(run, locateBrokerClient({ platform, env, probe: run, ...(installed ? { installed } : {}) }), ["status", id], { encoding: "utf8", timeout: 60_000, windowsHide: true, shell: false }, { platform, readFile });
45
54
  if (r.error || r.status !== 0) throw new Error(`the broker cannot report job ${id} (rustright status failed)`);
46
55
  let doc;
47
56
  try { doc = JSON.parse(r.stdout); } catch { throw new Error(`the broker's status of job ${id} is not JSON`); }
@@ -50,16 +59,35 @@ export function readBrokerReceipt(id, { run = defaultRun } = {}) {
50
59
  return { id, provenance: receipt.provenance };
51
60
  }
52
61
  /** `<bundle>/qualification.json`: the local stages that passed and the one artifact sha256 they bound, for the dispatcher. */
53
- export function writeQualification({ report, artifact, unit, commit, tree, runtime, executor, write = fs.writeFileSync }) {
62
+ export function writeQualification({ report, artifact, unit, commit, tree, runtime, executor, directOrigin = null, write = fs.writeFileSync }) {
54
63
  for (const stage of ["check", "compile", "package", "qualify"]) if (report.stages?.[stage]?.ok !== true) throw new Error(`release needs stage ${stage} ok before it runs`);
55
64
  if (!artifact?.artifactFile || !/^[0-9a-f]{64}$/.test(artifact.artifactSha256 ?? "")) throw new Error("release needs the qualified artifact");
65
+ // An npm unit is bound hosted by its content digest (pack-digest.mjs), not by the archive's bytes; for @rightkit/release the local archive
66
+ // must also have held the swap helper and its signing receipt, the two entries that digest leaves out.
67
+ const content = {};
68
+ if (unit.kind === "npm") {
69
+ if (!/^[0-9a-f]{64}$/.test(artifact.contentSha256 ?? "")) throw new Error("release needs the qualified npm archive's content digest");
70
+ content.contentSha256 = artifact.contentSha256;
71
+ const excluded = exclusionsFor(unit.name);
72
+ if (excluded.length) {
73
+ if (!Array.isArray(artifact.contentExcluded) || [...artifact.contentExcluded].sort().join("\n") !== [...excluded].sort().join("\n")) throw new Error(`release needs the qualified ${unit.name} archive to hold ${excluded.join(" and ")}`);
74
+ content.contentExcluded = [...artifact.contentExcluded].sort();
75
+ }
76
+ }
56
77
  const receipt = { schema: QUALIFICATION_SCHEMA, unit: { kind: unit.kind, name: unit.name, version: unit.version }, commit, tree, runtime, executor: executor?.kind ?? null,
57
- stages: Object.fromEntries(["check", "compile", "package", "qualify"].map((s) => [s, { ...report.stages[s] }])), artifactSha256: artifact.artifactSha256, writtenAt: new Date().toISOString() };
58
- // The broker's signed receipts for the compile and package stages are what the hosted admission verifies; without them the
59
- // booleans above are only this process's say-so.
60
- for (const stage of ATTESTED_STAGES) {
61
- const list = report.stages[stage].receipts;
62
- if (!Array.isArray(list) || list.length < 1 || list.length > MAX_STAGE_RECEIPTS) throw new Error(`release needs 1 to ${MAX_STAGE_RECEIPTS} signed broker receipts of the ${stage} stage (found ${Array.isArray(list) ? list.length : "none"})`);
78
+ stages: Object.fromEntries(["check", "compile", "package", "qualify"].map((s) => [s, { ...report.stages[s] }])), artifactSha256: artifact.artifactSha256, ...content, writtenAt: new Date().toISOString() };
79
+ // GB-29: a direct Linux origin has no broker and so no signed receipts; it carries its direct-origin record instead (direct-origin.mjs), which the hosted side re-derives and never trusts
80
+ // by itself. Every other host owes the broker's signed receipts for the compile and package stages.
81
+ if (executor?.kind === "direct") {
82
+ if (!directOrigin) throw new Error("a direct origin's qualification needs its direct-origin record");
83
+ receipt.directOrigin = directOrigin;
84
+ } else {
85
+ // The broker's signed receipts for the compile and package stages are what the hosted admission verifies; without them the
86
+ // booleans above are only this process's say-so.
87
+ for (const stage of ATTESTED_STAGES) {
88
+ const list = report.stages[stage].receipts;
89
+ if (!Array.isArray(list) || list.length < 1 || list.length > MAX_STAGE_RECEIPTS) throw new Error(`release needs 1 to ${MAX_STAGE_RECEIPTS} signed broker receipts of the ${stage} stage (found ${Array.isArray(list) ? list.length : "none"})`);
90
+ }
63
91
  }
64
92
  if (JSON.stringify(receipt).length > MAX_QUALIFICATION_BYTES) throw new Error("the qualification receipt with its broker receipts exceeds the workflow dispatch input limit");
65
93
  const file = path.join(path.dirname(artifact.artifactFile), "qualification.json");
@@ -67,17 +95,93 @@ export function writeQualification({ report, artifact, unit, commit, tree, runti
67
95
  return file;
68
96
  }
69
97
 
98
+ /**
99
+ * GB-07 (BROKER-CONTRACT section 16): the coordinator that dispatches and pre-flights a unit is the TRUSTED, INSTALLED one: the running gate's own `scripts/ci/rk-pipeline.mjs`, found from this
100
+ * module's own location (never from the `root` snapshot or the `checkout` the release was asked about, so a modified or planted coordinator there cannot change the candidate's logic) with the
101
+ * digest of its bytes, which the release reports. Throws when the gate has none beside it.
102
+ */
103
+ export function trustedCoordinator({ moduleUrl = import.meta.url, exists = fs.existsSync, readFile = fs.readFileSync } = {}) {
104
+ const file = path.resolve(path.dirname(fileURLToPath(moduleUrl)), "..", "..", "scripts", "ci", "rk-pipeline.mjs");
105
+ if (!exists(file)) throw new Error(`the installed coordinator ${file} is not beside the running gate; dispatch and pre-flight run only the gate's own installed rk-pipeline (never one under the snapshot or the checkout), so nothing was started`);
106
+ return { file, sha256: createHash("sha256").update(readFile(file)).digest("hex") };
107
+ }
108
+
109
+ export { controlPlaneProblem };
110
+
111
+ /**
112
+ * GB-07: the in-gate-process entry to the trusted, installed coordinator. The coordinator's own module is imported into THIS (the gate's) process, from the gate's own installed location
113
+ * (trustedCoordinator), and its `runPipeline` runs the canary, pre-flight, dispatch and watch as in-process calls over the explicit root, commit and qualification the release passes: no child
114
+ * process is started from a path under the snapshot or the checkout, and the coordinator derives no root from where it lives. `expectedSha256` is a registered pin (the digest the host enrolled for
115
+ * the installed coordinator); the file is hashed before and again after it is imported, so bytes that change under the gate are refused. A coordinator that sits inside the managed snapshots root
116
+ * or inside the source snapshot is refused (it would be the candidate's own bytes), and so is a module without the entry.
117
+ */
118
+ export async function loadInGatePipeline({ coordinator = trustedCoordinator(), expectedSha256 = null, snapshotsRoot = null, root = null, pathApi = path, importModule = (url) => import(url), readFile = fs.readFileSync } = {}) {
119
+ const fold = (p) => { const r = pathApi.resolve(String(p)); return pathApi === path.win32 ? r.toLowerCase() : r; };
120
+ const inside = (child, parent) => { const rel = pathApi.relative(fold(parent), fold(child)); return rel === "" || (!rel.startsWith("..") && !pathApi.isAbsolute(rel)); };
121
+ if (!coordinator?.file || !/^[0-9a-f]{64}$/.test(coordinator.sha256 ?? "")) throw new Error("the in-gate coordinator needs its installed file and the sha256 of its bytes; nothing was started");
122
+ if (expectedSha256 !== null && coordinator.sha256 !== expectedSha256) throw new Error(`the installed coordinator ${coordinator.file} is ${coordinator.sha256.slice(0, 12)}, not the pinned ${String(expectedSha256).slice(0, 12)}; the gate runs only the coordinator the host registered, so nothing was started`);
123
+ for (const [where, parent] of [["the managed snapshots root", snapshotsRoot], ["the source snapshot", root]]) {
124
+ if (parent && inside(coordinator.file, parent)) throw new Error(`the coordinator ${coordinator.file} is inside ${where} ${parent}: it would be the candidate's own bytes, never the trusted installed coordinator; nothing was started`);
125
+ }
126
+ const digest = () => createHash("sha256").update(readFile(coordinator.file)).digest("hex");
127
+ if (digest() !== coordinator.sha256) throw new Error(`the installed coordinator ${coordinator.file} changed after its digest was taken; nothing was started`);
128
+ const loaded = await importModule(pathToFileURL(coordinator.file).href);
129
+ if (digest() !== coordinator.sha256) throw new Error(`the installed coordinator ${coordinator.file} changed while it was loaded; nothing was started`);
130
+ if (typeof loaded?.runPipeline !== "function") throw new Error(`the installed coordinator ${coordinator.file} has no in-gate entry (runPipeline); nothing was started`);
131
+ return { runPipeline: loaded.runPipeline, coordinator: { file: coordinator.file, sha256: coordinator.sha256 } };
132
+ }
133
+
134
+ /** The managed snapshots root of this host (`<data root>/snapshots`), or null when the host's roots cannot be resolved. */
135
+ const managedSnapshotsRoot = ({ env, platform }) => { try { return (platform === "win32" ? path.win32 : path.posix).join(resolveManagedRoots({ env, platform }).dataRoot, "snapshots"); } catch { return null; } };
136
+
70
137
  const requireBrokerHost = (executor) => { if (executor?.kind !== "broker") throw new Error("unit release runs only on a broker-managed host (the hosted qualification is dispatched by the RustRight rightkit-dispatch adapter); a direct host has no gate-owned release route, so no upload was started"); };
71
138
 
72
- export async function releaseUnit({ unit, root, commit, executor, env, artifact, qualification, platform = process.platform, run = defaultRun, readJson = defaultReadJson, out = console.log, scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch", registryBytes = defaultRegistryBytes }) {
139
+ export async function releaseUnit({ unit, root, checkout = root, commit, tree = null, coordinator = null, pipeline = null, executor, env, artifact, qualification, platform = process.platform, run = defaultRun, readJson = defaultReadJson, out = console.log, scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch", registryBytes = defaultRegistryBytes }) {
73
140
  requireBrokerHost(executor);
141
+ // GB-07: the source root and the control-plane checkout are apart, and the coordinator is the gate's own installed one.
142
+ const snapshotsRoot = managedSnapshotsRoot({ env, platform });
143
+ const apart = controlPlaneProblem({ root, checkout, snapshotsRoot, pathApi: platform === "win32" ? path.win32 : path.posix });
144
+ if (apart) throw new Error(`${apart}; nothing was started`);
145
+ // The coordinator runs in this process when the gate loaded its in-gate entry (`pipeline`, loadInGatePipeline: production); otherwise the installed file runs as a child process (the older route).
146
+ const installed = pipeline?.coordinator ?? coordinator ?? trustedCoordinator();
147
+ if (pipeline) {
148
+ if (typeof pipeline.runPipeline !== "function") throw new Error("the in-gate coordinator entry has no runPipeline; nothing was started");
149
+ const api = platform === "win32" ? path.win32 : path.posix;
150
+ const fold = (p) => { const r = api.resolve(String(p)); return platform === "win32" ? r.toLowerCase() : r; };
151
+ const inside = (child, parent) => { const rel = api.relative(fold(parent), fold(child)); return rel === "" || (!rel.startsWith("..") && !api.isAbsolute(rel)); };
152
+ if (snapshotsRoot && inside(installed.file, snapshotsRoot)) throw new Error(`the coordinator ${installed.file} is inside the managed snapshots root ${snapshotsRoot}: only the installed coordinator runs; nothing was started`);
153
+ if (inside(installed.file, root) && checkout !== root) throw new Error(`the coordinator ${installed.file} is inside the source snapshot ${root}: it would be the candidate's own bytes, never the trusted installed coordinator; nothing was started`);
154
+ }
74
155
  if (typeof qualification !== "string" || !fs.existsSync(qualification)) throw new Error("release needs the unit qualification receipt (written by the gate after the qualify stage); nothing is dispatched without it");
75
156
  const registry = unit.kind === "npm" ? "npm" : "crates";
76
- const pipeline = path.join(root, "scripts", "ci", "rk-pipeline.mjs");
157
+ // rk-pipeline (the canary, the preflight rows, the dispatch) makes its broker calls as ROOT and ADAPTER requests, and the broker refuses any
158
+ // such request whose workspace is inside a managed snapshot (service.rs "a root request cannot run inside a managed snapshot"). So it runs from
159
+ // the PRIMARY CHECKOUT (`checkout`; `root` is the snapshot, which stays the source of the bytes that were qualified). rk-preflight proves the
160
+ // commit itself: it refuses a checkout whose tracked files are modified, and every row reads HEAD's committed bytes (resolveRowWorkspace
161
+ // compares each consumer manifest with `git show <sha>`); rk-pipeline dispatch pins HEAD to origin/main. Here HEAD must also be the
162
+ // qualified commit, or the pre-flight and the dispatch would prove another commit than the one that was packaged.
163
+ if (checkout !== root) {
164
+ const head = run("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" });
165
+ if (head.status !== 0 || String(head.stdout ?? "").trim() !== commit) throw new Error(`the primary checkout ${checkout} is not at the qualified commit ${commit.slice(0, 12)} (HEAD ${String(head.stdout ?? "").trim().slice(0, 12) || "unreadable"}); the release runs rk-pipeline from the checkout, so it proves nothing else, and nothing was started`);
166
+ }
167
+ // GB-07: the coordinator judges the primary checkout (the broker refuses a root request whose workspace is inside a managed snapshot), and the candidate's logic is protected as well: HEAD is the
168
+ // qualified commit (above) AND the checkout holds exactly that commit's bytes: no tracked file modified and nothing untracked under scripts/ci or packages/release (an untracked sibling is
169
+ // importable), checked BEFORE the first coordinator step starts. A modified or newer coordinator in the live checkout therefore can never stand in for the candidate's own.
170
+ if (checkout !== root) {
171
+ const dirty = run("git", ["status", "--porcelain", "--", "scripts/ci", "packages/release"], { cwd: checkout, encoding: "utf8" });
172
+ const tracked = run("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: checkout, encoding: "utf8" });
173
+ const changed = [dirty, tracked].map((r) => (r.status === 0 ? String(r.stdout ?? "").trim() : null));
174
+ if (changed.some((c) => c === null)) throw new Error(`the primary checkout ${checkout} cannot be read for modified coordinator files; nothing is dispatched`);
175
+ const lines = [...new Set(changed.flatMap((c) => c.split("\n").filter(Boolean)))];
176
+ if (lines.length) throw new Error(`the primary checkout ${checkout} holds modified or untracked files (${lines.slice(0, 4).map((l) => l.trim()).join("; ")}${lines.length > 4 ? "; ..." : ""}); the coordinator that dispatches ${unit.name} must be exactly the qualified commit ${commit.slice(0, 12)}'s bytes, so nothing is dispatched`);
177
+ }
77
178
  const stateFile = platform === "win32" ? "D:\\scratch\\rk-pipeline\\state.json" : "/Volumes/D/scratch/rk-pipeline/state.json";
78
- const step = (label, args, { accept = [0], acceptWhen = null } = {}) => {
179
+ const stepRun = pipeline
180
+ ? (args) => pipeline.runPipeline({ argv: args, root: checkout, env, platform, stateFile })
181
+ : (args) => run(process.execPath, [installed.file, ...args], { cwd: checkout, env, stdio: ["ignore", "pipe", "pipe"] });
182
+ const step = async (label, args, { accept = [0], acceptWhen = null } = {}) => {
79
183
  out(`rightkit-gate: [release] ${label}`);
80
- const r = run(process.execPath, [pipeline, ...args], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
184
+ const r = await stepRun(args);
81
185
  const accepted = r.status === 0 || (accept.includes(r.status) && (!acceptWhen || acceptWhen(r)));
82
186
  if (!accepted) {
83
187
  const said = `${String(r.stdout ?? "")}\n${String(r.stderr ?? "")}`.split("\n").map((l) => l.trim()).filter((l) => l && !l.includes("note:")).slice(-4).join(" | ");
@@ -85,21 +189,30 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
85
189
  }
86
190
  return r;
87
191
  };
192
+ // The candidate's identity is explicit: the commit and tree the gate qualified and the artifact digest it packaged (never read from files under the checkout). A dispatch record for the commit is the
193
+ // call's own only when it names the same tree and the same artifact digest; one that names another, or none, is ambiguous and never adopted.
194
+ const identity = { tree, artifactSha256: artifact?.artifactSha256 ?? null };
88
195
  const findDispatch = () => {
89
196
  const found = Object.values(readJson(stateFile).dispatches ?? {}).filter((d) => d.unit === unit.name && d.sha === commit && d.registry === registry);
90
197
  if (found.length > 1) throw new Error(`${found.length} dispatch records for ${unit.name} at ${commit.slice(0, 12)} in rk-pipeline state; reconcile before any retry`);
91
- return found[0];
198
+ const [record] = found;
199
+ if (record) {
200
+ if (identity.tree !== null && record.tree === undefined || identity.artifactSha256 !== null && record.artifactSha256 === undefined) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names no tree or artifact digest (no identity), so it cannot be reconciled with this candidate; reconcile rk-pipeline state by hand`);
201
+ if (identity.tree !== null && record.tree !== identity.tree) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names tree ${String(record.tree).slice(0, 12)}, not the candidate's ${identity.tree.slice(0, 12)}; reconcile rk-pipeline state by hand`);
202
+ if (identity.artifactSha256 !== null && record.artifactSha256 !== identity.artifactSha256) throw new Error(`the dispatch record of ${unit.name} at ${commit.slice(0, 12)} names artifact digest ${String(record.artifactSha256).slice(0, 12)}, not the candidate's ${identity.artifactSha256.slice(0, 12)}; reconcile rk-pipeline state by hand`);
203
+ }
204
+ return record;
92
205
  };
93
206
  // The pipeline canary (rk-release-canary: the candidate's `right-release doctor` against every app on both hosts, versus the
94
207
  // registry's version) runs before the pre-flight. rk-pipeline exits 2 both for "no pipeline canary for this unit" and for any
95
208
  // internal error, so 2 is accepted only when it says the former (reviewer, 2026-10-27); a canary error never reads as no canary.
96
- step(`pipeline canary of ${unit.name} (doctor on every app, both hosts, candidate vs registry)`, ["canary", unit.name],
209
+ await step(`pipeline canary of ${unit.name} (doctor on every app, both hosts, candidate vs registry)`, ["canary", unit.name, "--sha", commit],
97
210
  { accept: [2], acceptWhen: (r) => /no pipeline canary for this unit/.test(`${r.stdout ?? ""}\n${r.stderr ?? ""}`) });
98
- step(`local pre-flight of ${unit.name} through the broker`, ["preflight", unit.name, "--registry", registry]);
99
- step(`broker-admitted hosted qualification dispatch of ${unit.name} at ${commit.slice(0, 12)}`, ["dispatch", unit.name, "--registry", registry, "--qualification", qualification]);
211
+ await step(`local pre-flight of ${unit.name} through the broker`, ["preflight", unit.name, "--registry", registry, "--sha", commit]);
212
+ await step(`broker-admitted hosted qualification dispatch of ${unit.name} at ${commit.slice(0, 12)}`, ["dispatch", unit.name, "--registry", registry, "--sha", commit, "--qualification", qualification]);
100
213
  const dispatched = findDispatch();
101
214
  if (!dispatched?.runId || !dispatched.runAttempt) throw new Error(`no dispatch record with a run for ${unit.name} at ${commit.slice(0, 12)}; nothing is claimed (reconcile rk-pipeline state before any retry)`);
102
- step(`waiting for publish.yml run ${dispatched.runId}, its publish-direct upload run and the registry`, ["watch", "--wait"]);
215
+ await step(`waiting for publish.yml run ${dispatched.runId}, its publish-direct upload run and the registry`, ["watch", "--wait"]);
103
216
  const done = findDispatch();
104
217
  if (done?.status !== "published" || done.version !== unit.version || !done.uploadRunId) throw new Error(`${unit.name}: the watch ended without a published record (${done?.status ?? "none"}${done?.conclusion ? `: ${done.conclusion}` : ""}); nothing is claimed`);
105
218
  const receipt = readPublicationReceipt({ uploadRunId: done.uploadRunId, runId: done.runId, runAttempt: done.runAttempt, run, env, scratch });
@@ -116,7 +229,10 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
116
229
  const rows = (receipt.receipts ?? []).filter((r) => r.unit === unit.name && r.version === unit.version);
117
230
  if (rows.length !== 1) throw new Error(`${unit.name}@${unit.version}: the publication receipt holds ${rows.length} row(s) for this unit; exactly one is required`);
118
231
  const [row] = rows;
119
- if (row.published !== true || row.registryVerified !== true || row.outcome !== "published" || !/^[0-9a-f]{64}$/.test(row.sha256 ?? "")) throw new Error(`${unit.name}@${unit.version}: the publication receipt of upload run ${done.uploadRunId} records no verified publication (${row.outcome})`);
232
+ // The shared success definition (publication-outcomes.mjs, GB-45): `published` and the uploader's verified reconciliation of an earlier attempt's
233
+ // identical bytes (`published-by-earlier-attempt`) are both success; the registry readback below still has to serve the receipt's own hash.
234
+ const rowProblem = publicationRowProblem(row);
235
+ if (rowProblem) throw new Error(`${unit.name}@${unit.version}: the publication receipt of upload run ${done.uploadRunId} records no verified publication (${row.outcome}): ${rowProblem}`);
120
236
  // Read the registry back independently of the upload run: npm's dist.shasum (sha1) and the served tarball's sha256 (the receipt
121
237
  // row carries sha256 + sha1, not the sha512 integrity); crates: the sparse index cksum (sha256).
122
238
  const served = await registryBytes({ unit, registry, run, env, expected: row, scratch });
@@ -126,6 +242,7 @@ export async function releaseUnit({ unit, root, commit, executor, env, artifact,
126
242
  registry, runId: done.runId, runAttempt: done.runAttempt, requestId: done.requestId ?? null, uploadRunId: done.uploadRunId,
127
243
  hostedSha256: row.sha256, hostedSha1: row.sha1 ?? null, registryVerified: true,
128
244
  localArtifactSha256: artifact?.artifactSha256 ?? null, hostedEqualsLocal: artifact ? artifact.artifactSha256 === row.sha256 : null,
245
+ coordinator: installed,
129
246
  };
130
247
  }
131
248
 
@@ -217,7 +334,7 @@ export function knownRedEntry({ unit, root, platform, toRelease = false, readTex
217
334
  * ({ start(stage), end(stage) } returning true or false) is the door's ledger verify around each stage (7.08); `envExtra` carries the
218
335
  * Cargo output pin. Without `source` every behavior is the checkout route's, unchanged.
219
336
  */
220
- export async function runUnitGate({ selector, to, root, commonDir, commit, tree, executor, env, platform, check, source = null, stageGuard = null, envExtra = {}, loadOwner = () => import("./local-publish.mjs"), out = console.log, err = console.error, releaser = null, resolve = resolveUnit, writeReport, fetchReceipt = readBrokerReceipt, stageCommands = defaultStageCommands, readKnownRed }) {
337
+ export async function runUnitGate({ selector, to, root, commonDir, commit, tree, executor, env, platform, check, source = null, stageGuard = null, envExtra = {}, loadOwner = () => import("./local-publish.mjs"), out = console.log, err = console.error, releaser = null, loadPipeline = loadInGatePipeline, resolve = resolveUnit, writeReport, fetchReceipt = readBrokerReceipt, stageCommands = defaultStageCommands, readKnownRed, verifyReceipts = verifyQualificationReceipts, selfGit = null }) {
221
338
  const stages = stagesTo(to);
222
339
  if (!stages) { err(`rightkit-gate: invalid unit stage ${to}`); return 2; }
223
340
  let unit;
@@ -229,7 +346,8 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
229
346
  if (red) { err(`rightkit-gate: BLOCKED: ${selector} is listed in ci/known-red.json for ${red.host} (${red.key}: ${red.reason}); a known-red unit is not compiled, packaged, qualified or published. Fix the defect and remove the entry first`); return 1; }
230
347
  } catch (error) { err(`rightkit-gate: BLOCKED: ${error.message}`); return 1; }
231
348
  }
232
- const scratch = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch";
349
+ // GB-29: a Linux origin has no managed volume; its scratch is the gate's own state directory (XDG state), never a Mac path.
350
+ const scratch = platform === "win32" ? "D:\\scratch" : platform === "linux" ? path.join(env.XDG_STATE_HOME || path.join(env.HOME || "/tmp", ".local", "state"), "rightkit", "scratch") : "/Volumes/D/scratch";
233
351
  const bundle = path.join(scratch, `rk-unit-${randomBytes(12).toString("hex")}`);
234
352
  const argv = ["--local", unit.kind === "npm" ? "--package" : "--crate", unit.name, "--workspace", root, "--qualify-only", "--bundle", bundle];
235
353
  const runtimeDir = path.dirname(fileURLToPath(import.meta.url));
@@ -238,21 +356,44 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
238
356
  const report = { schema: "rightkit.unit-gate-report/1", unit, commit, tree, runtime, executor: executor.kind, stages: {} };
239
357
  const save = () => { if (writeReport) writeReport(report); };
240
358
  let state, owner, artifact, currentStage, checkTier = null;
241
- // A run headed to release has its compile and package broker jobs sent with a nonce bound to this commit, tree, unit and stage
242
- // (RIGHTKIT_ATTEST_NONCE), so each job's signed receipt can be verified hosted for exactly that and nothing else.
243
- const attested = stages.includes("release") && executor?.kind === "broker";
359
+ // A broker-host run that reaches the qualify stage (--to qualify, or --to release) has its compile and package jobs attested: a pnpm job
360
+ // is sent the stage nonce, a carried Cargo call a nonce derived from it, both bound to this commit, tree, unit and stage, so each job's
361
+ // signed receipt can be verified hosted for exactly that and nothing else. `--to qualify` is the sanctioned no-upload route to that
362
+ // evidence: it writes the qualification and verifies it locally exactly as the hosted admission will, and stops there.
363
+ const attested = stages.includes("qualify") && executor?.kind === "broker";
364
+ // GB-29: a direct Linux origin has no broker to attest its stages; it records its own direct-origin evidence (direct-origin.mjs), verified locally as the hosted side will.
365
+ const directLinux = stages.includes("qualify") && executor?.kind === "direct" && platform === "linux";
366
+ const directStages = {};
367
+ const directQualification = () => {
368
+ const registry = unit.kind === "npm" ? "npm" : "crates";
369
+ const record = buildDirectOrigin({ repository: RIGHTKIT_REPOSITORY, commit, tree, unit, registry, platform, artifact: { artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 }, stages: directStages });
370
+ verifyDirectOrigin(record, { repository: RIGHTKIT_REPOSITORY, commit, tree, unit, registry, ...(unit.kind === "npm" ? { contentSha256: artifact.contentSha256 } : { artifactSha256: artifact.artifactSha256 }) });
371
+ return writeQualification({ report, artifact, unit, commit, tree, runtime, executor, directOrigin: record });
372
+ };
244
373
  const stageEnv = async (stage, action) => {
245
374
  const run = startRun({ commonDir, commit, tree, stage });
246
- const nonce = attested && ATTESTED_STAGES.includes(stage) ? { RIGHTKIT_ATTEST_NONCE: qualificationNonce({ commit, tree, unit: `${unit.kind}:${unit.name}`, stage }) } : {};
375
+ const nonce = attested && ATTESTED_STAGES.includes(stage) ? { [ATTEST_STAGE_NONCE_ENV]: qualificationNonce({ commit, tree, unit: `${unit.kind}:${unit.name}`, stage }) } : {};
247
376
  try { return await action({ ...env, ...envExtra, ...nonce, ...run.env }); } finally { endRun(run); }
248
377
  };
378
+ // The hosted admission's own verification of the qualification's receipts, run here against the commit before anything is dispatched
379
+ // (or, for --to qualify, before the run reports success): what it would refuse, this refuses first, naming why.
380
+ const gitAt = selfGit ?? ((...args) => {
381
+ const r = defaultRun("git", args, { cwd: source?.checkout ?? root });
382
+ if (r.error || r.status !== 0) throw new Error(`git ${args[0]} failed`);
383
+ return r.stdout;
384
+ });
385
+ const selfCheck = (file) => {
386
+ try { verifyReceipts(fs.readFileSync(file, "utf8"), { sha: commit, git: gitAt }); }
387
+ catch (error) { throw new Error(`the local check of the qualification's signed broker receipts failed; the hosted admission would refuse it, so nothing is dispatched: ${String(error.message).replace(/^publication admission refused: /, "")}`); }
388
+ };
249
389
  const receipts = {};
250
390
  const collectReceipts = (stage, ids) => {
251
391
  if (!attested || !ATTESTED_STAGES.includes(stage)) return;
252
392
  // Every job of the stage is fetched; the stage owes a signed receipt for EVERY command it runs (hosted admission requires the same),
253
393
  // so a missing one fails here, before any dispatch, naming it. Jobs that are none of the stage's commands are not carried.
254
394
  const fetched = [...new Set(ids ?? [])].map((id) => fetchReceipt(id));
255
- const { assigned, missing } = assignJobs(stageCommands({ unit, root, platform })[stage], fetched.map((r) => ({ id: r.id, payload: r.provenance.payload })));
395
+ // A carried cargo command is bound to the directory of the primary checkout it ran in (source.checkout; stage-commands.mjs requestIs).
396
+ const { assigned, missing } = assignJobs(stageCommands({ unit, root, platform })[stage], fetched.map((r) => ({ id: r.id, payload: r.provenance.payload })), { checkout: source?.checkout ?? null });
256
397
  if (missing.length) throw new Error(`the ${stage} stage's ${describeCommand(missing[0])} has no broker job receipt (${missing.length} required command(s) missing)`);
257
398
  receipts[stage] = fetched.filter((r) => assigned.has(r.id));
258
399
  };
@@ -277,21 +418,50 @@ export async function runUnitGate({ selector, to, root, commonDir, commit, tree,
277
418
  if (checkTier !== "release" && await check("release") !== 0) throw new Error("release-tier check failed");
278
419
  const qualified = await stageEnv(stage, (runEnv) => owner.qualifyLocalUnit(state, { env: runEnv }));
279
420
  verifyArtifact(qualified);
280
- if (!qualified.qualifiedOnly || !qualified.evidencePath || qualified.artifactSha256 !== artifact.artifactSha256) throw new Error("qualification did not bind the packaged artifact");
421
+ if (!qualified.qualifiedOnly || !qualified.evidencePath || qualified.artifactSha256 !== artifact.artifactSha256 || qualified.contentSha256 !== artifact.contentSha256) throw new Error("qualification did not bind the packaged artifact");
281
422
  artifact = qualified;
282
423
  }
283
424
  let released = null;
284
425
  if (stage === "release") {
285
426
  // The qualification receipt the dispatcher carries to publish.yml: the hosted run is bound to these bytes and stages.
427
+ if (directLinux) {
428
+ // GB-29: the direct-origin qualification is written and verified exactly as the hosted side will. Dispatching it needs the publish workflow's `direct` admission input, which is the
429
+ // reserved publication authorization (GC-T22): nothing is dispatched from a direct host until that input exists.
430
+ const written = directQualification();
431
+ throw new Error(`RK_DIRECT_RELEASE_HELD: the direct-origin qualification ${written} was written and verified; the committed publish workflow admits \`broker\` only, and adding \`direct\` is the reserved publication authorization (GATE-CONTRACT GC-T22, GC-L01), so nothing was dispatched`);
432
+ }
286
433
  requireBrokerHost(executor); // a direct host has no broker receipts to carry: refuse for that reason, not for the missing receipts
287
434
  const qualification = writeQualification({ report, artifact, unit, commit, tree, runtime, executor });
288
- released = await stageEnv(stage, (runEnv) => (releaser ?? releaseUnit)({ unit, root, commit, executor, env: runEnv, artifact, qualification, platform, out }));
435
+ selfCheck(qualification);
436
+ // GB-07: the default release runs the trusted installed coordinator IN this process (loadInGatePipeline: pinned to the gate's own installed bytes, never a file under the snapshot or the checkout);
437
+ // a coordinator that cannot be loaded starts nothing. An injected releaser (a test, or another route) owns its own coordinator.
438
+ released = await stageEnv(stage, async (runEnv) => (releaser ?? releaseUnit)({ unit, root, checkout: source?.checkout ?? root, commit, tree, executor, env: runEnv, artifact, qualification, platform, out,
439
+ ...(releaser ? {} : { pipeline: await loadPipeline({ root: source ? root : null, snapshotsRoot: managedSnapshotsRoot({ env: runEnv, platform }), pathApi: platform === "win32" ? path.win32 : path.posix }) }) }));
289
440
  }
290
441
  if (stageGuard && !(await stageGuard.end(stage))) throw new Error(`RK_CHECK_ERROR: the ${stage} stage's result covers no known bytes (a snapshot byte changed, or a write is outside the ledger)`);
291
- report.stages[stage] = { ok: true, ...(stage === "package" || stage === "qualify" ? { artifactFile: artifact.artifactFile, artifactSha256: artifact.artifactSha256 } : {}), ...(receipts[stage] ? { receipts: receipts[stage] } : {}), ...(released ?? {}) };
442
+ if (directLinux && (stage === "compile" || stage === "package")) {
443
+ // The stage guard confirmed the snapshot was the commit's bytes at start and at exit; without one a direct origin has no such evidence and records none.
444
+ if (!stageGuard) throw new Error(`a direct Linux origin needs the snapshot stage guard to evidence the ${stage} stage (RK_DIRECT_ORIGIN_REFUSED)`);
445
+ let command = `${stage} ${unit.kind}:${unit.name}`;
446
+ try { command = (stageCommands({ unit, root, platform })[stage] ?? []).map(describeCommand).join("; ") || command; } catch { /* the stage name alone identifies the command */ }
447
+ directStages[stage] = directStageEvidence({ stage, commit, tree, command, ...(stage === "package" ? { outputSha256: artifact.artifactSha256 } : {}) });
448
+ }
449
+ report.stages[stage] = { ok: true, ...(stage === "package" || stage === "qualify" ? { artifactFile: artifact.artifactFile, artifactSha256: artifact.artifactSha256, ...(artifact.contentSha256 ? { contentSha256: artifact.contentSha256 } : {}) } : {}), ...(receipts[stage] ? { receipts: receipts[stage] } : {}), ...(released ?? {}) };
292
450
  save();
293
451
  out(`rightkit-gate: [${stage}] ${unit.kind}:${unit.name} passed`);
294
452
  }
453
+ if (directLinux && !stages.includes("release")) {
454
+ currentStage = "qualify";
455
+ const qualification = directQualification();
456
+ out(`rightkit-gate: [qualify] ${unit.kind}:${unit.name} direct-origin qualification ${qualification} verified locally; nothing was dispatched`);
457
+ }
458
+ if (attested && !stages.includes("release")) {
459
+ // --to qualify on a broker host: the qualification is written beside the artifact and verified as the hosted run will, and no upload exists.
460
+ currentStage = "qualify";
461
+ const qualification = writeQualification({ report, artifact, unit, commit, tree, runtime, executor });
462
+ selfCheck(qualification);
463
+ out(`rightkit-gate: [qualify] ${unit.kind}:${unit.name} qualification ${qualification} verified locally; nothing was dispatched`);
464
+ }
295
465
  return 0;
296
466
  } catch (error) {
297
467
  if (currentStage) { report.stages[currentStage] = { ok: false }; save(); }
package/upload-large.mjs CHANGED
File without changes
package/web-build.mjs CHANGED
@@ -75,8 +75,8 @@ export function runWebBuild(steps, { appRoot, env = process.env, dryRun = false,
75
75
  log(`${dryRun ? "[dry-run] " : ""}${[invocation.cmd, ...invocation.args].join(" ")}`);
76
76
  if (dryRun) continue;
77
77
  // A web build is a build: through the one door (build-launch.mjs), a broker job on a managed host.
78
- const target = door(invocation.cmd, invocation.args, { env: invocation.env, cwd: invocation.cwd, exists: existsSync });
79
- const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: invocation.env, stdio: "inherit" });
78
+ const target = door(invocation.cmd, invocation.args, { env: invocation.env, stepEnv: step.env, cwd: invocation.cwd, exists: existsSync });
79
+ const result = spawnSyncFn(target.command, target.args, { cwd: invocation.cwd, env: target.env ? { ...invocation.env, ...target.env } : invocation.env, stdio: "inherit" });
80
80
  if (result.error) throw result.error;
81
81
  if (result.status !== 0) return result.status ?? 1;
82
82
  }
@@ -19,6 +19,9 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
19
19
  import path from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { spawnSync } from "./background-process.mjs";
22
+ import { buildLaunch, BUILD_LAUNCH_REFUSED } from "./build-launch.mjs";
23
+ import { doorRefusal } from "./door.mjs";
24
+ import { resolveCommand } from "./development.mjs";
22
25
  import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
23
26
  import { resolveArtifactPath, splitArtifactContext } from "./release-artifact-path.mjs";
24
27
  import { signingReceiptPath } from "./signing-receipts.mjs";
@@ -160,8 +163,23 @@ function sha256File(file, read) {
160
163
  export function defaultWinPackageDeps({ appRoot, target, env }) {
161
164
  const runSync = (command, args, options) => spawnSync(command, args, options);
162
165
  const spawn = (command, args, options) => {
163
- const shell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(command);
164
- return runSync(command, args, { ...options, shell, stdio: "inherit" });
166
+ // `pnpm` and `node` start as argv arrays under this very node (resolveCommand: the pnpm CLI file, never the pnpm.cmd shim), with no shell: a
167
+ // shell would re-quote `tauri --config <json>` and let an argument inject (L8). Only a command resolved to something else keeps the old
168
+ // rule (a shell for anything that is not an .exe or .com on Windows).
169
+ const direct = command === "pnpm" || command === "node";
170
+ // GB-28 (BR11): a compile-capable step takes the one door (build-launch.mjs) first. On a broker-managed host that is a carried adapter job
171
+ // (or a refusal, before anything starts); on a direct host it is the plain command, resolved to its argv form below. Nothing here reaches
172
+ // the shared toolchain around the door.
173
+ let launch = { command, args };
174
+ if (direct) {
175
+ const stepEnv = options?.env ?? env;
176
+ let planned;
177
+ try { planned = buildLaunch(command, args, { env: stepEnv, cwd: options?.cwd ?? appRoot, exists: existsSync }); }
178
+ catch (error) { if (error?.code === BUILD_LAUNCH_REFUSED) return { error, status: null, signal: null, stdout: "", stderr: String(error.message) }; throw error; }
179
+ launch = planned.command === command ? { ...resolveCommand(command, args, { root: options?.cwd ?? appRoot, env: stepEnv }), ...(planned.env ? { env: planned.env } : {}) } : planned;
180
+ }
181
+ const shell = !direct && process.platform === "win32" && !/\.(?:exe|com)$/i.test(command);
182
+ return runSync(launch.command, launch.args, { ...options, ...(launch.env ? { env: { ...(options?.env ?? env), ...launch.env } } : {}), shell, windowsHide: true, stdio: "inherit" });
165
183
  };
166
184
  const receiptFor = (phase) => signingReceiptPath({ appRoot, target, platform: "win", phase, stateRoot: env.RIGHT_RELEASE_STATE_ROOT });
167
185
  return {
@@ -219,6 +237,12 @@ function locate(declared, { env, fs, split = false }) {
219
237
  export function runWinPackageRecipe({ appRoot, config = {}, target, deps = {} } = {}) {
220
238
  const plan = validateWinPackage({ appRoot, config, target });
221
239
  const env = deps.env ?? process.env;
240
+ // GB-28 (GATE one entry): this exported helper is not a door of its own. With its real effects (no injected spawn) it refuses, before any
241
+ // compile, unless a live door run launched it; a dry run and the fakes a test injects start nothing real.
242
+ if (!deps.spawn && deps.dryRun !== true) {
243
+ const refusal = (deps.doorRefusal ?? doorRefusal)({ cwd: plan.appRoot, env, command: "win-package-recipe" });
244
+ if (refusal) throw new WinPackageConfigError([refusal]);
245
+ }
222
246
  const base = defaultWinPackageDeps({ appRoot: plan.appRoot, target, env });
223
247
  const d = { ...base, ...deps, env, fs: { ...base.fs, ...(deps.fs ?? {}) } };
224
248
  const { fs } = d;
Binary file
@@ -1,6 +0,0 @@
1
- {
2
- "schema": 1,
3
- "sha256": "a41ce13fd3bac31db0230d70bed20db0ed9bf554bdae2255bdf2064589b48f29",
4
- "identity": "Developer ID Application: Adrian D'souza (6KLGD3LLKF)",
5
- "codesign": "strict-valid"
6
- }