@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
@@ -6,9 +6,23 @@
6
6
  // build: cargo, rustc, rustright, rightkit (broker client), tauri / cargo-tauri, tsc without --noEmit, tsup, esbuild, swc, rollup,
7
7
  // webpack, `vite build`, `next build`, or a package-manager run of a build / build:* script.
8
8
  //
9
+ // Shell command text (exec, execSync, `shell: true`, `sh -c <text>`, `cmd /c <text>`, ...) is read by a small shell parser BEFORE the
10
+ // shell starts, and it fails closed: text the guard cannot read to the end is refused, never passed on. Refused before launch:
11
+ // command substitution `$( )` and backticks (also inside double quotes), process substitution, parameter expansions that evaluate
12
+ // text (`${!x}`, `${x@P}`, zsh `${(e)x}`), ANSI-C quoting `$'..'`, unquoted parentheses and braces (subshells, functions, groups,
13
+ // brace expansion), shell control forms (if/for/while/until/case/select/function/`!`), `eval`/`source`/`.`/`trap`/`alias`, programs
14
+ // that launch a program they are told (xargs, parallel, concurrently, run-s, ...), a command word built by expansion or a glob,
15
+ // and a shell that reads its commands from stdin. Newlines separate commands; backslash and quote joins are resolved before the
16
+ // program is named; leading assignments and redirections never hide the program; wrappers (env, nice, timeout, command, nohup, sudo,
17
+ // busybox, find, ...) have every following word checked as a possible program; shells nest to a bounded depth and a chain the guard
18
+ // cannot finish reading is refused. Ordinary noncompiling commands (echo, test, node, git, ... with plain arguments, including
19
+ // `$VAR` arguments and quoted text that merely names a compiler) are unchanged. Prefer an argv launch (spawn/execFile with an
20
+ // array) for Class A rows: it never reaches the shell parser.
21
+ //
9
22
  // Scope, exactly: launches from Node processes of the row. Not covered: a compiler API called inside a process
10
- // (typescript.createProgram().emit(), vite.build() / the rollup JS API), and launches by a non-Node program that does not load
11
- // this hook (rightkit#1240). esbuild's JS API is covered: it launches the esbuild service binary.
23
+ // (typescript.createProgram().emit(), vite.build() / the rollup JS API), launches by a non-Node program that does not load
24
+ // this hook (rightkit#1240: a shell script file, `python -c`, `perl -e`, `awk system()`), and the contents of a script file a shell
25
+ // runs. esbuild's JS API is covered: it launches the esbuild service binary.
12
26
  //
13
27
  // A Node child handed a replacement environment keeps the guard (withGuard restores it). There is no exemption: a unit whose tests
14
28
  // launch stand-ins named like compilers (stub scripts can run anything) runs after class A instead (npm.<unit>.afterClassA).
@@ -22,8 +36,18 @@ export const LAUNCH_GUARD_IMPORT = `--import=${pathToFileURL(fileURLToPath(impor
22
36
  export const REFUSAL_CODE = "RK_CLASS_A_COMPILE_LAUNCH";
23
37
  const COMPILERS = new Set(["cargo", "rustc", "rustright", "rightkit", "tauri", "cargo-tauri", "tsup", "esbuild", "swc", "rollup", "webpack"]);
24
38
  const PACKAGE_MANAGERS = new Set(["pnpm", "npm", "yarn", "bun"]);
25
- const SHELLS = new Set(["sh", "bash", "zsh", "dash", "cmd", "powershell", "pwsh"]);
26
- const WRAPPERS = new Set(["env", "npx", "pnpx", "exec", "time", "nice"]);
39
+ const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ash", "ksh", "mksh", "fish", "csh", "tcsh", "cmd", "powershell", "pwsh"]);
40
+ // Programs that run another program they are given: every later word is checked as a possible program (options and their values cannot
41
+ // be told apart from the program without each tool's grammar, so the check is deliberately wide).
42
+ const SCAN_WRAPPERS = new Set([
43
+ "env", "exec", "time", "nice", "npx", "pnpx", "command", "builtin", "nohup", "timeout", "setsid", "stdbuf", "ionice", "taskset", "chrt",
44
+ "flock", "sudo", "doas", "busybox", "xvfb-run", "unbuffer", "watch", "strace", "ltrace", "cross-env", "dotenv", "find", "call", "start", "arch", "script",
45
+ ]);
46
+ // Programs whose job is to launch commands from text or arguments the guard cannot read: refused outright.
47
+ const LAUNCHER_DYNAMIC = new Set(["xargs", "parallel", "concurrently", "npm-run-all", "run-s", "run-p", "iex", "invoke-expression", "invoke-command"]);
48
+ // Shell builtins that run text (shell text only).
49
+ const TEXT_DYNAMIC = new Set(["eval", "source", ".", "trap", "alias", "coproc"]);
50
+ const CONTROL_WORDS = new Set(["if", "then", "else", "elif", "fi", "for", "while", "until", "do", "done", "case", "esac", "select", "function", "{", "}", "!"]);
27
51
  // Options that take a value, per package manager: pnpm's `-w` is `--workspace-root` (a flag), npm's `-w` is `--workspace <name>`.
28
52
  const OPTIONS_WITH_VALUES = Object.freeze({
29
53
  pnpm: new Set(["--filter", "-F", "--dir", "-C"]),
@@ -31,46 +55,243 @@ const OPTIONS_WITH_VALUES = Object.freeze({
31
55
  yarn: new Set(["--cwd"]),
32
56
  bun: new Set(["--cwd", "--filter"]),
33
57
  });
58
+ const MAX_DEPTH = 12;
59
+ const DYN = "\u0000"; // prefixes a shell-text word whose value is decided by expansion or a glob (never present in a real argv)
60
+ const ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
61
+ const REDIRECT = /^(?:\d*|&)(?:<<<|<<-?|<>|<&|>&|>>|>\||<|>)/;
62
+ const REDIRECT_ONLY = /^(?:\d*|&)(?:<<<|<<-?|<>|<&|>&|>>|>\||<|>)$/;
34
63
 
35
64
  const base = (p) => path.basename(String(p ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat|ps1|js|mjs|cjs)$/i, "").toLowerCase();
65
+ const isDyn = (w) => typeof w === "string" && w.startsWith(DYN);
66
+ const plain = (w) => (isDyn(w) ? w.slice(1) : w);
36
67
 
37
- /** A shell command line split into commands (on && || ; |) and words, quotes removed: enough to read what a `sh -c` text launches. */
38
- export function shellWords(text) {
39
- const commands = []; let words = []; let word = ""; let quote = null;
40
- const end = () => { if (word) { words.push(word); word = ""; } };
68
+ /**
69
+ * Reads shell command text into commands and words (quotes and escapes resolved), or reports why it cannot be read safely.
70
+ * dialect: "sh" (POSIX-like shells), "cmd" (cmd.exe: backslash is a path character, only double quotes quote), "pwsh" (backtick escapes).
71
+ * A word decided by an expansion or an unquoted glob is returned prefixed with DYN. Returns { commands, meta, hazard }: `hazard` is
72
+ * a string when the text uses syntax the guard refuses, and `commands` is then only what was read before it.
73
+ */
74
+ function parseShell(text, dialect = "sh") {
41
75
  const s = String(text);
76
+ const posix = dialect === "sh";
77
+ const commands = []; const meta = [];
78
+ let words = []; let word = ""; let started = false; let dyn = false; let quote = null;
79
+ let cur = { piped: false, heredoc: false };
80
+ const end = () => {
81
+ if (word || started) {
82
+ if (dyn && !ASSIGN.test(word) && !REDIRECT.test(word) && word !== "[") word = DYN + word;
83
+ if (word) words.push(word);
84
+ }
85
+ word = ""; started = false; dyn = false;
86
+ };
87
+ const commit = () => { end(); if (words.length) { commands.push(words); meta.push(cur); cur = { piped: false, heredoc: false }; } words = []; };
88
+ const stop = (hazard) => { return { commands, meta, hazard }; };
89
+ const expands = (c) => c !== undefined && /[A-Za-z_0-9{@*#?$!-]/.test(c);
42
90
  for (let i = 0; i < s.length; i++) {
43
91
  const c = s[i];
44
- if (quote) { if (c === quote) quote = null; else word += c; continue; }
45
- if (c === "'" || c === "\"") { quote = c; continue; }
92
+ if (quote === "'") { if (c === "'") quote = null; else word += c; continue; }
93
+ if (quote === "\"") {
94
+ if (c === "\"") { quote = null; continue; }
95
+ if (c === "\\" && posix) {
96
+ const n = s[i + 1];
97
+ if (n !== undefined && "$`\"\\\n".includes(n)) { if (n !== "\n") word += n; i++; } else word += "\\";
98
+ continue;
99
+ }
100
+ if (c === "`" && posix) return stop("backtick command substitution");
101
+ if (c === "%" && dialect === "cmd") dyn = true;
102
+ if (c === "$") {
103
+ if (s[i + 1] === "(") return stop("command substitution $( )");
104
+ if (s[i + 1] === "{") {
105
+ const close = s.indexOf("}", i + 2);
106
+ if (close < 0) return stop("unterminated ${ }");
107
+ const inner = s.slice(i + 2, close);
108
+ if (/^[(!]/.test(inner) || inner.includes("@") || inner.includes("$(") || inner.includes("`")) return stop("parameter expansion that evaluates text");
109
+ word += s.slice(i, close + 1); dyn = true; i = close; continue;
110
+ }
111
+ if (expands(s[i + 1])) dyn = true;
112
+ }
113
+ word += c; continue;
114
+ }
115
+ if (c === "'" && dialect !== "cmd") { quote = "'"; started = true; continue; }
116
+ if (c === "\"") { quote = "\""; started = true; continue; }
117
+ if (c === "\\" && posix) {
118
+ const n = s[i + 1];
119
+ if (n === undefined) { word += "\\"; continue; }
120
+ if (n === "\n") { i++; continue; }
121
+ word += n; started = true; i++; continue;
122
+ }
123
+ if (c === "`") {
124
+ if (dialect === "pwsh") { word += s[i + 1] ?? ""; started = true; i++; continue; }
125
+ if (dialect === "cmd") { word += c; continue; }
126
+ return stop("backtick command substitution");
127
+ }
128
+ if (c === "$") {
129
+ if (s[i + 1] === "(") return stop("command substitution $( )");
130
+ if (s[i + 1] === "{") {
131
+ const close = s.indexOf("}", i + 2);
132
+ if (close < 0) return stop("unterminated ${ }");
133
+ const inner = s.slice(i + 2, close);
134
+ if (/^[(!]/.test(inner) || inner.includes("@") || inner.includes("$(") || inner.includes("`")) return stop("parameter expansion that evaluates text");
135
+ word += s.slice(i, close + 1); dyn = true; i = close; continue;
136
+ }
137
+ if (s[i + 1] === "'" && posix) return stop("ANSI-C quoting $'...'");
138
+ if (s[i + 1] === "\"" && posix) continue; // $"text": a locale-translated double-quoted string
139
+ if (expands(s[i + 1])) dyn = true;
140
+ word += c; continue;
141
+ }
142
+ if (c === "%" && dialect === "cmd") { dyn = true; word += c; continue; }
143
+ if (c === "^" && dialect === "cmd") return stop("cmd caret escape");
144
+ if (c === "(" || c === ")") return stop("unquoted parenthesis (subshell, function, process substitution or array)");
145
+ if (c === "{") {
146
+ if (s[i + 1] === "}") { word += "{}"; i++; continue; }
147
+ return stop("unquoted brace (group, function body or brace expansion)");
148
+ }
149
+ if ((c === "*" || c === "?" || c === "[") && !word && !started && c === "[" && (s[i + 1] === " " || s[i + 1] === undefined)) { word += c; continue; }
150
+ if (c === "*" || c === "?" || c === "[") { dyn = true; word += c; continue; }
151
+ if (c === "=" && !word && !started && posix) { dyn = true; word += c; continue; } // zsh `=cmd` expands to the path of cmd
152
+ if (c === "#" && !word && !started && dialect !== "cmd") { const nl = s.indexOf("\n", i); if (nl < 0) break; i = nl - 1; continue; }
153
+ if (c === "\n") { commit(); continue; }
46
154
  if (/\s/.test(c)) { end(); continue; }
47
- if (c === ";" || c === "|" || c === "&") { end(); if (words.length) commands.push(words); words = []; continue; }
155
+ if (c === "&" && (/[<>]$/.test(word) || s[i + 1] === ">")) { word += c; continue; }
156
+ if (c === ";" || c === "|" || c === "&") {
157
+ const pipe = c === "|" && s[i + 1] !== "|" && s[i - 1] !== "|";
158
+ end(); commit(); if (pipe) cur.piped = true;
159
+ continue;
160
+ }
161
+ if (c === "<" && s[i + 1] === "<") cur.heredoc = true;
48
162
  word += c;
49
163
  }
50
- end(); if (words.length) commands.push(words);
51
- return commands;
164
+ if (quote) return stop("unterminated quote");
165
+ commit();
166
+ return { commands, meta, hazard: null };
52
167
  }
53
168
 
54
- /** Why `[program, ...args]` is a compile or an emitting build, or null. */
55
- export function compileLaunch(program, args = []) {
56
- let words = [String(program), ...args.map(String)];
57
- for (let guard = 0; guard < 8 && words.length; guard++) {
58
- const name = base(words[0]);
59
- if (name === "node" && words[1] && !String(words[1]).startsWith("-")) { words = words.slice(1); continue; }
60
- if (WRAPPERS.has(name)) { words = words.slice(1).filter((w, i, a) => !(i === 0 && /^[A-Z_][A-Z0-9_]*=/.test(w))); continue; }
61
- if (SHELLS.has(name)) {
62
- const at = words.findIndex((w, i) => i > 0 && /^(-c|\/c|\/s|-Command|-command)$/i.test(w));
63
- if (at < 0 || !words[at + 1]) return null;
64
- for (const cmd of shellWords(words.slice(at + 1).join(" "))) { const why = compileLaunch(cmd[0], cmd.slice(1)); if (why) return why; }
169
+ /** A shell command line split into commands (on && || ; | & and newlines) and words, quotes and escapes resolved: enough to read what a `sh -c` text launches. */
170
+ export function shellWords(text) {
171
+ return parseShell(text).commands.map((c) => c.map(plain));
172
+ }
173
+
174
+ const dialectOfShell = (name) => (name === "cmd" ? "cmd" : name === "powershell" || name === "pwsh" ? "pwsh" : "sh");
175
+ function dialectOfOptions(options) {
176
+ if (typeof options?.shell === "string") return dialectOfShell(base(options.shell));
177
+ return process.platform === "win32" ? "cmd" : "sh";
178
+ }
179
+
180
+ /** Drops leading assignments (`CC=x`) and redirections (`>out`, `2>&1`, `> file`) from a shell-text command: neither hides the program. */
181
+ function stripPrefixes(words) {
182
+ let w = words;
183
+ while (w.length) {
184
+ const first = plain(w[0]);
185
+ if (ASSIGN.test(first)) { w = w.slice(1); continue; }
186
+ if (!isDyn(w[0]) && REDIRECT.test(w[0])) { w = REDIRECT_ONLY.test(w[0]) ? w.slice(2) : w.slice(1); continue; }
187
+ break;
188
+ }
189
+ return w;
190
+ }
191
+
192
+ /** Why one command of shell text is a compile (or cannot be read safely), or null. */
193
+ function commandWhy(cmd, dialect, depth) {
194
+ const words = stripPrefixes(cmd);
195
+ if (!words.length) return null;
196
+ if (isDyn(words[0])) return "shell command word is built by an expansion or a glob";
197
+ if (CONTROL_WORDS.has(words[0])) return `shell control/function syntax (${words[0]})`;
198
+ return launchWhy(words, { text: true, strict: true, dialect }, depth);
199
+ }
200
+
201
+ /**
202
+ * cmd.exe delayed expansion (`!var!`, `!var:~0,5!`) builds words after the guard has read the text, so it fails closed: text that turns
203
+ * it on (`setlocal enabledelayedexpansion`) or holds a `!name!` pair is refused, because the registry default can leave it on and the
204
+ * guard cannot tell. A lone `!` (an echoed "wow!") is not a reference and stays allowed, like a lone `%`-free word.
205
+ */
206
+ function cmdDelayedExpansionWhy(text) {
207
+ if (/enabledelayedexpansion/i.test(text)) return "cmd.exe delayed expansion (setlocal EnableDelayedExpansion)";
208
+ if (/![^!\r\n]+!/.test(text)) return "cmd.exe delayed expansion (!name! reference)";
209
+ return null;
210
+ }
211
+
212
+ /** Whether a cmd.exe option word (`/v`, `/V:ON`, `/q/v:on`) enables delayed expansion; `/v:off` does not. */
213
+ const cmdSwitchEnablesDelay = (w) => w.startsWith("/") && /\/v(?!:off$)/i.test(w) && !/^\/v:off$/i.test(w);
214
+
215
+ /** Why shell text is a compile or unreadable, or null. */
216
+ function shellTextWhy(text, dialect = "sh", depth = 0) {
217
+ if (dialect === "cmd") { const delayed = cmdDelayedExpansionWhy(String(text)); if (delayed) return `shell syntax refused before launch: ${delayed}`; }
218
+ const parsed = parseShell(text, dialect);
219
+ if (parsed.hazard) return `shell syntax refused before launch: ${parsed.hazard}`;
220
+ for (const cmd of parsed.commands) { const why = commandWhy(cmd, dialect, depth); if (why) return why; }
221
+ return null;
222
+ }
223
+
224
+ /** A shell invoked as `[name, ...args]`: reads its `-c` text, and refuses a shell that would read its commands from stdin. */
225
+ function shellLaunchWhy(name, words, ctx, depth) {
226
+ const dialect = dialectOfShell(name);
227
+ const rest = words.slice(1);
228
+ const flag = dialect === "cmd" ? /^\/[ckr]$/i : dialect === "pwsh" ? /^-(c|command)$/i : /^-[A-Za-z]*c[A-Za-z]*$/;
229
+ const at = rest.findIndex((w) => flag.test(w));
230
+ if (dialect === "pwsh" && rest.some((w) => /^-(e|ec|enc|encodedcommand)$/i.test(w))) return `${name} -EncodedCommand (text the guard cannot read)`;
231
+ if (dialect === "cmd") {
232
+ // Options come before the command text (after /c, /k or /r everything is text); a /V switch in any spelling is refused.
233
+ for (let k = 0; k < rest.length && (at < 0 || k < at); k++) {
234
+ if (!rest[k].startsWith("/") && !isDyn(rest[k])) break;
235
+ if (cmdSwitchEnablesDelay(plain(rest[k]))) return `shell syntax refused before launch: cmd.exe delayed expansion (${plain(rest[k])})`;
236
+ }
237
+ }
238
+ if (at >= 0) {
239
+ let j = at + 1;
240
+ if (dialect === "sh") { while (rest[j] === "--") j++; }
241
+ if (rest[j] === undefined) return null;
242
+ if (ctx.text && rest.slice(0, j + 1).some(isDyn)) return `${name} is given options or command text built by an expansion or a glob`;
243
+ const text = dialect === "sh" ? rest[j] : rest.slice(j).join(" ");
244
+ return shellTextWhy(text, dialect, depth + 1);
245
+ }
246
+ if (ctx.text && rest.some(isDyn)) return `${name} is given arguments built by an expansion or a glob`;
247
+ if (!ctx.strict) return null;
248
+ // No -c text: the shell runs a script file (not readable here, out of scope) or reads commands from stdin (refused).
249
+ const valueOptions = new Set(["-o", "-O", "+o", "+O", "--rcfile", "--init-file"]);
250
+ let operand = false; let stdin = false;
251
+ for (let k = 0; k < rest.length; k++) {
252
+ const w = rest[k];
253
+ if (dialect === "sh" && valueOptions.has(w)) { k++; continue; }
254
+ if (REDIRECT.test(w)) { if (/^\d*</.test(w)) stdin = true; if (REDIRECT_ONLY.test(w)) k++; continue; }
255
+ if (w === "-" || (dialect === "sh" && /^-[A-Za-z]*s[A-Za-z]*$/.test(w))) { stdin = true; continue; }
256
+ if (dialect === "pwsh" && /^-file$/i.test(w)) { operand = true; break; }
257
+ if (dialect === "cmd" ? w.startsWith("/") : w.startsWith("-") || w.startsWith("+")) continue;
258
+ operand = true; break;
259
+ }
260
+ if (stdin || !operand) return `${name} reading its commands from standard input (text the guard cannot read)`;
261
+ return null;
262
+ }
263
+
264
+ /** Why `words` (a program and its arguments) is a compile or an unreadable launch, or null. */
265
+ function launchWhy(words, ctx, depth) {
266
+ if (depth > MAX_DEPTH) return "launch nests too deeply to inspect";
267
+ let w = words.map(String);
268
+ for (let hop = 0; hop < MAX_DEPTH && w.length; hop++) {
269
+ if (ctx.text && isDyn(w[0])) return "a program word is built by an expansion or a glob";
270
+ const name = base(w[0]);
271
+ if (name === "node" && w[1] && !String(w[1]).startsWith("-")) { w = w.slice(1); continue; }
272
+ if (SHELLS.has(name)) return shellLaunchWhy(name, w, ctx, depth);
273
+ if (LAUNCHER_DYNAMIC.has(name)) return `${name} (runs a command the guard cannot read)`;
274
+ if (ctx.text && (TEXT_DYNAMIC.has(name) || TEXT_DYNAMIC.has(w[0]))) return `${w[0]} (runs shell text the guard cannot read)`;
275
+ const rest = w.slice(1);
276
+ if (SCAN_WRAPPERS.has(name)) {
277
+ if (name === "command" && /^-[vV]$/.test(rest[0] ?? "")) return null;
278
+ for (let i = 0; i < rest.length; i++) {
279
+ if (ctx.text && isDyn(rest[i]) && !ASSIGN.test(plain(rest[i]))) return `${name} is followed by a word built by an expansion or a glob`;
280
+ const why = launchWhy(rest.slice(i), ctx, depth + 1);
281
+ if (why) return why;
282
+ }
65
283
  return null;
66
284
  }
67
- const rest = words.slice(1);
68
285
  if (COMPILERS.has(name)) return name;
69
- if (name === "tsc") return rest.some((w) => /^--?noEmit(=true)?$/i.test(w)) ? null : "tsc (emits)";
70
- if (name === "vite" || name === "next") return rest.find((w) => !w.startsWith("-")) === "build" ? `${name} build` : null;
286
+ if (name === "tsc") return rest.some((x) => /^--?noEmit(=true)?$/i.test(x)) ? null : "tsc (emits)";
287
+ if (name === "vite" || name === "next") {
288
+ if (rest.some(isDyn)) return `${name} is given a word built by an expansion or a glob`;
289
+ return rest.find((x) => !x.startsWith("-")) === "build" ? `${name} build` : null;
290
+ }
71
291
  if (PACKAGE_MANAGERS.has(name)) {
72
292
  let next = null;
73
293
  for (let j = 0; j < rest.length; j++) {
294
+ if (isDyn(rest[j])) return `${name} is given a script or command built by an expansion or a glob`;
74
295
  if (OPTIONS_WITH_VALUES[name].has(rest[j])) { j++; continue; }
75
296
  if (rest[j].startsWith("-") || rest[j] === "run" || rest[j] === "run-script") continue;
76
297
  if (/^build(:|$)/.test(rest[j])) return `${name} ${rest[j]}`;
@@ -78,11 +299,16 @@ export function compileLaunch(program, args = []) {
78
299
  break;
79
300
  }
80
301
  if (!next || !next.length) return null;
81
- words = next; continue;
302
+ w = next; continue;
82
303
  }
83
304
  return null;
84
305
  }
85
- return null;
306
+ return w.length ? "launch chain too long to inspect" : null;
307
+ }
308
+
309
+ /** Why `[program, ...args]` is a compile or an emitting build, or null. A shell given a `-c` text is read by the shell parser (fail closed). */
310
+ export function compileLaunch(program, args = []) {
311
+ return launchWhy([String(program), ...args.map(String)], { text: false, strict: false }, 0);
86
312
  }
87
313
 
88
314
  function refuse(why, program, args) {
@@ -94,11 +320,18 @@ function refuse(why, program, args) {
94
320
 
95
321
  function check(program, args, options) {
96
322
  const argv = Array.isArray(args) ? args : [];
97
- if (options?.shell) { for (const cmd of shellWords([program, ...argv].join(" "))) { const why = compileLaunch(cmd[0], cmd.slice(1)); if (why) refuse(why, program, argv); } return; }
98
- const why = compileLaunch(program, argv);
323
+ const why = options?.shell
324
+ ? shellTextWhy([program, ...argv].join(" "), dialectOfOptions(options), 0)
325
+ : launchWhy([String(program), ...argv.map(String)], { text: false, strict: true }, 0);
99
326
  if (why) refuse(why, program, argv);
100
327
  }
101
328
 
329
+ function checkShellText(cmd, a) {
330
+ const options = a.find((x) => x && typeof x === "object");
331
+ const why = shellTextWhy(String(cmd), dialectOfOptions(options ?? {}), 0);
332
+ if (why) refuse(why, cmd, []);
333
+ }
334
+
102
335
  /** `env` with the guard switched on and loaded (NODE_OPTIONS appended, never replaced). */
103
336
  export function withGuard(env) {
104
337
  const current = String(env.NODE_OPTIONS ?? "");
@@ -117,8 +350,8 @@ export function installLaunchGuard() {
117
350
  childProcess.spawnSync = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.spawnSync.apply(this, keep(a)); };
118
351
  childProcess.execFile = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.execFile.apply(this, keep(a)); };
119
352
  childProcess.execFileSync = function (...a) { const [p, v, opt] = split(a); check(p, v, opt); return o.execFileSync.apply(this, keep(a)); };
120
- childProcess.exec = function (cmd, ...a) { for (const c of shellWords(cmd)) { const why = compileLaunch(c[0], c.slice(1)); if (why) refuse(why, cmd, []); } return o.exec.call(this, cmd, ...keep(a)); };
121
- childProcess.execSync = function (cmd, ...a) { for (const c of shellWords(cmd)) { const why = compileLaunch(c[0], c.slice(1)); if (why) refuse(why, cmd, []); } return o.execSync.call(this, cmd, ...keep(a)); };
353
+ childProcess.exec = function (cmd, ...a) { checkShellText(cmd, a); return o.exec.call(this, cmd, ...keep(a)); };
354
+ childProcess.execSync = function (cmd, ...a) { checkShellText(cmd, a); return o.execSync.call(this, cmd, ...keep(a)); };
122
355
  childProcess.fork = function (modulePath, ...a) { const v = Array.isArray(a[0]) ? a[0] : []; check(modulePath, v, {}); return o.fork.call(this, modulePath, ...keep(a)); };
123
356
  syncBuiltinESMExports();
124
357
  }
@@ -425,6 +425,8 @@ Commands:
425
425
  disk run-root --release <name> End a run-root lease
426
426
  disk schedule install|uninstall|status Hourly launchd job (Mac) / hidden Task Scheduler entry (Windows) running "disk run --auto", bound to the stable per-user copy; status fails when no full pass finished in 26 h
427
427
  deps --check|--audit|--update Shared dependency lane
428
+ deps --cargo-lock --manifest <Cargo.toml> --pin <crate>=<version> [--pin <crate>=<version>]...
429
+ Refresh one workspace's Cargo.lock (cargo update -p <crate> --precise <version>, one request per pin): the broker in the primary checkout on Mac and Dell, plain cargo on Linux; never a build. Refuses, and restores the lock, if any other file changes
428
430
  update check|apply|gate|land|discard|run|schedule [--repo <path>]
429
431
  Run the pinned @rightkit/outdated updater (right-update) that moves stale pins; reads .rightkit/update.json
430
432
  hardening <artifact...> Run the Right Suite hardening scan
package/clippy-route.mjs CHANGED
@@ -31,6 +31,8 @@ import { resolveExecutor as defaultResolveExecutor } from "./gate-executor.mjs";
31
31
  import { directPlatform } from "./managed-cargo.mjs";
32
32
  import { defaultSpawnAsync, sidecarBoundCrate } from "./deterministic-scans.mjs";
33
33
  import { gateStateDir } from "./gate-stages.mjs";
34
+ import { ProvenanceError, verifyAttributionDetail } from "./provenance-verify.mjs";
35
+ import { isToolchainUnknown } from "./snapshot-request.mjs";
34
36
 
35
37
  export const ROUTE_RECEIPT_SCHEMA = "rightkit.gate-clippy-route-receipt/1";
36
38
  export const ROUTE_DIAGNOSTICS_SCHEMA = "rightkit.gate-clippy-route-diagnostics/1";
@@ -261,26 +263,36 @@ export function summarizeRun({ messages, stderr = "", exit = null, signal = null
261
263
  // ---- the broker's half of the receipt: attribution and toolchain (rows 8.9, 8.16) ------------------------------------------------------
262
264
 
263
265
  /**
264
- * The gate's reading of a broker receipt for a Clippy request (BROKER-CONTRACT B4 to B6; the gate side only, wired when the carrier
265
- * is). Missing attribution, a truncated or unavailable one, or a missing toolchain identity makes the route result FAILED, never a
266
- * partial success: `{ aggregate, code, units, problems, toolchain, complete }`. The units are the receipt's per package and per
267
- * target `attribution` rows.
266
+ * The gate's reading of a broker receipt for a Clippy request (BROKER-CONTRACT B4 to B6; the gate side only, wired when the carrier is). The receipt's
267
+ * `attribution` is COMPACT (counts and a `detail` descriptor `{path, sha256}`); the per package and per target rows live in the evidence file the descriptor names
268
+ * (`detailBytes`, read by the caller from that path). GB-50: the per-unit result is derived only through `verifyAttributionDetail` (RustRight's
269
+ * verify_attribution_detail, vector attribution-detail-vectors.v1.json): the file's bytes hash to the signed descriptor, its schema, request id and toolchain are
270
+ * this receipt's, it is available and not truncated, every row is well formed and the compact counts equal the rows. Counts alone never pass (`units: 2` with exit 0
271
+ * and no matching detail is FAILED). Missing or unverifiable evidence makes the route result FAILED, never a partial success:
272
+ * `{ aggregate, code, units, failedUnits, problems, toolchain, complete }`. A unit with errors, or a nonzero exit, is a failure.
273
+ * The Clippy carrier stays explicitly refused (RK_CLIPPY_BROKER_CARRIER_NOT_WIRED) until the whole route is wired.
268
274
  */
269
- export function routeFromBrokerReceipt(receipt) {
275
+ export function routeFromBrokerReceipt(receipt, detailBytes = null) {
270
276
  const problems = [];
271
277
  const attribution = receipt?.attribution;
272
278
  const toolchain = typeof receipt?.toolchain === "string" && receipt.toolchain.trim() ? receipt.toolchain : null;
279
+ let rows = null; let failedUnits = 0;
273
280
  if (!attribution || typeof attribution !== "object") problems.push("the receipt carries no attribution");
274
281
  else if (attribution.available !== true) problems.push(`the receipt's attribution is unavailable (${attribution.reason ?? "no reason"})`);
275
- else if (!Array.isArray(attribution.units)) problems.push("the receipt's attribution lists no units");
276
- else if (Number(attribution.truncated ?? 0) > 0) problems.push(`the receipt's attribution is truncated (${attribution.truncated} unit(s) not listed)`);
277
- if (toolchain === null) problems.push("the receipt carries no toolchain identity");
278
- const units = Array.isArray(attribution?.units) ? attribution.units.map((u) => ({ package: u.package, packageId: u.packageId ?? null, target: u.target, kind: u.kind ?? "", errors: Number(u.errors ?? 0), warnings: Number(u.warnings ?? 0), status: Number(u.errors ?? 0) > 0 ? "failed" : "clean", fresh: u.fresh ?? null })) : [];
282
+ else if (detailBytes === null || detailBytes === undefined) problems.push("the receipt's attribution is compact counts only and no detail file was supplied: counts alone cannot satisfy attribution");
283
+ else {
284
+ try { ({ rows, failedUnits } = verifyAttributionDetail(receipt, detailBytes)); } catch (e) {
285
+ if (!(e instanceof ProvenanceError)) throw e;
286
+ problems.push(`the receipt's attribution is not verified against its detail rows (${e.code}: ${e.message})`);
287
+ }
288
+ }
289
+ if (toolchain === null) problems.push(isToolchainUnknown(receipt?.toolchain) ? `the broker could not name the toolchain (${receipt.toolchain.reason}; RK_TOOLCHAIN_IDENTITY_UNAVAILABLE): the identity is UNKNOWN, not evidence` : "the receipt carries no toolchain identity");
290
+ const units = rows ? rows.map((u) => ({ package: u.package, packageId: u.packageId ?? null, target: u.target, kind: u.kind ?? "", errors: Number(u.errors ?? 0), warnings: Number(u.warnings ?? 0), status: Number(u.errors ?? 0) > 0 ? "failed" : "clean", fresh: u.fresh ?? null })) : [];
279
291
  const exit = receipt?.exit;
280
292
  const exitCode = typeof exit === "number" ? exit : typeof exit?.code === "number" ? exit.code : typeof exit?.status === "number" ? exit.status : null;
281
293
  if (exitCode === null) problems.push("the receipt carries no exit status");
282
- const aggregate = problems.length || exitCode !== 0 || units.some((u) => u.status === "failed") ? "FAILED" : "PASSED";
283
- return { aggregate, code: problems.length ? ROUTE_CODES.receiptEvidence : null, units, problems, toolchain, complete: aggregate === "PASSED" };
294
+ const aggregate = problems.length || exitCode !== 0 || failedUnits > 0 ? "FAILED" : "PASSED";
295
+ return { aggregate, code: problems.length ? ROUTE_CODES.receiptEvidence : null, units, failedUnits, problems, toolchain, complete: aggregate === "PASSED" };
284
296
  }
285
297
 
286
298
  // ---- sidecar-bound scope (row 8.8) -----------------------------------------------------------------------------------------------------
@@ -7,8 +7,11 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { spawnSync } from "./background-process.mjs";
9
9
  import { SECRET_PATTERNS } from "./publish-cargo.mjs";
10
+ import { COMPILE_INPUTS_CANONICAL, COMPILE_INPUTS_LEGACY, SnapshotRequestError, readDeclaredInputs } from "./snapshot-request.mjs";
10
11
 
11
- export const COMPILE_INPUTS_FILE = ".rightkit/compile-inputs.json";
12
+ /** The canonical declaration (GB-49); the legacy `.rightkit/compile-inputs.json` is read only when it is absent (snapshot-request.mjs readDeclaredInputs, the one reader). */
13
+ export const COMPILE_INPUTS_FILE = COMPILE_INPUTS_CANONICAL;
14
+ export { COMPILE_INPUTS_LEGACY };
12
15
 
13
16
  /** Files of one declared input in the broker's order (children by name), refusing links; [{ name, abs }]. */
14
17
  function inputFiles(root, input) {
@@ -32,20 +35,14 @@ function inputFiles(root, input) {
32
35
  */
33
36
  export function judgeCompileInputs(root, { spawn = spawnSync, execPath = process.execPath } = {}) {
34
37
  const findings = [];
35
- let declared = [];
36
- const file = path.join(root, COMPILE_INPUTS_FILE);
37
- if (!fs.existsSync(file)) return { inputs: [], findings };
38
- try {
39
- declared = JSON.parse(fs.readFileSync(file, "utf8")).inputs;
40
- if (!Array.isArray(declared) || declared.some((p) => typeof p !== "string")) throw new Error("`inputs` must be a list of paths");
41
- } catch (e) {
42
- return { inputs: [], findings: [`${COMPILE_INPUTS_FILE}: ${e.message}`] };
43
- }
38
+ // One reader for every consumer: canonical first, legacy only when the canonical file is absent, shared link, size, path and overlap checks.
39
+ let declared;
40
+ try { declared = readDeclaredInputs(root); } catch (e) { if (e instanceof SnapshotRequestError) return { inputs: [], findings: [e.message] }; throw e; }
41
+ if (declared.length === 0) return { inputs: [], findings };
44
42
  const inputs = [];
45
43
  for (const input of declared) {
46
- if (path.isAbsolute(input) || input.split(/[\\/]/).some((part) => part === ".." || part === "." || part === "")) { findings.push(`${input}: not a plain relative path`); continue; }
47
44
  let files;
48
- try { files = inputFiles(root, input); } catch (e) { findings.push(`${input}: ${e.code === "ENOENT" ? "missing from the captured tree" : e.message}`); continue; }
45
+ try { files = inputFiles(root, input.split(/[\\/]/).join("/")); } catch (e) { findings.push(`${input}: ${e.code === "ENOENT" ? "missing from the captured tree" : e.message}`); continue; }
49
46
  const hash = crypto.createHash("sha256");
50
47
  for (const f of files) {
51
48
  const bytes = fs.readFileSync(f.abs);
@@ -0,0 +1,18 @@
1
+ // GB-07 (BROKER-CONTRACT section 16): where a unit publication's control plane may run. One definition for the gate's release (unit-gate.mjs) and the coordinator's in-gate entry
2
+ // (scripts/ci/rk-pipeline.mjs runPipeline), so neither can drift from the broker's own rule.
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Why the control-plane checkout (the primary checkout, where the coordinator's uncarried root requests are admitted) is not apart from the managed snapshots, or null. The broker refuses a root
7
+ * request whose working directory is inside a managed snapshot or a path a live snapshot lease holds (RK_CHECK_ERROR: "a root request cannot run inside a managed snapshot"), so the control plane may
8
+ * not be inside the managed snapshots root; with a `root` (the source: the snapshot where the carried build and pack steps stay bound) it may not be inside the source either, and a source that is
9
+ * itself a snapshot may not be its own control plane. A direct, unsnapshotted source keeps one root for both. `pathApi` is the platform's path module; comparison folds case on Windows.
10
+ */
11
+ export function controlPlaneProblem({ root = null, checkout, snapshotsRoot = null, pathApi = path }) {
12
+ const fold = (p) => { const n = pathApi.normalize(String(p)); return pathApi === path.win32 ? n.toLowerCase() : n; };
13
+ const inside = (child, parent) => { const rel = pathApi.relative(fold(parent), fold(child)); return rel === "" || (!rel.startsWith("..") && !pathApi.isAbsolute(rel)); };
14
+ if (typeof checkout !== "string" || !pathApi.isAbsolute(checkout)) return `the control-plane checkout ${JSON.stringify(checkout)} must be an absolute path`;
15
+ if (snapshotsRoot && inside(checkout, snapshotsRoot)) return `the control-plane checkout ${checkout} is inside the managed snapshots root ${snapshotsRoot}: the broker refuses an uncarried root request there (RK_CHECK_ERROR: a root request cannot run inside a managed snapshot); the coordinator runs from the primary checkout, apart from the source snapshot`;
16
+ if (root !== null && fold(checkout) !== fold(root) && inside(checkout, root)) return `the control-plane checkout ${checkout} is inside the source root ${root}: the source (the snapshot) and the control plane (the primary checkout) are separate roots`;
17
+ return null;
18
+ }