@rightkit/release 0.6.3 → 0.6.4

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 CHANGED
@@ -59,6 +59,18 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
59
59
  - S1-S4 need `compiledOut`: `feature:<name>`, `cfg:test`, `cfg:debug_assertions` or, for Swift, `swift:DEBUG` (the read sits inside `#if DEBUG`; the read exists only there). The source walk skips test directories, including Swift bundles named `Tests`, `UITests`, `<App>Tests` and `<App>UITests`. S5 needs `userGesture: true`. S6 needs `valueType` of `bool`, `int` or `enum`.
60
60
  - `anchor` and `note` are informational. Fixtures: `fixtures/release-switches/` (`app/` is a clean example).
61
61
 
62
+ #### Non-shipped tooling: `releaseSwitches.nonShipped`
63
+
64
+ On desktop (non-iOS) targets the source walk covers the whole app root, so app tooling that never ships (a bakeoff harness) can produce `unregistered-read` findings that cannot truthfully be declared in `switches.json`. Declare such directories in the release config:
65
+
66
+ ```json
67
+ "releaseSwitches": { "nonShipped": [ { "dir": "bakeoff", "reason": "bakeoff harness; not compiled or bundled" } ] }
68
+ ```
69
+
70
+ - It is an exclusion list: any directory not listed is still scanned, so an omission fails safe. `dir` is relative to the app root; `reason` is required and non-empty. iOS is unchanged (it already walks only the Xcode project and the local crates in its cargo tree).
71
+ - Every entry is validated before the walk and a bad one fails the release with `inventory-invalid`. Refused: a dir that is missing, not a real directory (a symlink), absolute, escaping the root, or the root itself; an empty `reason`; a dir that contains or sits inside a local crate of the resolved release cargo tree (a crate at the app root protects `src/`), `src-tauri`, or the Tauri `frontendDist` bundle; a dir that contains a `package.json` or sits inside a directory below the root that has one; when the root has a `package.json`, `src/`, `public/` and every existing directory named by the root `index.html` (`src`/`href`), a quoted string in `vite.config.*` (root, publicDir, build input) or `include`/`files` of `tsconfig.json`, plus anything inside or containing them; and any dir holding a path named by `src-tauri/tauri.conf.json` `beforeBuildCommand`/`beforeDevCommand`/`beforeBundleCommand` a root `package.json` `build*` script, or the release recipe's own commands (`targets.<os>.prePackage`, `package`, `postSign`, `packageHook`, the finalizer; followed through `pnpm|npm|yarn run <script>`), because those run during the release build.
72
+ - Not protected: the release code does not otherwise know the frontend source layout. A frontend input that none of the rules above names (for example one referenced only from a nested config, or built by a script not reachable from those commands) is not refused, so list only directories you know never ship.
73
+
62
74
  ## Commands
63
75
 
64
76
  `right-release` maps these subcommands to the modules in this package:
@@ -109,7 +121,7 @@ a JSON line before and after (`{"event":"e2e_suite_completed","suite":"tooling",
109
121
  (repeatable) selects suites; a group name such as `journeys` selects all its chunks. `--runner vitest -- run --project unit`
110
122
  runs a runner once with the arguments after `--` (watch mode, one project); `-- <args>` after `--suite` is allowed when exactly
111
123
  one suite is selected. `--list [--json]` prints the resolved commands and runs nothing. Wrap it as the repository needs, for
112
- example `rightkit storage run --app <app> -- right-release test`.
124
+ example `rustright storage run --app <app> -- right-release test`.
113
125
 
114
126
  ### `right-release build --web` (or `web-build`)
115
127
 
@@ -187,9 +199,27 @@ Only a target directory under a managed root (`resolveManagedRoots`: `RIGHTKIT_D
187
199
  `postSign`: it packs the signed, stapled `.app` and signs the tarball. The default output is `<App>.app.tar.gz`, named after the
188
200
  app in `notarize.app`. Declare the file in `updater.artifacts` as before. See `docs/MAC-PACKAGE-RECIPE.md`.
189
201
 
190
- ## QA receipts (warn-and-record)
202
+ ## QA receipts and admission (warn by default, refuse per app)
203
+
204
+ `right-release qa --stage source|installed --platform mac|win` runs `rightkit-qa` and writes a MAC'd receipt. Admission is a per-app policy in `qa-admission-policy.json`:
191
205
 
192
- `right-release qa --stage source|installed --platform mac|win` still runs `rightkit-qa` and writes a signed receipt, and receipt verification is unchanged. A missing or invalid receipt no longer blocks a release. `build`, `upload`, `publish-update` and `github` print `[right-release] QA warning: ...` to stderr, continue, and record the outcome:
206
+ ```json
207
+ { "schema": 1,
208
+ "apps": { "postright": { "mode": "refuse", "since": "2026-11-02", "set_by": "adrian" } },
209
+ "approvedRunners": [ { "sha256": "<64 hex>", "version": "0.8.0" } ] }
210
+ ```
211
+
212
+ - **`warn` (default; an app with no entry).** A missing or invalid receipt does not block. `build`, `upload`, `publish-update` and `github` print `[right-release] QA warning: ...` to stderr, continue, and record the outcome (below).
213
+ - **`refuse`.** The same refusal throws `QA admission refused for <app> (policy refuse since ... by ...)`; nothing is sealed, uploaded or published. Switch an app on with `node release-qa-policy.mjs set --app <app> --mode refuse --set-by <name>`. Moving an app back to `warn` (or deleting its entry) is refused unless `set_by` is `adrian`; a hand edit that does so is caught on every load by comparing with the copy committed at `RIGHT_RELEASE_QA_POLICY_BASELINE_REF` (default `HEAD`; CI should use `origin/main`). `set_by` is a recorded claim, not an authenticated identity: the file must be owner-protected in the repository (CODEOWNERS and branch protection), and a downgrade that was already committed becomes the new baseline. An unreadable or malformed policy file is a hard error, never a fall back to warn.
214
+
215
+ Hardening, applied in both modes (a failure is a warning in `warn` and a refusal in `refuse`):
216
+
217
+ 1. **Platform and release id.** The sealed receipt must be for the platform and the release id being sealed or published.
218
+ 2. **Approved runner.** The `rightkit-qa` executable's sha256 recorded in the receipt must be listed in `approvedRunners`. An empty list approves nothing. The binary is digested before the run and again after it; a swap during the run is refused.
219
+ 3. **Every release-critical journey.** With `qa.journeys` in the release config (`{ manifest?: "journeys.toml", receipt?: "<path>", args?: { source?, installed? } }`) `right-release qa` runs `rightkit-qa journeys run` (or reads `receipt`), validates the `rightkit.journeys.receipt/1` through `rightkit-qa journeys validate-receipt --manifest .. --tree ..` (exit code 0 and a final `ACCEPTED` line), and seals a `journeys` summary. `release-qa-floor.json` maps the `journey` floor id to "all release-critical journeys passed": at admission the required set is recomputed from the `journeys.toml` committed at the receipt's commit (every `release_critical` journey that lists the platform), and each must be `passed`. Zero critical journeys is never a pass. One matching check no longer satisfies the floor.
220
+ 4. **Signing outside test execution.** Checked: the receipt records the test processes' pids and the window they ran in; admission refuses a receipt sealed before that window ended, signing that began before it ended (`signingStartedAt`), a sealing or admitting process that is a test process, `RIGHT_RELEASE_QA_STAGE` set at admission (admission or signing inside a QA run), and `right-release qa` itself refuses to seal inside a QA run. Not checkable here: that test processes cannot reach the signing key (OS credential store and signing agent ACL) and that CI runs QA and signing as separate jobs (the carried receipt, rightkit#183, is the hook).
221
+
222
+ `right-release qa` keeps the older behavior for evidence checks and verification of the receipt itself; the outcome is recorded:
193
223
 
194
224
  - Source stage: sealed into the manifest as `qa: { status: "passed", sourceReceiptDigest, floorDigest }` or `qa: { status: "missing" | "rejected", reason }`.
195
225
  - Installed stage: written to `.right-release/state/<releaseId>/<platformDir>/qa-installed.json` as `{ status, reason?, receiptDigest?, checkedAt, provider }`. It is written when publication starts, so a later upload failure still leaves a record.
@@ -212,7 +242,7 @@ right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id d
212
242
 
213
243
  It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, stages a `ditto` copy beside it in the destination and verifies that copy, quits the running instance with SIGTERM (15 s, then SIGKILL), swaps the staged copy in (one atomic exchange when the `rightkit-swap` helper is found, otherwise two journaled renames; the old bundle is restored on failure), re-verifies the installed copy, only then moves the old bundle to `~/.Trash/<App> <timestamp>.app`, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`, among others). `right-release install-app --recover [--dest <dir>] [--app-name <App.app>] [--json]` rolls an interrupted install forward or back from its journal; every install runs that recovery first. Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
214
244
 
215
- The shipped `bin/darwin/rightkit-swap` is Developer ID signed. Local publish adds it via `right-release publish npm --package @rightkit/release --local` on the Mac (managed `rightkit cargo build --release -p rightkit-fs --bin rightkit-swap`, `codesign --options runtime --timestamp`, strict verify). CI qualification adds it via `publish.yml` build-swap → sign-swap → pack (`docs/CI.md`); sign-swap needs `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_KEYCHAIN_PASSWORD` & `APPLE_DEVELOPER_ID` in the `release` environment, & stays blocked until root provisions the certificate pair. Exporting that identity requires an Aqua login session: SSH export fails `errSecInteractionNotAllowed` (-25308) & the Aqua signing agent exposes only codesign/updater/notarize/staple, no key export — do not retry either route. `install-app` refuses a helper that exists but is not validly signed (`SWAP_HELPER_UNSIGNED`, naming the fix); with no helper at all it uses the journaled two-rename fallback and logs it (`swap-fallback`, `replaced {swap}`).
245
+ The shipped `bin/darwin/rightkit-swap` is Developer ID signed. Local publish adds it via `right-release publish npm --package @rightkit/release --local` on the Mac (managed `rustright cargo build --release -p rightkit-fs --bin rightkit-swap`, `codesign --options runtime --timestamp`, strict verify). CI qualification adds it via `publish.yml` build-swap → sign-swap → pack (`docs/CI.md`); sign-swap needs `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_KEYCHAIN_PASSWORD` & `APPLE_DEVELOPER_ID` in the `release` environment, & stays blocked until root provisions the certificate pair. Exporting that identity requires an Aqua login session: SSH export fails `errSecInteractionNotAllowed` (-25308) & the Aqua signing agent exposes only codesign/updater/notarize/staple, no key export — do not retry either route. `install-app` refuses a helper that exists but is not validly signed (`SWAP_HELPER_UNSIGNED`, naming the fix); with no helper at all it uses the journaled two-rename fallback and logs it (`swap-fallback`, `replaced {swap}`).
216
246
 
217
247
  Development builds (from `@rightkit/ci` `devArtifact`, Developer ID-signed but not notarized) install with `--dev` and exactly one of `--dmg`, `--zip <path.zip>` (extracted with `ditto -x -k`), or `--app <path.app>`: `right-release install-app --dev --zip dev-mac.zip`. `--dev` still requires `codesign --verify --deep --strict`, a Team ID, no Team ID change, and a bundle-id match; it skips only `spctl` and `stapler`, removes `com.apple.quarantine` from the new bundle only, prints `DEV BUILD (not notarized)`, and sets `dev: true` on every JSON event.
218
248
 
@@ -384,7 +414,7 @@ Updater artifacts are declared as for R2 but need no `key`; `platform` must be a
384
414
  ```js
385
415
  targets: {
386
416
  win: {
387
- updater: { keyFile: "%APPDATA%/RightKit/updater.key", artifacts: [
417
+ updater: { artifacts: [
388
418
  { file: "src-tauri/target/release/bundle/nsis/Ember_1.2.3_x64-setup.exe", signature: "src-tauri/target/release/bundle/nsis/Ember_1.2.3_x64-setup.exe.sig", platform: "windows-x86_64" },
389
419
  ] },
390
420
  artifacts: ["dist/Ember-1.2.3-portable-win.zip"], // portable zips seal alongside; they are not in latest.json
@@ -395,6 +425,8 @@ targets: {
395
425
  }
396
426
  ```
397
427
 
428
+ Updater signing keys are never named in the config: `sign-updater` reads the minisign key from the OS credential provider (`rightkit-credentials`, namespace `rightsuite-updater-key`; see `protected-credentials.mjs`). `targets.<platform>.updater.keyFile` is refused at config validation (`RELEASE_CONFIG_UPDATER_KEY_FILE`, listed by `right-release doctor` and the build with every other finding) and is never forwarded to the signer, so remove it from apps that still set it.
429
+
398
430
  NSIS, MSI, `.app.tar.gz` and AppImage bundles all work the same way (any file signed by `sign-updater`). With no sealed installer the installer platform-trust check is skipped (reported in the output); installer-bearing seals still run it. The app side: `plugins.updater.endpoints` is the `latest.json` URL above and `plugins.updater.pubkey` is the suite updater public key (see `@rightkit/app-update` README).
399
431
 
400
432
  ## Android (AAB build, Play internal track, dev install)
Binary file
@@ -750,13 +750,13 @@ export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-g
750
750
 
751
751
  // ---- command resolution -------------------------------------------------------------------------------------
752
752
 
753
- // Cargo only through the broker: `rightkit cargo`, or a `cargo` on PATH that is the managed shim (it runs cargo-guard.mjs).
753
+ // Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim (it runs cargo-guard.mjs).
754
754
  function resolveCargoCommand(o) {
755
755
  const which = (cmd) => {
756
756
  const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]);
757
757
  return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
758
758
  };
759
- if (which("rightkit").length) return ["rightkit", "cargo"];
759
+ if (which("rustright").length) return ["rustright", "cargo"];
760
760
  const first = which("cargo")[0];
761
761
  if (first) { try { if (o.readFile(first).includes("cargo-guard.mjs")) return ["cargo"]; } catch { /* unreadable: not the shim */ } }
762
762
  return null;
@@ -900,12 +900,12 @@ export async function main(argv, io = {}) {
900
900
  const readLive = (f) => o.readFile(f);
901
901
 
902
902
  // Units come from the live checkout (cargo reads it); a dirty checkout then blocks below.
903
- let rust; let npm; let cargoCommand = ["rightkit", "cargo"];
903
+ let rust; let npm; let cargoCommand = ["rustright", "cargo"];
904
904
  try {
905
905
  const manifests = tracked("Cargo.toml");
906
906
  if (manifests.length) {
907
907
  cargoCommand = resolveCargoCommand(o);
908
- if (!cargoCommand) { o.err(`${tag} BLOCKED: neither \`rightkit\` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used`); return 1; }
908
+ if (!cargoCommand) { o.err(`${tag} BLOCKED: neither \`rustright\` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used`); return 1; }
909
909
  if (!o.exists(path.join(root, GRANT_FILE))) { o.err(`${tag} BLOCKED: no local compile grant (${GRANT_FILE}); the broker refuses cargo, so the Rust units cannot be discovered or scanned`); return 1; }
910
910
  }
911
911
  // Untracked (not ignored) manifests only: git does not descend into ignored directories, so this stays cheap. A member of a
@@ -270,7 +270,8 @@ Commands:
270
270
  upload --platform ios --release <id> [--apply] Sealed IPA to TestFlight, then the exact build into the beta groups; dry run unless --apply (macOS)
271
271
  ios-credentials store --key-id <KEYID> --issuer <uuid> | check
272
272
  Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
273
- qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt (a missing receipt warns and is recorded; it never blocks)
273
+ qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa (and the journeys lane when the config sets qa.journeys) and write the sealed QA receipt. Refused inside a QA run. The receipt must match the platform and release id, come from an approved runner digest, and show every release-critical journey passed
274
+ Admission is per app in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks build, upload and publish. Only adrian moves an app from refuse back to warn
274
275
  qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List source/installed QA status per release; exit 1 if an uploaded build lacks passing QA
275
276
  publish-update --release <id> --platform mac|win --tier patch|update
276
277
  Upload sealed bytes to R2 & register RightApps
@@ -307,7 +308,7 @@ Commands:
307
308
  apple profile renew [--config <file>] [--profile <file>] [--dry-run]
308
309
  Renew macOS profile for current Keychain signer
309
310
  signing-agent install|uninstall|status Manage invisible Aqua-session signing service
310
- doctor [--platform mac|win] Inspect one app's release config
311
+ doctor [--platform mac|win] [--gates] Inspect one app's release config; --gates also runs the build's pre-build source gates
311
312
  doctor --all Verify all Right Suite app release contracts
312
313
  suite-doctor Verify all local Right Suite repositories
313
314
  ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
@@ -10,10 +10,10 @@
10
10
  // repository has none. A deferred scan is listed by name (result.deferred, scanDeferrals), is never a
11
11
  // pass (result.ok and result.complete are false while any scan is deferred) and cannot become a SCAN_B receipt.
12
12
  // Content scans run on the exported HEAD (contentRoot). Only a scan that inherently needs the live
13
- // checkout uses liveRoot: `rightkit cargo` scans (the broker binds to the registered checkout), the
13
+ // checkout uses liveRoot: `rustright cargo` scans (the broker binds to the registered checkout), the
14
14
  // tsc, eslint and package tests (need installed node_modules).
15
15
  // Class B runs only when every selected class A scan passed (SCAN_B is distinct from SCAN_A and
16
- // follows it). Rust commands go through `rightkit cargo`, JavaScript tools through `pnpm exec`.
16
+ // follows it). Rust commands go through `rustright cargo`, JavaScript tools through `pnpm exec`.
17
17
  // The runner takes injectable spawn/exists/readFile/readdir so tests execute nothing real. The
18
18
  // result is a plain object (per-scan status, tool versions, scanSetDigest) usable as receipt content
19
19
  // (packages/release/pipeline-receipts.mjs); it carries no authority by itself.
@@ -34,7 +34,7 @@ export const SCAN_CLASS = Object.freeze({ A: "A", B: "B" });
34
34
  export const CLASS_A_SCAN_PREFIXES = Object.freeze([
35
35
  "rustfmt", "node-check", "json-parse", "toml-parse", "yaml-parse", "ownership-catalog",
36
36
  "actionlint", "pnpm-lockfile", "pnpm-audit", "cargo-metadata", "cargo-deny", "cargo-audit", "cargo-package-list",
37
- "changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions",
37
+ "changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions", "suite-api",
38
38
  ]);
39
39
  export const CLASS_B_SCAN_PREFIXES = Object.freeze([
40
40
  "clippy", "check", "check-all-features", "check-features", "check-dependent", "doc", "tsc", "eslint", "swift-build", "test", "generated-docs",
@@ -206,9 +206,11 @@ const ESLINT_FILES = ["eslint.config.js", "eslint.config.mjs", ".eslintrc.json"]
206
206
  const ROOT_LINT_FILES = [...ESLINT_FILES, "biome.json"];
207
207
  // Everything scripts/generate-index.mjs reads or writes (its header and loadInventory): crate and package manifests and
208
208
  // READMEs, the root Cargo.toml (workspace version), the versions and ownership data, the ownership-availability helper it imports, itself, and its outputs.
209
+ // The Right Suite API contract and every projection scripts/gen-suite-api.mjs writes from it (the test-vector copy included).
210
+ const SUITE_API_INPUT = /^(contracts\/right-suite-api\.v1\.json|scripts\/gen-suite-api\.mjs|crates\/rightkit-license\/src\/suite_api_generated\.rs|packages\/app-update\/src\/suite-api\.generated\.ts|packages\/release\/suite-api\.generated\.mjs|crates\/rightkit-license\/test-vectors\/right-suite-api\.v1\.json)$/;
209
211
  const GENERATOR_INPUT = /^(crates\/[^/]+\/(Cargo\.toml|README\.md)|packages\/[^/]+\/(package\.json|README\.md)|Cargo\.toml|packages\/release\/(rightkit-versions\.json|rightkit-ownership\.json|ownership-availability\.mjs)|scripts\/generate-index\.mjs|INDEX\.md|docs\/(ADOPTION|CAPABILITY-MAP)\.md)$/;
210
212
  const PROBES = Object.freeze({
211
- rustfmt: ["--version"], node: ["--version"], rightkit: ["cargo", "--version"], pnpm: ["--version"],
213
+ rustfmt: ["--version"], node: ["--version"], rustright: ["cargo", "--version"], pnpm: ["--version"],
212
214
  actionlint: ["-version"], "right-git": ["--version"], swift: ["--version"],
213
215
  });
214
216
 
@@ -221,7 +223,7 @@ function commandText(d) {
221
223
  /**
222
224
  * The scan plan for a change: derived from the changed files plus the manifests they belong to.
223
225
  * `contentRoot` is a clean export of the commit (pure content checks); `liveRoot` is the checkout the
224
- * managed `rightkit cargo` and `pnpm` run in. A scan whose files did not change is not planned.
226
+ * managed `rustright cargo` and `pnpm` run in. A scan whose files did not change is not planned.
225
227
  */
226
228
  export function planScans({
227
229
  changed = [], contentRoot, liveRoot = contentRoot, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8"),
@@ -230,8 +232,8 @@ export function planScans({
230
232
  // CI shards (one crate per runner, where the port, pipe, desktop and wall-clock conflicts that force the serial lane do not exist)
231
233
  // and records a note naming them (Adrian 2026-10-22: compile locally on the warm cache, qualify the conflicting tests on VMs).
232
234
  quietTests = "local",
233
- // The command prefix every cargo scan runs through: `rightkit cargo` on a managed host, plain `cargo` in CI.
234
- cargoCommand = ["rightkit", "cargo"],
235
+ // The command prefix every cargo scan runs through: `rustright cargo` on a managed host, plain `cargo` in CI.
236
+ cargoCommand = ["rustright", "cargo"],
235
237
  // Physical containment: a changed path whose real path leaves the real content root (a symlink) is refused.
236
238
  realpath = (p) => fs.realpathSync(p),
237
239
  // Bytes of a changed file for the input digest (defaults to readFile; the gates pass the raw reader).
@@ -260,8 +262,8 @@ export function planScans({
260
262
  rustLocks = null,
261
263
  } = {}) {
262
264
  if (!contentRoot) throw new TypeError("planScans: contentRoot is required");
263
- if (!Array.isArray(cargoCommand) || cargoCommand.length === 0 || !["rightkit", "cargo"].includes(cargoCommand[0]) || cargoCommand.slice(1).some((a) => a !== "cargo")) {
264
- throw new TypeError('planScans: cargoCommand must be ["rightkit", "cargo"] or ["cargo"]');
265
+ if (!Array.isArray(cargoCommand) || cargoCommand.length === 0 || !["rustright", "cargo"].includes(cargoCommand[0]) || cargoCommand.slice(1).some((a) => a !== "cargo")) {
266
+ throw new TypeError('planScans: cargoCommand must be ["rustright", "cargo"] or ["cargo"]');
265
267
  }
266
268
  const refusals = []; const accepted = [];
267
269
  for (const raw of new Set(changed)) {
@@ -434,6 +436,9 @@ export function planScans({
434
436
 
435
437
  // Exceptions expire by date, not by change: checked on every run, whatever changed.
436
438
  if (exists(path.join(contentRoot, "ci/advisory-exceptions.json"))) add({ id: "advisory-exceptions", class: A, internal: "advisory-exceptions" });
439
+ // Like generated-docs, planned only when a changed file is one of the contract's inputs or projections. The generator runs from
440
+ // the trusted scanner checkout (the import below); the exported contract and projections are data it checks.
441
+ if (files.some((f) => SUITE_API_INPUT.test(f)) && exists(path.join(contentRoot, "contracts/right-suite-api.v1.json"))) add({ id: "suite-api", class: A, internal: "suite-api" });
437
442
  if (rustTouched) {
438
443
  for (const ws of touchedWs) add({ id: `cargo-metadata${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, ...metadataArgs()) });
439
444
  for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, ...cargoIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
@@ -578,7 +583,7 @@ export function planScans({
578
583
  const deferred = [];
579
584
  if (!localCompile) {
580
585
  scans = scans.map((x) => {
581
- if (x.tool !== "rightkit" && !(x.tool === "cargo" && cargoCommand.length === 1)) return x;
586
+ if (x.tool !== "rustright" && !(x.tool === "cargo" && cargoCommand.length === 1)) return x;
582
587
  deferred.push(x.id);
583
588
  return { ...x, deferred: "no local compile grant; the broker refuses this scan" };
584
589
  });
@@ -758,6 +763,12 @@ const INTERNAL = {
758
763
  // Every RustSec exception is dated, scoped to the locked versions and identical in deny.toml and
759
764
  // .cargo/audit.toml (ci/advisory-exceptions.json; Astra 2026-10-22). Expiry fails every run.
760
765
  "advisory-exceptions": (s, ctx) => checkAdvisoryExceptions({ root: ctx.contentRoot, readFile: ctx.read }),
766
+ // Every Right Suite API projection must equal what the generator writes from the contract. The pure check is imported from the
767
+ // scanner's own checkout (scripts/ sits beside packages/release), never from the content root.
768
+ "suite-api": async (s, ctx) => {
769
+ const { suiteApiProblems } = await import("../../scripts/gen-suite-api.mjs");
770
+ return suiteApiProblems((f) => ctx.read(f));
771
+ },
761
772
  "ownership-catalog": (s, ctx) => (ctx.read(s.files[0]) === ctx.read(s.files[1]) ? [] : ["copy packages/release/rightkit-ownership.json to crates/rightkit-ownership/catalog/ownership-catalog.json"]),
762
773
  "changelog-version": (s, ctx) => {
763
774
  const version = (ctx.read(s.files[0]).match(/^version\s*=\s*"([^"]+)"/m) || [])[1];
package/development.mjs CHANGED
@@ -50,7 +50,7 @@ export function developmentEnvironment(env = process.env) {
50
50
  result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
51
51
  }
52
52
  // Same default as the agent-bin `rightkit` shim: children that run the managed client directly
53
- // (resolveCommand("rightkit"), build scripts) must not lose the broker endpoint.
53
+ // (resolveCommand("rustright"), build scripts) must not lose the broker endpoint.
54
54
  const socket = discoverBrokerSocket(result);
55
55
  if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
56
56
  }
@@ -298,7 +298,8 @@ function execute(command, { root, env, dryRun, run, label }) {
298
298
  export function resolveCommand(command, args, { root, env = process.env } = {}) {
299
299
  if (command === "node") return { command: process.execPath, args };
300
300
  if (command === "pnpm") return { command: process.execPath, args: [resolvePnpmCli({ root, env }), ...args] };
301
- if (command === "rightkit") {
301
+ if (command === "rightkit") throw new Error("`rightkit` is the retired name of the RustRight client (rustright#91); use rustright (cmd: \"rustright\")");
302
+ if (command === "rustright") {
302
303
  const client = managedRuntimeClient(env);
303
304
  if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
304
305
  const socket = discoverBrokerSocket(env);
@@ -0,0 +1,111 @@
1
+ // hostedWorkflows: { policy: "declared-unrelated", files: { "<file>.yml": "<reason>" | { reason, idToken } } }
2
+ //
3
+ // For a repository that ships more than the released app (a monorepo whose web or server pipeline is a hand-written
4
+ // GitHub workflow). The released app still builds, signs and uploads only through right-release on its native host;
5
+ // every hosted workflow must be declared with the reason it exists, and none may be able to build, sign or upload a
6
+ // release. What makes that hold is credentials: an admitted workflow reaches no secret but GITHUB_TOKEN (no secrets
7
+ // context, no `secrets: inherit`, no reusable workflow, no escape that could spell one), mints no OIDC token unless the
8
+ // file declares why (`idToken`), and runs only commit-pinned actions. Without an Apple, Azure or store credential it
9
+ // cannot sign or upload as Orthic Labs. The command and artifact patterns are a denylist backstop, not the guarantee:
10
+ // a script file the workflow runs is not read.
11
+ export const DECLARED_UNRELATED_POLICY = "declared-unrelated";
12
+
13
+ const FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,60}\.ya?ml$/;
14
+ const MIN_REASON = 12;
15
+
16
+ // Release machinery a declared-unrelated workflow must never run. Matched on the whole file, comments included: a comment cannot hide a command.
17
+ const RELEASE_MACHINERY = [
18
+ [/\bright-release\b/, "right-release"],
19
+ [/\bxcodebuild\b[^\n]*(?:\barchive\b|-exportArchive\b|-exportOptionsPlist\b)/, "xcodebuild archive/export"],
20
+ [/\bcodesign\b/, "codesign"],
21
+ [/\bnotarytool\b|\bstapler\b/, "notarization"],
22
+ [/\baltool\b|\biTMSTransporter\b|\bupload-testflight\b|\bapp-store-connect\b/i, "App Store upload"],
23
+ [/\bfastlane\b/, "fastlane"],
24
+ [/\bsecurity\s+import\b|\bapple-actions\//, "Apple signing keychain import"],
25
+ [/\bproductbuild\b|\bpkgbuild\b/, "installer package build"],
26
+ [/\bsigntool\b|\bAzureSignTool\b|\bazure\/(?:trusted|artifact)-signing/i, "Windows code signing"],
27
+ [/\btauri(?:-apps\/tauri-action|\s+build\b)/, "Tauri app build"],
28
+ ];
29
+
30
+ // A `uses:` must be a local action (./...), a docker image by digest, or owner/repo[/path]@<40-hex commit>.
31
+ const PINNED_USES = /^(?:\.\/[^\s]*|docker:\/\/[^\s@]+@sha256:[0-9a-f]{64}|[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+@[0-9a-f]{40})$/;
32
+
33
+ const normalize = (source) => source.replace(/\r\n?/g, "\n");
34
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
35
+ const entryReason = (entry) => (isObject(entry) ? entry.reason : entry);
36
+
37
+ /** Shape problems of a declared-unrelated hostedWorkflows value (an empty list when it is well formed). */
38
+ export function declaredUnrelatedShapeProblems(value) {
39
+ if (!isObject(value)) return ["hostedWorkflows must be a policy string or an object"];
40
+ const problems = [];
41
+ for (const key of Object.keys(value)) if (key !== "policy" && key !== "files") problems.push(`hostedWorkflows.${key} is not a known field (policy, files)`);
42
+ if (value.policy !== DECLARED_UNRELATED_POLICY) problems.push(`hostedWorkflows.policy must be "${DECLARED_UNRELATED_POLICY}"`);
43
+ const files = value.files;
44
+ if (!isObject(files) || Object.keys(files).length === 0) {
45
+ problems.push("hostedWorkflows.files must map each workflow file name to the reason it exists");
46
+ return problems;
47
+ }
48
+ for (const [file, entry] of Object.entries(files)) {
49
+ if (!FILE_NAME.test(file)) problems.push(`hostedWorkflows.files has an invalid workflow file name ${JSON.stringify(file)}`);
50
+ if (isObject(entry)) {
51
+ for (const key of Object.keys(entry)) if (key !== "reason" && key !== "idToken") problems.push(`hostedWorkflows.files["${file}"].${key} is not a known field (reason, idToken)`);
52
+ if (entry.idToken !== undefined && (typeof entry.idToken !== "string" || entry.idToken.trim().length < MIN_REASON)) problems.push(`hostedWorkflows.files["${file}"].idToken must say why the workflow mints an OIDC token (at least ${MIN_REASON} characters)`);
53
+ }
54
+ const reason = entryReason(entry);
55
+ if (typeof reason !== "string" || reason.trim().length < MIN_REASON) problems.push(`hostedWorkflows.files["${file}"] must say why the workflow exists (at least ${MIN_REASON} characters)`);
56
+ }
57
+ return problems;
58
+ }
59
+
60
+ function credentialRefusal(file, source, entry) {
61
+ // GitHub decodes YAML escapes before it evaluates expressions, so an escape could spell `secrets` or a command.
62
+ if (/\\[xuU]/.test(source)) return `${file} contains a \\x, \\u or \\U escape; a declared-unrelated workflow must spell every name literally`;
63
+ // The only secret is GITHUB_TOKEN; any other use of the word (secrets.X, secrets['X'], secrets: inherit, toJSON(secrets),
64
+ // join(secrets)) is refused. A path segment such as /run/secrets/ is not the context.
65
+ const rest = source.replace(/\bsecrets\.GITHUB_TOKEN\b/g, "");
66
+ if (/(?<![\w/-])secrets\b(?![\w/-])/i.test(rest)) return `${file} references secrets other than GITHUB_TOKEN (or passes the secrets context on); a declared-unrelated workflow can hold no signing or store credential`;
67
+ for (const [, ref] of source.matchAll(/^\s*(?:-\s+)?uses:\s*["']?([^\s"'#]+)/gm)) {
68
+ if (/\.github\/workflows\//.test(ref)) return `${file} calls the reusable workflow ${ref}; a called workflow is not checked`;
69
+ if (!PINNED_USES.test(ref)) return `${file} uses ${ref}, which is not pinned to a full commit sha (or a local action, or a docker image by digest)`;
70
+ }
71
+ if (/^\s*id-token\s*:\s*write\b/m.test(source) && !(isObject(entry) && entry.idToken)) return `${file} mints an OIDC token (id-token: write) without declaring why; set hostedWorkflows.files["${file}"] to { reason, idToken: "<why>" }`;
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Why a declared-unrelated workflow set is refused, or null when it is admitted.
77
+ * workflows: the file names present in .github/workflows; read(file): its text; artifacts: release artifact paths
78
+ * from the config's targets (a workflow that names one could produce or ship it).
79
+ */
80
+ export function declaredUnrelatedRefusal({ declaration, workflows, read, artifacts = [] }) {
81
+ const shape = declaredUnrelatedShapeProblems(declaration);
82
+ if (shape.length) return shape.join("; ");
83
+ const declared = Object.keys(declaration.files).sort();
84
+ const present = [...workflows].sort();
85
+ const undeclared = present.filter((file) => !declared.includes(file));
86
+ if (undeclared.length) return `undeclared hosted workflow file(s): ${undeclared.join(", ")} (declare each in hostedWorkflows.files with its reason, or remove it)`;
87
+ const missing = declared.filter((file) => !present.includes(file));
88
+ if (missing.length) return `hostedWorkflows.files declares workflow file(s) that do not exist: ${missing.join(", ")}`;
89
+ for (const file of present) {
90
+ const source = normalize(read(file));
91
+ const credential = credentialRefusal(file, source, declaration.files[file]);
92
+ if (credential) return credential;
93
+ for (const [pattern, label] of RELEASE_MACHINERY) {
94
+ if (pattern.test(source)) return `${file} runs release machinery (${label}); the released app builds, signs and uploads only through right-release on its native host`;
95
+ }
96
+ for (const artifact of artifacts) {
97
+ if (artifact && source.includes(artifact)) return `${file} names the release artifact ${artifact}`;
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+
103
+ /** Release artifact paths a config's targets declare (artifacts, iosPackage archive and ipa). */
104
+ export function releaseArtifactPaths(config) {
105
+ const paths = new Set();
106
+ for (const target of Object.values(config?.targets ?? {})) {
107
+ for (const artifact of target?.artifacts ?? []) if (typeof artifact === "string") paths.add(artifact);
108
+ for (const key of ["archive", "ipa"]) if (typeof target?.iosPackage?.[key] === "string") paths.add(target.iosPackage[key]);
109
+ }
110
+ return [...paths];
111
+ }
package/ios-dev.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  // | { kind: "device", device: "<devicectl identifier>", bundleId, productName?, launch?: true, launchArgs?: [] },
14
14
  // }
15
15
  // The build commands are generated from the config (a hand-written `build` is refused). The device is chosen by id only, never
16
- // "the only device": `install.device`, `--serial` or RIGHTKIT_IOS_DEVICE. DerivedData comes from `rightkit storage xcode-args`.
16
+ // "the only device": `install.device`, `--serial` or RIGHTKIT_IOS_DEVICE. DerivedData comes from `rustright storage xcode-args`.
17
17
  import * as nodeFs from "node:fs";
18
18
  import os from "node:os";
19
19
  import path from "node:path";
@@ -102,7 +102,7 @@ export function validateIosDevelopment(target, { root, env = process.env } = {})
102
102
  install = { ...descriptor, productName: descriptor.productName ?? target.scheme, launchArgs: [...(descriptor.launchArgs ?? [])] };
103
103
  }
104
104
  const normalized = { project, projectSpec, scheme: target.scheme, configuration, teamId: target.teamId, extraBuildSettings, prepare, install };
105
- return { ...normalized, build: iosDevelopmentBuildCommands(normalized, { root, storage: { derivedData: "<rightkit storage xcode-args -derivedDataPath>" }, destinationId: "<device or simulator id>" }) };
105
+ return { ...normalized, build: iosDevelopmentBuildCommands(normalized, { root, storage: { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, destinationId: "<device or simulator id>" }) };
106
106
  }
107
107
 
108
108
  const destinationFor = (install, id) => (install?.kind === "device" ? `id=${id}` : `platform=iOS Simulator,id=${id}`);
package/ios-host.mjs CHANGED
@@ -34,29 +34,29 @@ export function assertGuiSigningSession({ operation, env = process.env, run = sp
34
34
  }
35
35
 
36
36
  /**
37
- * DerivedData and result-bundle paths. With RIGHTKIT_RUN_ROOT set they come from `rightkit storage xcode-args --run-root <root>`
37
+ * DerivedData and result-bundle paths. With RIGHTKIT_RUN_ROOT set they come from `rustright storage xcode-args --run-root <root>`
38
38
  * (a JSON argv list holding -derivedDataPath and -resultBundlePath), the same source CodeRight and HeardRight use. Without a
39
39
  * managed run the fallback is a directory under the right-release state root, never an in-repo DerivedData.
40
40
  */
41
41
  export function resolveXcodeStorage({ env = process.env, run = spawnSync, cwd = process.cwd(), fallbackRoot }) {
42
42
  const runRoot = typeof env.RIGHTKIT_RUN_ROOT === "string" ? env.RIGHTKIT_RUN_ROOT.trim() : "";
43
43
  if (!runRoot) {
44
- if (!fallbackRoot) throw new IosHostError("IOS_STORAGE_UNAVAILABLE", "no RIGHTKIT_RUN_ROOT and no fallback storage root; run via: rightkit storage run --app <app> -- right-release ...");
44
+ if (!fallbackRoot) throw new IosHostError("IOS_STORAGE_UNAVAILABLE", "no RIGHTKIT_RUN_ROOT and no fallback storage root; run via: rustright storage run --app <app> -- right-release ...");
45
45
  return { derivedData: path.join(fallbackRoot, "DerivedData"), resultBundle: undefined, source: "fallback" };
46
46
  }
47
- const result = run("rightkit", ["storage", "xcode-args", "--run-root", runRoot], { cwd, env, encoding: "utf8", windowsHide: true, shell: false, timeout: 30_000 });
48
- if (result.error || result.status !== 0) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args failed (${result.status ?? "unavailable"})`);
47
+ const result = run("rustright", ["storage", "xcode-args", "--run-root", runRoot], { cwd, env, encoding: "utf8", windowsHide: true, shell: false, timeout: 30_000 });
48
+ if (result.error || result.status !== 0) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args failed (${result.status ?? "unavailable"})`);
49
49
  let args;
50
50
  try { args = JSON.parse(String(result.stdout ?? "")); } catch { args = null; }
51
- if (!Array.isArray(args) || !args.length || args.some((arg) => typeof arg !== "string")) throw new IosHostError("IOS_STORAGE_FAILED", "rightkit storage xcode-args returned no argv list");
51
+ if (!Array.isArray(args) || !args.length || args.some((arg) => typeof arg !== "string")) throw new IosHostError("IOS_STORAGE_FAILED", "rustright storage xcode-args returned no argv list");
52
52
  const value = (flag, required) => {
53
53
  const index = args.indexOf(flag);
54
54
  if (index < 0 || index + 1 >= args.length) {
55
- if (required) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args is missing ${flag}`);
55
+ if (required) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args is missing ${flag}`);
56
56
  return undefined;
57
57
  }
58
58
  const found = args[index + 1];
59
- if (!path.isAbsolute(found)) throw new IosHostError("IOS_STORAGE_FAILED", `rightkit storage xcode-args ${flag} is not an absolute path`);
59
+ if (!path.isAbsolute(found)) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args ${flag} is not an absolute path`);
60
60
  return found;
61
61
  };
62
62
  return { derivedData: value("-derivedDataPath", true), resultBundle: value("-resultBundlePath", false), source: "rightkit-storage" };
@@ -378,7 +378,7 @@ const plannedTrust = (spec) => {
378
378
  };
379
379
 
380
380
  /** The ordered plan (pure; no secret is part of any argument). Used by --dry-run and by the run itself. */
381
- export function iosPackagePlan(spec, { root, buildNumber = "<YYYYMMDDNN>", storage = { derivedData: "<rightkit storage xcode-args -derivedDataPath>" }, receiptDir }) {
381
+ export function iosPackagePlan(spec, { root, buildNumber = "<YYYYMMDDNN>", storage = { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, receiptDir }) {
382
382
  const optionsFile = path.join(receiptDir, "ios-export-options.plist");
383
383
  return {
384
384
  steps: [
@@ -504,7 +504,7 @@ export function scanForbiddenStrings(dir, strings, fs = nodeFs) {
504
504
  * scanFiles(label, files, o) release.mjs's assertNoQaControlBinaries (QA rules plus the release-switch binary layer)
505
505
  * detectSession / route GUI signing session seams (ios-host.mjs)
506
506
  * gitHead() the source commit; it must not change during the build
507
- * storage() DerivedData and result bundle (rightkit storage xcode-args)
507
+ * storage() DerivedData and result bundle (rustright storage xcode-args)
508
508
  * scanForbidden(dir, strings) forbidden-string scan
509
509
  * counterDir where the per-app build number counter lives (the vault state root under right-release build)
510
510
  */
package/local-publish.mjs CHANGED
@@ -529,7 +529,7 @@ async function publishUnit(parsed, deps, batch) {
529
529
  // #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed).
530
530
  try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: unitPath, platform, run, env, log }); }
531
531
  catch (error) { refuse(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
532
- packCommands.push(`rightkit cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
532
+ packCommands.push(`rustright cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
533
533
  }
534
534
  if (kind === "npm") {
535
535
  // The feed a tarball ships must list the version it ships with (#319): stage it before packing.
package/local-qualify.mjs CHANGED
@@ -370,16 +370,16 @@ function brokerIdentity(deps) {
370
370
  return { name: "rustright", client: String(client).replaceAll("\\", "/"), sha256: sha256((deps.readFile ?? fs.readFileSync)(client)) };
371
371
  }
372
372
 
373
- // Cargo only through the broker: `rightkit cargo`, or a `cargo` on PATH that is the managed shim (scripts/local-gate.mjs).
373
+ // Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim (scripts/local-gate.mjs).
374
374
  function detectCargoCommand(deps) {
375
375
  const which = (command) => {
376
376
  const result = (deps.run ?? spawnSync)((deps.platform ?? process.platform) === "win32" ? "where" : "which", [command], { encoding: "utf8", windowsHide: true });
377
377
  return result.status === 0 ? String(result.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
378
378
  };
379
- if (which("rightkit").length) return ["rightkit", "cargo"];
379
+ if (which("rustright").length) return ["rustright", "cargo"];
380
380
  const first = which("cargo")[0];
381
381
  if (first) { try { if (fs.readFileSync(first, "utf8").includes("cargo-guard.mjs")) return ["cargo"]; } catch { /* not the managed shim */ } }
382
- return refuse("neither `rightkit` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used");
382
+ return refuse("neither `rustright` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used");
383
383
  }
384
384
 
385
385
  const loadFromRoot = (root, file) => {
package/local-runtime.mjs CHANGED
@@ -176,7 +176,7 @@ export async function measureRuntimeCrate(options, deps = {}) {
176
176
  const run = deps.run ?? ((command, args, settings) => spawnCollect(command, args, { maxBuffer: 64 * 1024 * 1024, ...settings }));
177
177
  const invoke = (args, settings) => run(cargoCommand[0], [...cargoCommand.slice(1), ...args], { cwd: root, windowsHide: true, ...settings });
178
178
  const toolchainOf = async () => {
179
- const rustc = await run(cargoCommand[0] === "rightkit" ? "rightkit" : "rustc", cargoCommand[0] === "rightkit" ? ["rustc", "-Vv"] : ["-Vv"], { cwd: root, env: { ...baseEnv, ...brokerEnv }, windowsHide: true, timeout: 60_000 });
179
+ const rustc = await run(cargoCommand[0] === "rustright" ? "rustright" : "rustc", cargoCommand[0] === "rustright" ? ["rustc", "-Vv"] : ["-Vv"], { cwd: root, env: { ...baseEnv, ...brokerEnv }, windowsHide: true, timeout: 60_000 });
180
180
  const cargo = await invoke(["-V"], { env: { ...baseEnv, ...brokerEnv }, timeout: 60_000 });
181
181
  if (rustc.error || rustc.status !== 0 || cargo.error || cargo.status !== 0) throw new Error("cannot read the toolchain through the broker");
182
182
  return { rustc: String(rustc.stdout).trim(), cargo: String(cargo.stdout).trim() };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
@@ -352,11 +352,12 @@ export function admitInvocation(step, options = {}) {
352
352
  if (name === "right-release") return admitRightRelease(args, label);
353
353
  if (name === "pnpm") return admitPnpm(args, inner, label, chain);
354
354
  if (name === "node") return admitNode(args, inner, label);
355
- if (name === "rightkit") {
356
- if (args[0] !== "cargo" || !CARGO_SUBCOMMANDS.has(args[1])) return refuse("R1-managed-client", `${label}: only rightkit cargo ${[...CARGO_SUBCOMMANDS].join("|")} is admitted`);
357
- return shellSafeArgs(args, label) ?? admit(`rightkit-cargo-${args[1]}`, "managed-cargo");
355
+ if (name === "rightkit") return refuse("R1-retired-client", `${label}: rightkit is the retired name of the RustRight client (rustright#91); use rustright ${args.slice(0, 2).join(" ")}`);
356
+ if (name === "rustright") {
357
+ if (args[0] !== "cargo" || !CARGO_SUBCOMMANDS.has(args[1])) return refuse("R1-managed-client", `${label}: only rustright cargo ${[...CARGO_SUBCOMMANDS].join("|")} is admitted`);
358
+ return shellSafeArgs(args, label) ?? admit(`rustright-cargo-${args[1]}`, "managed-cargo");
358
359
  }
359
- if (name === "cargo" || name === "rustc" || name === "rustdoc") return refuse("R1-bare-cargo", `${label}: run Cargo through the managed client (rightkit cargo ...), never bare ${name}`);
360
+ if (name === "cargo" || name === "rustc" || name === "rustdoc") return refuse("R1-bare-cargo", `${label}: run Cargo through the managed client (rustright cargo ...), never bare ${name}`);
360
361
  if (name === "tauri") return refuse("R1-direct-tauri", `${label}: run the Tauri CLI as pnpm exec tauri ..., or through right-release; a bare tauri is not an approved installed runtime`);
361
362
  if (PACKAGE_MANAGERS.has(name)) return refuse("R1-package-manager", `${label}: ${name} is not allowed; the workspace uses pnpm`);
362
363
  if (INTERPRETERS.has(name)) return refuse("R1-interpreter", `${label}: ${name} runs script text or a script file the app owns; only an admitted shared generator may do that`);