@rightkit/release 0.6.13 → 0.6.14

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 (109) 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/darwin/rightkit-swap +0 -0
  10. package/bin/rightkit-gate.mjs +1190 -337
  11. package/broker-client.mjs +106 -0
  12. package/broker-exception.mjs +211 -0
  13. package/broker-handshake.mjs +160 -0
  14. package/broker-pins.mjs +27 -8
  15. package/broker-wire.mjs +262 -0
  16. package/build-launch.mjs +364 -12
  17. package/build-ownership.mjs +117 -49
  18. package/build-release.mjs +3 -2
  19. package/cache-command.mjs +7 -2
  20. package/candidate-build-receipt.mjs +96 -0
  21. package/candidate-receipt-command.mjs +29 -0
  22. package/cargo-contract.mjs +4 -3
  23. package/cargo-ledger.mjs +1 -1
  24. package/cargo-license-notices.mjs +2 -2
  25. package/cargo-shapes.mjs +332 -0
  26. package/cargo-target-shim.mjs +44 -5
  27. package/cargo-target.mjs +5 -0
  28. package/carried-adapter-run.mjs +31 -0
  29. package/carried-cargo-run.mjs +9 -0
  30. package/carried-cargo.mjs +237 -0
  31. package/carrier-census.mjs +124 -0
  32. package/class-a-launch-guard.mjs +266 -33
  33. package/cli/right-release.mjs +2 -0
  34. package/clippy-route.mjs +23 -11
  35. package/compile-inputs.mjs +9 -12
  36. package/control-plane.mjs +18 -0
  37. package/deps-cargo-lock.mjs +208 -0
  38. package/deps.mjs +5 -0
  39. package/deterministic-scans.mjs +273 -44
  40. package/development-signing.mjs +14 -1
  41. package/development.mjs +109 -24
  42. package/direct-origin.mjs +107 -0
  43. package/disk-command.mjs +1 -0
  44. package/disk-policy.mjs +18 -3
  45. package/freshness-census.mjs +206 -58
  46. package/gate-executor.mjs +30 -12
  47. package/gate-stages.mjs +93 -16
  48. package/github-release.mjs +2 -1
  49. package/github-upload-gate.mjs +2 -1
  50. package/install-tauri-authenticode.mjs +346 -0
  51. package/ios-dev.mjs +77 -23
  52. package/ios-package-recipe.mjs +100 -41
  53. package/linux-artifacts.mjs +17 -2
  54. package/linux-qualifier.mjs +83 -0
  55. package/local-publish.mjs +36 -14
  56. package/local-runtime.mjs +2 -2
  57. package/lock-identity.mjs +138 -0
  58. package/lsclean.sh +0 -0
  59. package/managed-cargo.mjs +151 -44
  60. package/managed-roots.mjs +130 -12
  61. package/media-sidecar-catalog.mjs +1 -1
  62. package/native-adapters.mjs +411 -0
  63. package/native-helpers.mjs +26 -18
  64. package/native-tool-planning.mjs +94 -0
  65. package/ownership-mark.mjs +182 -0
  66. package/pack-digest.mjs +122 -0
  67. package/package.json +12 -12
  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 +14 -10
  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
@@ -0,0 +1,138 @@
1
+ // Process identity for the build-ownership submit lock (ownership-mark.mjs): pid + start time + boot id. A LEAF module (node builtins and
2
+ // background-process.mjs only) because ownership-mark.mjs is imported by snapshot-broker.mjs and build-ownership.mjs, which cannot import
3
+ // each other. build-ownership.mjs keeps the full process probe for the ownership record; this is the minimum the lock needs, with the same
4
+ // meaning: a holder is dead only when its pid is gone, or the pid now runs a process that started at another time, or the host rebooted.
5
+ import fs from "node:fs";
6
+ import { spawnSync } from "./background-process.mjs";
7
+
8
+ /** Two start times of one pid are one process when they agree to within this (ps prints whole seconds). Same figure as build-ownership.mjs. */
9
+ export const LOCK_SAME_START_MS = 3000;
10
+
11
+ /** An absolute path for a system tool (a child of the gate may hold no PATH): the first candidate that exists, else the bare name. */
12
+ const systemTool = (name, candidates) => candidates.find((c) => { try { return fs.statSync(c).isFile(); } catch { return false; } }) ?? name;
13
+ const PS = () => systemTool("ps", ["/bin/ps", "/usr/bin/ps"]);
14
+ const SYSCTL = () => systemTool("sysctl", ["/usr/sbin/sysctl", "/sbin/sysctl"]);
15
+ const PERL = () => systemTool("perl", ["/usr/bin/perl"]);
16
+ const run = (spawn, command, args, env = {}) => spawn(command, args, { encoding: "utf8", env: { ...process.env, ...env }, timeout: 30_000, maxBuffer: 1 << 20 });
17
+
18
+ /** This boot's identity (Linux boot_id, macOS kern.bootsessionuuid); null where it cannot be read (Windows, a failure). */
19
+ export function lockBootId({ platform = process.platform, readFile = fs.readFileSync, spawn = spawnSync } = {}) {
20
+ try {
21
+ if (platform === "linux") { const id = String(readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim(); return /^[0-9a-f-]{16,}$/i.test(id) ? id : null; }
22
+ if (platform === "darwin") {
23
+ const r = run(spawn, SYSCTL(), ["-n", "kern.bootsessionuuid"]);
24
+ const id = String(r.stdout ?? "").trim();
25
+ return !r.error && r.status === 0 && /^[0-9a-f-]{16,}$/i.test(id) ? id : null;
26
+ }
27
+ } catch { /* unknown */ }
28
+ return null;
29
+ }
30
+
31
+ /**
32
+ * `{ alive, startedMs }` for `pid`; throws when it cannot be established (the caller then treats the holder as live: fail closed).
33
+ * Unix: kill(pid, 0) for existence, `ps -o lstart=` for the start. Windows: this process from its time origin, another from Get-Process.
34
+ */
35
+ export function lockProcessIdentity(pid, { platform = process.platform, spawn = spawnSync, kill = process.kill.bind(process) } = {}) {
36
+ if (pid === process.pid && platform === "win32") return { alive: true, startedMs: Math.round(performance.timeOrigin) };
37
+ let exists = true;
38
+ try { kill(pid, 0); } catch (e) { if (e.code === "ESRCH") exists = false; else if (e.code !== "EPERM") throw e; }
39
+ if (!exists) return { alive: false, startedMs: null };
40
+ if (platform === "win32") {
41
+ const script = `$p = Get-Process -Id ${Number(pid)} -ErrorAction SilentlyContinue; if ($p) { 'START:' + ([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds() } else { 'GONE' }`;
42
+ const r = run(spawn, "powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")]);
43
+ if (r.error || r.status !== 0) throw new Error(`powershell: ${r.error?.message ?? `exit ${r.status}`}`);
44
+ const out = String(r.stdout ?? "").trim();
45
+ if (out === "GONE") return { alive: false, startedMs: null };
46
+ const m = /START:(\d+)/.exec(out);
47
+ if (!m) throw new Error(`cannot read the start time of pid ${pid}`);
48
+ return { alive: true, startedMs: Number(m[1]) };
49
+ }
50
+ const r = run(spawn, PS(), ["-o", "lstart=", "-p", String(pid)], { LC_ALL: "C", LANG: "C" });
51
+ if (r.error) throw new Error(`ps: ${r.error.message}`);
52
+ const text = String(r.stdout ?? "").trim();
53
+ if (text === "") { if (r.status === 1) return { alive: false, startedMs: null }; throw new Error(`ps gave no start time for pid ${pid}`); }
54
+ const startedMs = Date.parse(text.replace(/\s+/g, " "));
55
+ if (!Number.isFinite(startedMs)) throw new Error(`cannot read the start time of pid ${pid} (${text})`);
56
+ return { alive: true, startedMs };
57
+ }
58
+
59
+ /** This process's identity as a lock holder records it. Cached: a process's start never changes. */
60
+ let cachedSelf = null;
61
+ export function selfLockIdentity(options = {}) {
62
+ if (options.fresh !== true && cachedSelf) return cachedSelf;
63
+ const id = lockProcessIdentity(process.pid, options);
64
+ const self = { pid: process.pid, startedMs: id.startedMs, bootId: lockBootId(options) };
65
+ if (options.fresh !== true) cachedSelf = self;
66
+ return self;
67
+ }
68
+
69
+ // ---- the gate's identity as RustRight reads it (BROKER-CONTRACT section 16, GB-76) ----
70
+ // The carrier owner record names the gate by `{ pid, startTimeMs }`, and the broker judges it with `rustright_process::identity_state`: the recorded start time must EQUAL what RustRight reads
71
+ // for that pid (Same), or the pid is `Reused`. The lock identity above tolerates a few seconds; this does not, so it computes RustRight's own figure on each platform:
72
+ // Linux `/proc/<pid>/stat` field 22 (start ticks since boot) and `/proc/stat` btime: `btime * 1000 + ticks * 1000 / CLK_TCK` (integer division)
73
+ // macOS the process's kernel start timeval (kinfo_proc's first field, `sysctl -b kern.proc.pid.<pid>`: int64 seconds, int32 microseconds), `(sec * 1e6 + usec) / 1000` floored
74
+ // Windows GetProcessTimes' creation time as UNIX milliseconds (`Process.StartTime`, floored to a millisecond)
75
+ // `null` when the figure cannot be read: the owner record then carries null and the broker keeps the carrier for ever (unverifiable), never retires it.
76
+
77
+ /** Linux: the start time in epoch ms from a `/proc/<pid>/stat` text, `/proc/stat` text and CLK_TCK, or null. The command name may hold spaces and parentheses: fields count from the LAST `)`. */
78
+ export function linuxStartTimeMs(stat, procStat, hz) {
79
+ const close = String(stat).lastIndexOf(")");
80
+ if (close < 0) return null;
81
+ const ticks = Number(String(stat).slice(close + 1).trim().split(/\s+/)[19]);
82
+ const boot = Number(/^btime\s+(\d+)/m.exec(String(procStat))?.[1]);
83
+ if (!Number.isInteger(ticks) || ticks < 0 || !Number.isInteger(boot) || !Number.isFinite(hz) || hz <= 0) return null;
84
+ return boot * 1000 + Math.floor((ticks * 1000) / hz);
85
+ }
86
+
87
+ /** macOS: the start time in epoch ms from the bytes `sysctl -b kern.proc.pid.<pid>` prints (the start timeval leads kinfo_proc), or null. */
88
+ export function darwinStartTimeMs(bytes) {
89
+ if (!Buffer.isBuffer(bytes) || bytes.length < 12) return null;
90
+ const sec = bytes.readBigInt64LE(0); const usec = bytes.readInt32LE(8);
91
+ if (sec <= 0n || usec < 0 || usec >= 1_000_000) return null;
92
+ return Number((sec * 1_000_000n + BigInt(usec)) / 1000n);
93
+ }
94
+
95
+ /** Prints the `kinfo_proc` row of the pid in $ARGV[0] (sysctl [CTL_KERN=1, KERN_PROC=14, KERN_PROC_PID=1, pid] through SYS___sysctl, 202), or exits 1 printing nothing. */
96
+ const DARWIN_KINFO_PERL = [
97
+ 'my $mib = pack("i4", 1, 14, 1, $ARGV[0]); my $buf = "\\0" x 1024; my $len = pack("Q", 1024);',
98
+ "syscall(202, $mib, 4, $buf, $len, 0, 0) == 0 or exit 1;",
99
+ 'binmode STDOUT; print substr($buf, 0, unpack("Q", $len));',
100
+ ].join(" ");
101
+
102
+ /** `pid`'s start time as RustRight's `start_time_ms` reads it, or null when it cannot be read (gone, denied, an unknown platform). `readFile`, `spawn` and `hz` are seams for a simulated host. */
103
+ export function processStartTimeMs(pid, { platform = process.platform, readFile = fs.readFileSync, spawn = spawnSync, hz = null } = {}) {
104
+ try {
105
+ if (!Number.isInteger(pid) || pid <= 0) return null;
106
+ if (platform === "linux") {
107
+ let clock = hz;
108
+ if (clock === null) { const r = run(spawn, "getconf", ["CLK_TCK"]); const n = Number(String(r.stdout ?? "").trim()); clock = !r.error && r.status === 0 && Number.isInteger(n) && n > 0 ? n : 100; }
109
+ return linuxStartTimeMs(readFile(`/proc/${pid}/stat`, "utf8"), readFile("/proc/stat", "utf8"), clock);
110
+ }
111
+ if (platform === "darwin") {
112
+ const options = { env: { ...process.env, LC_ALL: "C" }, timeout: 30_000, maxBuffer: 1 << 20 };
113
+ const read = (r) => (r.error || r.status !== 0 ? null : darwinStartTimeMs(Buffer.isBuffer(r.stdout) ? r.stdout : Buffer.from(String(r.stdout ?? ""), "latin1")));
114
+ const named = read(spawn(SYSCTL(), ["-b", `kern.proc.pid.${pid}`], options));
115
+ if (named !== null) return named;
116
+ // macOS 27's sysctl(8) no longer resolves `kern.proc.pid.<pid>` by name ("unknown oid"), so the row is read through the numeric MIB
117
+ // [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid] (the one RustRight reads, rustright-process macos.rs) by the system perl: the same bytes, the same figure.
118
+ return read(spawn(PERL(), ["-e", DARWIN_KINFO_PERL, String(pid)], options));
119
+ }
120
+ if (platform === "win32") {
121
+ const script = `$p = Get-Process -Id ${Number(pid)} -ErrorAction SilentlyContinue; if ($p) { 'START:' + ([DateTimeOffset]$p.StartTime).ToUnixTimeMilliseconds() } else { 'GONE' }`;
122
+ const r = run(spawn, "powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")]);
123
+ if (r.error || r.status !== 0) return null;
124
+ const m = /START:(\d+)/.exec(String(r.stdout ?? ""));
125
+ return m ? Number(m[1]) : null;
126
+ }
127
+ } catch { /* unreadable: null */ }
128
+ return null;
129
+ }
130
+
131
+ /** This process's own start time as RustRight reads it. Cached: a process's start never changes. A null (unreadable) is retried on the next call. */
132
+ let cachedSelfStart = null;
133
+ export function selfStartTimeMs(options = {}) {
134
+ if (options.fresh !== true && cachedSelfStart !== null) return cachedSelfStart;
135
+ const ms = processStartTimeMs(process.pid, options);
136
+ if (options.fresh !== true && ms !== null) cachedSelfStart = ms;
137
+ return ms;
138
+ }
package/lsclean.sh CHANGED
File without changes
package/managed-cargo.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { discoverBrokerSocket, managedControlCandidates, resolveManagedRoots } from './managed-roots.mjs';
2
- import { randomUUID } from "node:crypto";
1
+ import { discoverBrokerSocket, managedControlCandidates, managedRuntimeClient, resolveManagedRoots } from './managed-roots.mjs';
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { homedir, userInfo } from "node:os";
4
- import { closeSync, existsSync, mkdirSync, openSync, readSync, rmSync, writeFileSync } from "node:fs";
4
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { spawn as spawnProcess, spawnSync } from "./background-process.mjs";
7
+ import { CARRIER_MISSING_CODE, carriedCargoMapOf, runCarriedCargo } from "./carried-cargo.mjs";
7
8
 
8
9
  /**
9
10
  * Linux, and any host that is neither Mac nor Windows, never uses the RustRight broker: WSL2, a VM, a cloud or CI runner, Linux hosted on
@@ -12,6 +13,103 @@ import { spawn as spawnProcess, spawnSync } from "./background-process.mjs";
12
13
  */
13
14
  export const directPlatform = (platform = process.platform) => platform !== "darwin" && platform !== "win32";
14
15
 
16
+ /**
17
+ * The optional Linux broker route (Adrian, 2026-10-31: "linux normally not needing broker" is not "broker not allowed on linux"). Linux stays direct
18
+ * unless the host DECLARES a broker, explicitly and only through these variables; a RustRight client on PATH, a socket name in the older broker
19
+ * variables or a shim directory never activates it. A declared route is validated here, once, for every caller (the executor, managed-cargo's
20
+ * carried seam and its client route, the gate entry): an invalid or unreachable declared broker REFUSES, it never falls back to direct cargo.
21
+ * RIGHTKIT_LINUX_BROKER_CLIENT absolute path of the installed RustRight client (`rustright`)
22
+ * RIGHTKIT_LINUX_BROKER_SOCKET absolute path of the broker's endpoint (the unix socket the client talks to)
23
+ * RIGHTKIT_LINUX_BROKER_CLIENT_SHA256 REQUIRED: the client's pinned SHA-256 (runtime identity); absent or different refuses
24
+ */
25
+ export const LINUX_BROKER_CLIENT_ENV = "RIGHTKIT_LINUX_BROKER_CLIENT";
26
+ export const LINUX_BROKER_SOCKET_ENV = "RIGHTKIT_LINUX_BROKER_SOCKET";
27
+ export const LINUX_BROKER_CLIENT_SHA256_ENV = "RIGHTKIT_LINUX_BROKER_CLIENT_SHA256";
28
+ export const LINUX_BROKER_ROUTE = "linux-broker";
29
+ export const LINUX_BROKER_REFUSAL = "RK_LINUX_BROKER_UNAVAILABLE";
30
+
31
+ /** True when this Linux (direct-platform) host declared a broker route, valid or not. Presence of any declaring variable counts, so a half-written declaration refuses. */
32
+ export const linuxBrokerDeclared = (env = process.env, platform = process.platform) =>
33
+ directPlatform(platform) && [LINUX_BROKER_CLIENT_ENV, LINUX_BROKER_SOCKET_ENV, LINUX_BROKER_CLIENT_SHA256_ENV].some((name) => env?.[name] !== undefined);
34
+
35
+ /** A host that compiles direct: a direct platform that declared no broker route. */
36
+ export const directHost = (platform = process.platform, env = process.env) => directPlatform(platform) && !linuxBrokerDeclared(env, platform);
37
+
38
+ const sha256Cache = new Map();
39
+ const LINUX_ROUTE_FS = Object.freeze({
40
+ realpath: (p) => realpathSync.native(p),
41
+ stat: (p) => statSync(p),
42
+ uid: () => (typeof process.getuid === "function" ? process.getuid() : -1),
43
+ digest: (real, meta) => {
44
+ const key = `${real}\0${meta.dev}\0${meta.ino}\0${meta.size}\0${meta.mtimeMs}`;
45
+ if (!sha256Cache.has(key)) sha256Cache.set(key, createHash("sha256").update(readFileSync(real)).digest("hex"));
46
+ return sha256Cache.get(key);
47
+ },
48
+ });
49
+
50
+ /** Why an installed path is not trusted, or null: every directory on it, and the entry itself, belongs to root or to the gate's user and is not group- or world-writable (a root-owned sticky directory such as /tmp excepted). */
51
+ function untrustedChain(real, ops, uid, { entryMode = true } = {}) {
52
+ const parts = real.split("/").filter(Boolean);
53
+ let at = "";
54
+ for (let i = -1; i < parts.length; i += 1) {
55
+ if (i >= 0) at = `${at}/${parts[i]}`;
56
+ const here = at || "/";
57
+ const last = i === parts.length - 1;
58
+ let meta;
59
+ try { meta = ops.stat(here); } catch (e) { return `${here} cannot be inspected (${e.code ?? e.message})`; }
60
+ if (meta.uid !== 0 && meta.uid !== uid) return `${here} is owned by uid ${meta.uid}, neither root nor the gate's user`;
61
+ // A shared sticky directory owned by root (/tmp) lets nobody replace another user's entry, so only it may stay writable.
62
+ const stickyShared = !last && meta.uid === 0 && (meta.mode & 0o1000) !== 0;
63
+ if ((!last || entryMode) && (meta.mode & 0o022) !== 0 && !stickyShared) return `${here} is group- or world-writable`;
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * Resolve the declared Linux broker route: `{ declared: false }` (the direct default), `{ declared: true, ok: false, code, problem }` (refuse) or
70
+ * `{ declared: true, ok: true, client, socket, clientSha256 }`. Judged: the client is an absolute, regular, executable file reached through a
71
+ * root- or gate-user-owned chain that nobody else can write, and matches the required pinned digest; the endpoint is an absolute unix
72
+ * socket on such a chain (the broker authenticates its peers itself); and no other broker endpoint is named in the environment. Whether the broker
73
+ * behind the socket answers, and what it advertises, is asked of the real client by the gate's capability handshake, which refuses the route.
74
+ * `fsOps` is for tests of the judging only (stat/realpath/uid/digest over a described tree); it is never an alternative broker.
75
+ */
76
+ export function resolveLinuxBrokerRoute({ env = process.env, platform = process.platform, fsOps } = {}) {
77
+ if (!linuxBrokerDeclared(env, platform)) return { declared: false };
78
+ const ops = { ...LINUX_ROUTE_FS, ...fsOps };
79
+ const refuse = (problem) => ({ declared: true, ok: false, code: LINUX_BROKER_REFUSAL, problem: `${LINUX_BROKER_REFUSAL}: the declared Linux broker route cannot be used: ${problem}; nothing compiled and cargo never runs direct around a declared broker` });
80
+ const absolute = (name) => {
81
+ const value = env[name];
82
+ if (typeof value !== "string" || !value) return { problem: `${name} is ${value === undefined ? "not set" : "empty"} (a declared route needs both ${LINUX_BROKER_CLIENT_ENV} and ${LINUX_BROKER_SOCKET_ENV})` };
83
+ if (!path.posix.isAbsolute(value) || value.split("/").includes("..") || value.includes("\0")) return { problem: `${name} must be an absolute path with no '..' (got ${JSON.stringify(value.slice(0, 120))})` };
84
+ return { value };
85
+ };
86
+ const uid = ops.uid();
87
+ const c = absolute(LINUX_BROKER_CLIENT_ENV); if (c.problem) return refuse(c.problem);
88
+ const k = absolute(LINUX_BROKER_SOCKET_ENV); if (k.problem) return refuse(k.problem);
89
+ let client; let clientMeta;
90
+ try { client = ops.realpath(c.value); clientMeta = ops.stat(client); } catch (e) { return refuse(`the client ${c.value} cannot be resolved (${e.code ?? e.message})`); }
91
+ if (!clientMeta.isFile?.()) return refuse(`the client ${client} is not a regular file`);
92
+ if ((clientMeta.mode & 0o111) === 0) return refuse(`the client ${client} is not executable`);
93
+ const clientProblem = untrustedChain(client, ops, uid);
94
+ if (clientProblem) return refuse(`the client is not trusted: ${clientProblem}`);
95
+ let socket; let socketMeta;
96
+ try { socket = ops.realpath(k.value); socketMeta = ops.stat(socket); } catch (e) { return refuse(`the broker endpoint ${k.value} does not exist or cannot be inspected (${e.code ?? e.message}); the broker is not running here`); }
97
+ if (!socketMeta.isSocket?.()) return refuse(`the broker endpoint ${socket} is not a unix socket`);
98
+ const socketProblem = untrustedChain(socket, ops, uid, { entryMode: false });
99
+ if (socketProblem) return refuse(`the broker endpoint is not trusted: ${socketProblem}`);
100
+ for (const other of ["RUSTRIGHT_BUILD_BROKER_SOCKET", "RIGHTKIT_BUILD_BROKER_SOCKET"]) {
101
+ if (typeof env[other] === "string" && env[other] !== "" && env[other] !== k.value && env[other] !== socket) return refuse(`${other} names another endpoint (${env[other]}); one route is declared, so the choice is ambiguous`);
102
+ }
103
+ let clientSha256;
104
+ try { clientSha256 = ops.digest(client, clientMeta); } catch (e) { return refuse(`the client ${client} cannot be read for its identity (${e.code ?? e.message})`); }
105
+ // The pin is REQUIRED (root decision, 2026-10-31): a declaration without the client's digest, or with another one, is refused.
106
+ const pinned = env[LINUX_BROKER_CLIENT_SHA256_ENV];
107
+ if (pinned === undefined || pinned === "") return refuse(`${LINUX_BROKER_CLIENT_SHA256_ENV} is not set (the declared client's SHA-256 is required; the client ${client} has ${clientSha256})`);
108
+ if (!/^[0-9a-f]{64}$/i.test(String(pinned))) return refuse(`${LINUX_BROKER_CLIENT_SHA256_ENV} is not a SHA-256 digest`);
109
+ if (String(pinned).toLowerCase() !== clientSha256) return refuse(`the client ${client} has digest ${clientSha256}, not the pinned ${String(pinned).toLowerCase()}`);
110
+ return { declared: true, ok: true, client, socket, clientSha256 };
111
+ }
112
+
15
113
  const MANAGED_AGENT_BIN = /(?:^|\/)(?:\.rightkit-managed|rightkitmanagedagent|rightkit\/build-control)\/agent-bin\/?$/i;
16
114
  function fileHead(file) {
17
115
  let fd = null;
@@ -24,53 +122,32 @@ function fileHead(file) {
24
122
  * RIGHTKIT_MANAGED_AGENT_BIN, or a script that routes through the broker, read 4 KiB at most), and the first real cargo left on it.
25
123
  * Plain `cargo` then can only be a real toolchain; with none, a cargo launch fails as missing, never through a shim.
26
124
  */
27
- export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead } = {}) {
28
- const configured = env.RIGHTKIT_MANAGED_AGENT_BIN ? path.posix.resolve(env.RIGHTKIT_MANAGED_AGENT_BIN) : null;
125
+ export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead, platform = process.platform } = {}) {
126
+ // `platform` only matters for a forced direct run on Windows (a recorded broker exception): ";" separators, backslash paths, cargo.exe.
127
+ const win = platform === "win32";
128
+ const norm = (d) => (win ? String(d).replaceAll("\\", "/") : d);
129
+ const configured = env.RIGHTKIT_MANAGED_AGENT_BIN ? path.posix.resolve(norm(env.RIGHTKIT_MANAGED_AGENT_BIN)) : null;
29
130
  const kept = []; const dropped = []; let cargo = null;
30
- for (const dir of String(env.PATH ?? "").split(":").filter(Boolean)) {
31
- const candidate = path.posix.join(dir, "cargo");
32
- const isShim = exists(candidate) && (MANAGED_AGENT_BIN.test(dir) || (configured && path.posix.resolve(dir) === configured) || /cargo-guard\.mjs|rustright/i.test(readHead(candidate)));
131
+ for (const dir of String(env.PATH ?? "").split(win ? ";" : ":").filter(Boolean)) {
132
+ const candidates = win ? ["cargo.exe", "cargo.cmd", "cargo.bat", "cargo"].map((n) => path.win32.join(dir, n)) : [path.posix.join(dir, "cargo")];
133
+ const found = candidates.find((c) => exists(c));
134
+ const isShim = Boolean(found) && (MANAGED_AGENT_BIN.test(norm(dir)) || (configured && path.posix.resolve(norm(dir)) === configured) || /cargo-guard\.mjs|rustright/i.test(readHead(found)));
33
135
  if (isShim) { dropped.push(dir); continue; }
34
136
  kept.push(dir);
35
- if (!cargo && exists(candidate)) cargo = candidate;
137
+ if (!cargo && found) cargo = found;
36
138
  }
37
- return { path: kept.join(":"), dropped, cargo };
139
+ return { path: kept.join(win ? ";" : ":"), dropped, cargo };
38
140
  }
39
141
 
40
- /** `env` for a direct host: PATH without RustRight shim directories. Unchanged on Mac and Windows. */
142
+ /** `env` for a direct host: PATH without RustRight shim directories. Unchanged on Mac, Windows and a declared Linux broker route. */
41
143
  export function directCargoEnv(env = process.env, platform = process.platform, exists = existsSync) {
42
- if (!directPlatform(platform)) return env;
144
+ if (!directHost(platform, env)) return env; // Mac, Windows, and a Linux host that declared a broker route (no direct cargo there)
43
145
  const direct = directCargoPath({ env, exists });
44
146
  return direct.dropped.length ? { ...env, PATH: direct.path } : env;
45
147
  }
46
148
 
47
- export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
48
- if (directPlatform(platform)) return undefined;
49
- const roots = managedControlCandidates({ env, platform });
50
- const paths = platform === 'win32' ? path.win32 : path.posix;
51
- // runtime-current ships the native `rustright` client.
52
- for (const root of roots) {
53
- const client = paths.join(root, "runtime-current", platform === 'win32' ? "rustright.exe" : "rustright");
54
- if (exists(client)) return client;
55
- }
56
- // RustRight's own install (2026-10-17 cutover): the Mac agent-bin shims exec this client directly.
57
- if (platform === 'darwin') {
58
- const client = path.posix.join(env.HOME || homedir(), 'Library', 'Application Support', 'RustRight', 'runtime', 'rustright');
59
- if (exists(client)) return client;
60
- // Broker-managed HOME may point at a build home; the installed native client belongs to the OS account.
61
- const accountClient = path.posix.join(userInfo().homedir, 'Library', 'Application Support', 'RustRight', 'runtime', 'rustright');
62
- if (exists(accountClient)) return accountClient;
63
- }
64
- // RustRight's own install on Windows (2026-10-18 cutover): the runtime moved out of the control root to
65
- // <drive>\.rustright\runtime, beside the control root's drive, and runtime-current was retired.
66
- if (platform === 'win32') {
67
- for (const root of roots) {
68
- const client = path.win32.join(path.win32.parse(root).root, '.rustright', 'runtime', 'rustright.exe');
69
- if (exists(client)) return client;
70
- }
71
- }
72
- return undefined;
73
- }
149
+ // The installed-runtime client lookup lives in managed-roots.mjs (a leaf module, so broker-client.mjs can use it without an import cycle).
150
+ export { managedRuntimeClient };
74
151
 
75
152
  /** Command line that runs the managed client: the native rustright client runs directly. */
76
153
  export function managedRuntimeCommand(client, args) {
@@ -95,18 +172,44 @@ export function acquireManagedRunLease({ command, env = process.env, platform =
95
172
  }
96
173
 
97
174
  export function managedHost(env, platform = process.platform) {
98
- if (directPlatform(platform)) return false; // Linux is never broker-managed (directPlatform), whatever the env names
99
- return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET)
175
+ // Linux is broker-managed only through the explicit declared route (linuxBrokerDeclared), whatever else the env names.
176
+ if (directPlatform(platform)) return linuxBrokerDeclared(env, platform);
177
+ return Boolean(env.RUSTRIGHT_BUILD_BROKER_SOCKET || env.RIGHTKIT_BUILD_BROKER_SOCKET)
100
178
  || /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent|rightkit[/\\]build-control)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
101
179
  }
102
180
 
103
- export function runCargoSync(args, options = {}, { platform = process.platform, exists = existsSync, spawn = spawnSync } = {}) {
181
+ /**
182
+ * The carried seam (rightkit#1301): on a broker host, a call made by a child of a snapshot door stage, which the gate started with the
183
+ * stage's carrier map in its environment, goes through that carrier (carried-cargo.mjs: one signed, ledgered request per call, from the
184
+ * manifest's directory, output directory from the receipt). A map that cannot be read refuses the call; it is never run uncarried.
185
+ * A direct Linux host never has a carrier; a Linux host that declared a broker route has the same one. A call made with no carrier in its environment keeps the managed client route as before; refusing a
186
+ * carrier-less compile that comes from a snapshot stage is RustRight's side (rightkit#1301 step 3).
187
+ */
188
+ function carriedSeam(args, options, { platform, spawn }) {
189
+ if (directHost(platform, options.env ?? process.env)) return null;
190
+ const carried = carriedCargoMapOf(options.env ?? process.env);
191
+ if (!carried) return null;
192
+ if (carried.error) return { status: 101, signal: null, stdout: "", stderr: `rightkit-gate: ${CARRIER_MISSING_CODE}: ${carried.error}\n`, error: Object.assign(new Error(`${CARRIER_MISSING_CODE}: ${carried.error}`), { code: CARRIER_MISSING_CODE }) };
193
+ return runCarriedCargo(args, options, { map: carried.map, spawn });
194
+ }
195
+
196
+ export function runCargoSync(args, options = {}, { platform = process.platform, exists = existsSync, spawn = spawnSync, fsOps } = {}) {
104
197
  const env = options.env ?? process.env;
198
+ const seam = carriedSeam(args, options, { platform, spawn });
199
+ if (seam) return seam;
200
+ // A Linux host that declared a broker route: every call goes to the validated client at the declared endpoint, or is refused. Never direct.
201
+ const linux = resolveLinuxBrokerRoute({ env, platform, fsOps });
202
+ if (linux.declared) {
203
+ if (!linux.ok) return unavailable(linux.problem);
204
+ const result = spawn(linux.client, ["cargo", ...args], { ...options, env: { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: linux.socket } });
205
+ if (result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) return { ...result, error: unavailable(String(result.stderr).trim()).error };
206
+ return result;
207
+ }
105
208
  const client = managedRuntimeClient(env, platform, exists);
106
209
  if (client) {
107
210
  const socket = discoverBrokerSocket(env, { platform, exists });
108
211
  if (platform === 'darwin' && (!socket || !exists(socket))) return unavailable('RightKit broker socket is unavailable');
109
- const childEnv = socket ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : env;
212
+ const childEnv = socket ? { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: socket } : env;
110
213
  const invocation = managedRuntimeCommand(client, ["cargo", ...args]);
111
214
  const result = spawn(invocation.command, invocation.args, { ...options, env: childEnv });
112
215
  if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
@@ -155,6 +258,8 @@ export function spawnCollect(command, args, options = {}, { spawnAsync = spawnPr
155
258
  });
156
259
  }
157
260
 
261
+ const carriedSeamActive = (args, options, { platform = process.platform } = {}) => !directHost(platform, options.env ?? process.env) && Boolean(carriedCargoMapOf(options.env ?? process.env));
262
+
158
263
  /**
159
264
  * Async twin of `runCargoSync`: the same client, broker-socket and darwin-never-direct selection (it asks
160
265
  * `runCargoSync` to choose, with a spawn that only records the invocation), then runs the chosen invocation
@@ -163,12 +268,14 @@ export function spawnCollect(command, args, options = {}, { spawnAsync = spawnPr
163
268
  */
164
269
  export async function runCargoAsync(args, options = {}, deps = {}) {
165
270
  const { spawnAsync = spawnCollect, ...syncDeps } = deps;
271
+ // A carried call (a snapshot stage's) is verified in process: it runs whole, then resolves.
272
+ if (carriedSeamActive(args, options, syncDeps)) return runCargoSync(args, options, syncDeps);
166
273
  let chosen;
167
274
  const probe = runCargoSync(args, options, { ...syncDeps, spawn: (command, commandArgs, commandOptions) => { chosen = { command, args: commandArgs, options: commandOptions }; return { status: 0, stdout: "", stderr: "" }; } });
168
275
  if (!chosen) return probe;
169
276
  const result = await spawnAsync(chosen.command, chosen.args, chosen.options);
170
277
  const platform = syncDeps.platform ?? process.platform;
171
- if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
278
+ if ((platform === 'darwin' || linuxBrokerDeclared(options.env ?? process.env, platform)) && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
172
279
  return { ...result, error: unavailable(String(result.stderr).trim()).error };
173
280
  }
174
281
  return result;
package/managed-roots.mjs CHANGED
@@ -1,15 +1,41 @@
1
- import { existsSync } from 'node:fs';
2
- import { homedir } from 'node:os';
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { homedir, userInfo } from 'node:os';
3
4
  import { posix, win32 } from 'node:path';
5
+ import path from 'node:path';
4
6
 
5
- /** RIGHTKIT_MANAGED_ROOT remains a data-root override for existing release callers. */
6
- export function resolveManagedRoots({ env = process.env, platform = process.platform, home = homedir(), controlRoot, dataRoot } = {}) {
7
+ /**
8
+ * The value of `RUSTRIGHT_<name>`, else of its legacy `RIGHTKIT_<name>` spelling (empty is unset). The same precedence as RustRight's resolver
9
+ * (rustright-config managed_roots.rs `env_named`): a current name always wins over the legacy one it replaces, so a stale legacy variable can never override it.
10
+ */
11
+ export const envNamed = (env, name) => {
12
+ for (const key of [`RUSTRIGHT_${name}`, `RIGHTKIT_${name}`]) if (typeof env?.[key] === 'string' && env[key] !== '') return env[key];
13
+ return undefined;
14
+ };
15
+
16
+ /**
17
+ * The managed roots (GB-63: ONE resolver, the shape of RustRight's `rustright-config::managed_roots`, exercised against its shared fixtures). The data root is `DATA_ROOT`,
18
+ * `MANAGED_ROOT`, `EXTERNAL_ROOT` (RUSTRIGHT_ names first, then the legacy RIGHTKIT_ ones), else the legacy root; the control root is `CONTROL_ROOT`, else the platform default.
19
+ * `runtimeCurrent` (the wire key, kept for compatibility) is `<control>/runtime`, the one fixed live runtime directory; the installer's own runtime sits at its final RustRight
20
+ * path, `runtimeRoot` (`RUSTRIGHT_RUNTIME_ROOT`, else RustRight's per-platform default) and `liveRuntime` = `<runtimeRoot>/runtime`, which supersedes it.
21
+ *
22
+ * `legacyRoot` is the pre-rename Mac/Windows location the shared fixtures pin on every platform; it is never a default for a host that is neither: a Linux (or
23
+ * other) host that names no data root gets `<home>/.local/state/rightkit` (the account's own state directory, where its runs, snapshots and build-ownership record
24
+ * already live), never `/Volumes/D/.rightkit-managed`, a Mac volume that a Linux gate would otherwise create as root. Everything else is RustRight's order:
25
+ * the data root is named by the same env, `controlRoot` = `dataRoot` off Mac, so a host that names its roots (every declared Linux broker, whose installer sets
26
+ * RUSTRIGHT_DATA_ROOT / RUSTRIGHT_CONTROL_ROOT) resolves exactly as `rustright-config::managed_roots` does. RustRight resolves a host with NO environment the same
27
+ * way (rustright 07829c05, pinned in the shared fixtures as "linux no-env defaults to the account state directory"). The broker's answering roots, not either
28
+ * resolver's guess, say where a running broker keeps its state.
29
+ */
30
+ export function resolveManagedRoots({ env = process.env, platform = process.platform, home = homedir(), controlRoot, dataRoot, runtimeRoot } = {}) {
7
31
  const paths = platform === 'win32' ? win32 : posix;
8
32
  const legacyRoot = platform === 'win32' ? 'D:\\.rightkit-managed' : '/Volumes/D/.rightkit-managed';
9
- dataRoot ??= env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT || legacyRoot;
10
- controlRoot ??= env.RIGHTKIT_CONTROL_ROOT || (platform === 'darwin'
33
+ const managedPlatform = platform === 'darwin' || platform === 'win32';
34
+ dataRoot ??= envNamed(env, 'DATA_ROOT') || envNamed(env, 'MANAGED_ROOT') || envNamed(env, 'EXTERNAL_ROOT') || (managedPlatform ? legacyRoot : paths.join(home, '.local', 'state', 'rightkit'));
35
+ controlRoot ??= envNamed(env, 'CONTROL_ROOT') || (platform === 'darwin'
11
36
  ? paths.join(home, 'Library', 'Application Support', 'RightKit', 'build-control') : dataRoot);
12
- return Object.freeze({ controlRoot, dataRoot, legacyRoot, runtimeCurrent: paths.join(controlRoot, 'runtime-current'),
37
+ runtimeRoot ??= envNamed(env, 'RUNTIME_ROOT') || (platform === 'darwin' ? paths.join(home, 'Library', 'Application Support', 'RustRight') : platform === 'win32' ? 'D:\\.rustright' : controlRoot);
38
+ return Object.freeze({ controlRoot, dataRoot, legacyRoot, runtimeCurrent: paths.join(controlRoot, 'runtime'), runtimeRoot, liveRuntime: paths.join(runtimeRoot, 'runtime'),
13
39
  configPath: paths.join(controlRoot, 'config.json'), socket: platform === 'win32' ? null : paths.join(controlRoot, 'broker.sock'),
14
40
  logs: paths.join(controlRoot, 'logs'), agentBin: paths.join(controlRoot, 'agent-bin'),
15
41
  agentPolicy: paths.join(controlRoot, 'agent-policy'), ownershipInstall: paths.join(controlRoot, 'ownership-install'),
@@ -18,22 +44,114 @@ export function resolveManagedRoots({ env = process.env, platform = process.plat
18
44
  : paths.join(controlRoot, 'signing-agent') });
19
45
  }
20
46
 
47
+ /**
48
+ * Where a gate run keeps evidence that must not live in the repository (failing scans' complete output, the direct executor's
49
+ * diagnostics): RIGHTKIT_SCRATCH_ROOT, else `<data root>/runs`. A managed Mac or Windows host has a data root, and so does any host
50
+ * that names one in the environment. A direct (Linux and other) host with none used to fall to the Mac data volume,
51
+ * `/Volumes/D/.rightkit-managed/runs`, and create it; its data root now defaults to the account's own state directory
52
+ * (resolveManagedRoots: `~/.local/state/rightkit`), beside the snapshots and the build-ownership record, so its runs are `~/.local/state/rightkit/runs`.
53
+ */
54
+ export function gateRunsRoot({ env = process.env, platform = process.platform, home = homedir() } = {}) {
55
+ if (env.RIGHTKIT_SCRATCH_ROOT) return env.RIGHTKIT_SCRATCH_ROOT;
56
+ return (platform === 'win32' ? win32 : posix).join(resolveManagedRoots({ env, platform, home }).dataRoot, 'runs');
57
+ }
58
+
21
59
  export function managedControlCandidates(options = {}) {
22
60
  const platform = options.platform ?? process.platform;
23
61
  if (!['darwin', 'win32'].includes(platform)) {
24
62
  const env = options.env ?? process.env;
25
- return [...new Set([options.controlRoot, env.RIGHTKIT_CONTROL_ROOT, options.dataRoot, env.RIGHTKIT_DATA_ROOT, env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT, options.clientRoot].filter(Boolean))];
63
+ return [...new Set([options.controlRoot, envNamed(env, 'CONTROL_ROOT'), options.dataRoot, envNamed(env, 'DATA_ROOT'), envNamed(env, 'MANAGED_ROOT'), envNamed(env, 'EXTERNAL_ROOT'), options.clientRoot].filter(Boolean))];
26
64
  }
27
65
  const roots = resolveManagedRoots(options);
28
66
  return [...new Set([roots.controlRoot, roots.dataRoot, options.clientRoot, roots.legacyRoot].filter(Boolean))];
29
67
  }
30
68
 
31
- export function discoverBrokerSocket(env = process.env, { exists = existsSync, ...options } = {}) {
32
- if (typeof env.RIGHTKIT_BUILD_BROKER_SOCKET === 'string' && env.RIGHTKIT_BUILD_BROKER_SOCKET) return env.RIGHTKIT_BUILD_BROKER_SOCKET;
33
- if ((options.platform ?? process.platform) === 'win32') return undefined;
34
- for (const root of managedControlCandidates({ ...options, env })) {
69
+ /** The pipe RustRight's installer enforces, and the pre-rename one still named by processes started before the cutover (which means the same broker). */
70
+ export const CANONICAL_WINDOWS_PIPE = '\\\\.\\pipe\\rustright-broker';
71
+ export const LEGACY_WINDOWS_PIPE = '\\\\.\\pipe\\rightkit-build-control';
72
+
73
+ /** The `windowsPipe` an installed broker config names (config.json of the first candidate root that has a valid one); a pre-rename name maps to the canonical pipe; anything but `\\.\pipe\<name>` is ignored. */
74
+ export function configuredWindowsPipe(roots, readFile = (file) => readFileSync(file, 'utf8')) {
75
+ for (const root of roots) {
76
+ let pipe;
77
+ try { pipe = JSON.parse(readFile(win32.join(root, 'config.json')))?.windowsPipe; } catch { continue; }
78
+ if (typeof pipe !== 'string' || !pipe.startsWith('\\\\.\\pipe\\')) continue;
79
+ const name = pipe.slice('\\\\.\\pipe\\'.length);
80
+ if (!name || /[\\/]/.test(name)) continue;
81
+ return pipe.toLowerCase() === LEGACY_WINDOWS_PIPE.toLowerCase() ? CANONICAL_WINDOWS_PIPE : pipe;
82
+ }
83
+ return undefined;
84
+ }
85
+
86
+ /**
87
+ * The broker endpoint, in RustRight's order (GB-63): an explicit `RUSTRIGHT_BUILD_BROKER_SOCKET` (else the legacy spelling) is authoritative (on Windows the pre-rename pipe means
88
+ * the canonical one); on Unix the first existing `broker.sock` among the candidate roots; on Windows there is no socket file but there is always an endpoint: the pipe the installed
89
+ * config names (a custom or migrated name is never ignored), else the canonical pipe, so a Windows shell with no environment still reaches the broker.
90
+ */
91
+ export function discoverBrokerSocket(env = process.env, { exists = existsSync, readFile, ...options } = {}) {
92
+ const platform = options.platform ?? process.platform;
93
+ const explicit = envNamed(env, 'BUILD_BROKER_SOCKET');
94
+ if (explicit) return platform === 'win32' && explicit.toLowerCase() === LEGACY_WINDOWS_PIPE.toLowerCase() ? CANONICAL_WINDOWS_PIPE : explicit;
95
+ const candidates = managedControlCandidates({ ...options, env });
96
+ if (platform === 'win32') return configuredWindowsPipe(candidates, readFile) ?? CANONICAL_WINDOWS_PIPE;
97
+ for (const root of candidates) {
35
98
  const socket = posix.join(root, 'broker.sock');
36
99
  if (exists(socket)) return socket;
37
100
  }
38
101
  return undefined;
39
102
  }
103
+
104
+ // The declared Linux broker route's variables (owner: managed-cargo.mjs, which imports this leaf module and so cannot be imported here; its names are spelled
105
+ // once more because a cycle would break the leaf property broker-client.mjs relies on). The route is opt-in and declared explicitly, never inferred.
106
+ const LINUX_DECLARATION = Object.freeze({ client: 'RIGHTKIT_LINUX_BROKER_CLIENT', socket: 'RIGHTKIT_LINUX_BROKER_SOCKET', sha256: 'RIGHTKIT_LINUX_BROKER_CLIENT_SHA256' });
107
+ const sha256OfFile = (file) => createHash('sha256').update(readFileSync(file)).digest('hex');
108
+ const declaredAbsolute = (value) => typeof value === 'string' && posix.isAbsolute(value) && !value.split('/').includes('..') && !value.includes('\0');
109
+
110
+ /**
111
+ * The client of a Linux host's DECLARED broker route (RIGHTKIT_LINUX_BROKER_CLIENT / _SOCKET / _CLIENT_SHA256), which is the one client the gate's executor
112
+ * (managed-cargo resolveLinuxBrokerRoute) drives: the same variables, the same absolute-path rule, the same required SHA-256 pin, the same refusal of another named
113
+ * endpoint, and the executor's real path. A host that declared nothing stays direct and has no client (undefined, as before); a declaration that is incomplete,
114
+ * names a client that is absent, is not the pinned bytes, or sits beside another endpoint also yields undefined here, never a different client: the typed refusal
115
+ * (RK_LINUX_BROKER_UNAVAILABLE, with its reason) is the executor's. The owner-and-writability walk of the path is the executor's too; this lookup is for callers
116
+ * that only need to find the client the declared route uses, and anything that RUNS a scan or a build goes through the executor's validated `client`.
117
+ * A RustRight client on PATH, a shim directory or a managed root never makes a Linux host a broker host.
118
+ */
119
+ function declaredLinuxBrokerClient(env, exists, { realpath = (file) => realpathSync.native(file), digest = sha256OfFile } = {}) {
120
+ const named = (name) => (typeof env?.[name] === 'string' ? env[name] : undefined);
121
+ if (!Object.values(LINUX_DECLARATION).some((name) => named(name) !== undefined)) return undefined;
122
+ const client = named(LINUX_DECLARATION.client); const socket = named(LINUX_DECLARATION.socket); const pinned = named(LINUX_DECLARATION.sha256);
123
+ if (!declaredAbsolute(client) || !declaredAbsolute(socket) || !/^[0-9a-f]{64}$/i.test(pinned ?? '') || !exists(client)) return undefined;
124
+ const real = (file) => { try { return realpath(file); } catch { return file; } };
125
+ for (const other of ['RUSTRIGHT_BUILD_BROKER_SOCKET', 'RIGHTKIT_BUILD_BROKER_SOCKET']) {
126
+ const value = named(other);
127
+ if (value !== undefined && value !== '' && value !== socket && real(value) !== real(socket)) return undefined; // two endpoints: ambiguous, so none
128
+ }
129
+ const resolved = real(client);
130
+ try { if (digest(resolved).toLowerCase() !== pinned.toLowerCase()) return undefined; } catch { return undefined; }
131
+ return resolved;
132
+ }
133
+
134
+ /**
135
+ * The installed RustRight client, selected in ONE order everywhere (doctor, carry, status, lease and dispatch all call this): the live runtime at its final RustRight path
136
+ * (`<runtimeRoot>/runtime`), the control root's `runtime` (the shared fixtures' `runtimeCurrent`), and only then the legacy `runtime-current` link of a host not yet cut over. The
137
+ * Windows client is the native `rustright.exe`, never a `.cmd` shim; the caller probes the exact client returned before it marks any work (snapshot-broker.mjs clientStartable).
138
+ */
139
+ export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync, ops = {}) {
140
+ if (platform !== 'darwin' && platform !== 'win32') return declaredLinuxBrokerClient(env, exists, ops); // a direct (Linux) host has a broker client only by declaring one
141
+ const paths = platform === 'win32' ? path.win32 : path.posix;
142
+ const exe = platform === 'win32' ? 'rustright.exe' : 'rustright';
143
+ const home = env.HOME || env.USERPROFILE || homedir();
144
+ const roots = managedControlCandidates({ env, platform, home });
145
+ const configured = resolveManagedRoots({ env, platform, home });
146
+ const ordered = [paths.join(configured.liveRuntime, exe)];
147
+ for (const root of roots) ordered.push(paths.join(root, 'runtime', exe));
148
+ for (const root of roots) ordered.push(paths.join(root, 'runtime-current', exe)); // the pre-cutover link: last, never preferred over the live runtime
149
+ if (platform === 'darwin') {
150
+ ordered.push(path.posix.join(home, 'Library', 'Application Support', 'RustRight', 'runtime', 'rustright'));
151
+ // Broker-managed HOME may point at a build home; the installed native client belongs to the OS account.
152
+ ordered.push(path.posix.join(userInfo().homedir, 'Library', 'Application Support', 'RustRight', 'runtime', 'rustright'));
153
+ } else {
154
+ for (const root of roots) ordered.push(path.win32.join(path.win32.parse(root).root, '.rustright', 'runtime', 'rustright.exe'));
155
+ }
156
+ return ordered.find((client) => exists(client));
157
+ }
@@ -10,7 +10,7 @@ import { runCargoSync } from "./managed-cargo.mjs";
10
10
  const invalid = (code, message) => Object.assign(new Error(message), { code });
11
11
 
12
12
  export function cargoFfmpegCrate(manifestPath, { run = runCargoSync } = {}) {
13
- const result = run(["metadata", "--offline", "--format-version", "1", "--manifest-path", manifestPath], {
13
+ const result = run(["metadata", "--frozen", "--format-version", "1", "--manifest-path", manifestPath], {
14
14
  cwd: path.dirname(manifestPath),
15
15
  encoding: "utf8",
16
16
  maxBuffer: 256 * 1024 * 1024,