@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.
- package/README.md +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -337
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +14 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/class-a-launch-guard.mjs
CHANGED
|
@@ -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),
|
|
11
|
-
// this hook (rightkit#1240
|
|
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
|
-
|
|
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
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 ===
|
|
45
|
-
if (
|
|
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 === "
|
|
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
|
-
|
|
51
|
-
|
|
164
|
+
if (quote) return stop("unterminated quote");
|
|
165
|
+
commit();
|
|
166
|
+
return { commands, meta, hazard: null };
|
|
52
167
|
}
|
|
53
168
|
|
|
54
|
-
/**
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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((
|
|
70
|
-
if (name === "vite" || name === "next")
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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) {
|
|
121
|
-
childProcess.execSync = function (cmd, ...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
|
}
|
package/cli/right-release.mjs
CHANGED
|
@@ -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
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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 (
|
|
276
|
-
else
|
|
277
|
-
|
|
278
|
-
|
|
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 ||
|
|
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) -----------------------------------------------------------------------------------------------------
|
package/compile-inputs.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|