@rightkit/release 0.6.24 → 0.6.26

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 (59) hide show
  1. package/README.md +11 -35
  2. package/android-dev.mjs +2 -1
  3. package/app-check.mjs +1 -2
  4. package/app-closure.mjs +3 -2
  5. package/assembled-app.mjs +11 -3
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +6 -0
  8. package/bin/rightkit-gate.mjs +83 -26
  9. package/build-helper-trace.mjs +264 -0
  10. package/build-helpers-command.mjs +184 -0
  11. package/build-launch.mjs +11 -75
  12. package/build-release.mjs +10 -11
  13. package/cargo-profile-contract.mjs +19 -29
  14. package/cargo-target-shim.mjs +31 -0
  15. package/cli/right-release.mjs +5 -7
  16. package/create-mac-updater.mjs +4 -0
  17. package/cross-targets.mjs +94 -0
  18. package/deterministic-scans.mjs +310 -20
  19. package/development.mjs +8 -8
  20. package/freshness-census.mjs +1 -1
  21. package/gate-stages.mjs +1 -1
  22. package/install-assembled-app.mjs +1 -1
  23. package/install-tauri-authenticode.mjs +3 -3
  24. package/local-publish.mjs +27 -5
  25. package/mac-package-recipe.mjs +55 -2
  26. package/managed-cargo.mjs +27 -1
  27. package/managed-roots.mjs +12 -4
  28. package/package.json +1 -5
  29. package/pipeline-admission.mjs +1 -1
  30. package/pipeline-receipts.mjs +302 -302
  31. package/provenance-verify.mjs +3 -1
  32. package/qa-build-features.mjs +11 -2
  33. package/release-qa-audit.mjs +9 -27
  34. package/release-qa-command.mjs +23 -64
  35. package/release-qa-fixtures.mjs +9 -5
  36. package/release-qa-floor.json +0 -1
  37. package/release-qa-policy.mjs +9 -10
  38. package/release-qa-receipt.mjs +6 -44
  39. package/release-source-identity.mjs +1 -12
  40. package/release-switch-registry.json +4 -1
  41. package/rightkit-registry-resolver.mjs +35 -3
  42. package/rightkit-versions.json +2 -2
  43. package/sign-windows.mjs +1 -1
  44. package/snapshot-broker.mjs +33 -8
  45. package/snapshot-metadata.mjs +32 -0
  46. package/snapshot-request.mjs +6 -3
  47. package/stage-sidecar.mjs +28 -11
  48. package/suite-signing-policy.mjs +8 -58
  49. package/templates/cargo-profiles.toml +6 -25
  50. package/test-command.mjs +40 -140
  51. package/updater-shipped-key.mjs +26 -0
  52. package/web-build.mjs +29 -4
  53. package/qa-journeys-prebuilt-run.mjs +0 -18
  54. package/qa-journeys-prebuilt.mjs +0 -148
  55. package/qa-layout.mjs +0 -229
  56. package/qa-snapshot-run.mjs +0 -53
  57. package/qa-snapshot.mjs +0 -164
  58. package/sign-qa-bundle.mjs +0 -210
  59. package/suite-signing.mjs +0 -259
@@ -5,7 +5,7 @@
5
5
  // 1. build the app without bundling (`target.build`: a `tauri build ... --no-bundle`; runDevelopment runs it before this module)
6
6
  // 2. patch the bundle marker (UNK -> NSS BEFORE signing: Tauri rewrites it during bundling, and doing that to signed bytes breaks Authenticode)
7
7
  // 3. sign the exe and its sidecars, then verify them (signer subject, timestamp)
8
- // 4. carried `tauri bundle --bundles nsis --debug` (the installer embeds the exe as signed)
8
+ // 4. carried `tauri bundle --bundles nsis` (the installer embeds the exe as signed)
9
9
  // 5. check the payload (the exe inside the installer is byte for byte the signed one)
10
10
  // 6. sign the installer, then verify it
11
11
  // 7. install (the silent installer, as the unsigned kind runs it)
@@ -74,10 +74,10 @@ export function validateAuthenticodeBuild(build, label = "development.targets.wi
74
74
  if (!builds) refuse("TAURI_AUTHENTICODE_BUILD", `${label} has no \`tauri build --no-bundle\` step: this install kind signs the raw exe the build leaves in its output directory`);
75
75
  }
76
76
 
77
- /** The carried bundle command (step 4): the same Tauri bundle the release makes, debug profile, no Tauri-side signing (the installer is signed by step 6). */
77
+ /** The carried bundle command (step 4): the same Tauri bundle the release makes (the release profile, GC-A07: no `--debug`), no Tauri-side signing (the installer is signed by step 6). */
78
78
  export function authenticodeBundleCommand(descriptor) {
79
79
  const override = { bundle: { createUpdaterArtifacts: false, windows: { signCommand: null } } };
80
- return { cmd: "pnpm", args: ["exec", "tauri", "bundle", "--bundles", "nsis", "--debug", "--target", descriptor.targetTriple, "--config", JSON.stringify(override)], ...(descriptor.tauriCwd ? { cwd: descriptor.tauriCwd } : {}), name: "tauri bundle" };
80
+ return { cmd: "pnpm", args: ["exec", "tauri", "bundle", "--bundles", "nsis", "--target", descriptor.targetTriple, "--config", JSON.stringify(override)], ...(descriptor.tauriCwd ? { cwd: descriptor.tauriCwd } : {}), name: "tauri bundle" };
81
81
  }
82
82
 
83
83
  const exeBase = (name) => String(name).replace(/\.exe$/i, "");
package/local-publish.mjs CHANGED
@@ -31,6 +31,7 @@ import { recordPublished, stageSourceVersions, workspaceFeedOptions } from "./ri
31
31
  import { prepareSwapHelper, SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
32
32
  import { exclusionsFor, npmContentDigest } from "./pack-digest.mjs";
33
33
  import { requireDoorRun, gateStateDir } from "./gate-stages.mjs";
34
+ import { crossCompileEnv, crossTargetsFor, oneOutput } from "./cross-targets.mjs";
34
35
 
35
36
  export const LOCAL_OPERATOR = "agent (local-build grant)";
36
37
  export const LOCAL_HOST_POLICY_REASON = "Adrian 2026-10-14 local-build grant (.rightkit-local-build.json); Windows covered by the next CI run";
@@ -341,6 +342,8 @@ const consumerDirs = (rows) => [...new Set(rows.map((row) => path.posix.dirname(
341
342
  // the file on disk must be the committed file, or the publish is refused.
342
343
  export const GATE_CONFIG_FILES = Object.freeze(["ci/gate.config.json", "ci/asset-gated.json"]);
343
344
 
345
+ export { CROSS_TARGETS, ZIG_TARGETS, CROSS_CC_ENV, crossTargetsFor, crossCompileEnv } from "./cross-targets.mjs";
346
+
344
347
  /** The CI gate's commands for one unit (scripts/ci/gate.mjs npmGate/rustGate), mirrored exactly. */
345
348
  export function localTestPlan({ kind, unit, dir, config, manifest, batch = false, platform = "darwin" }) {
346
349
  if (kind === "npm") {
@@ -367,6 +370,16 @@ export function localTestPlan({ kind, unit, dir, config, manifest, batch = false
367
370
  // Batch mode lints the whole batch once (batchClippyInvocations) and records it as a batch receipt.
368
371
  // Every tested feature set is linted (-D warnings), not only the first.
369
372
  if (!batch) for (const features of featureSets) steps.push({ command: "cargo", args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"], test: false });
373
+ // Every crate is checked for the other two OS targets too, every tested feature set, -D warnings (CROSS_TARGETS). Batch mode
374
+ // lints them with the batch's own clippy (batchClippyInvocations crossTargetsOf).
375
+ if (!batch) for (const target of crossTargetsFor(entry, platform)) {
376
+ for (const features of featureSets) {
377
+ const env = crossCompileEnv(target);
378
+ // GC-A14: a host without the zig C toolchain fails the check, never skips it.
379
+ if (!env) refuse(`RK_CROSS_CC_UNPROVISIONED: the zig C toolchain for ${target} is not provisioned; run node scripts/ci/provision-zig.mjs <managed data root>/zig (or set RIGHTKIT_CROSS_CC_DIR)`);
380
+ steps.push({ command: "cargo", args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", "--target", target, ...features, "--", "-D", "warnings"], test: false, ...(env ? { env } : {}) });
381
+ }
382
+ }
370
383
  for (const features of allFeatureCheckSets(entry)) steps.push({ command: "cargo", args: ["check", "-p", unit, "--message-format=json-render-diagnostics", "--all-targets", ...features], test: false });
371
384
  // rustdoc with -D warnings, for the tested and the checked feature sets: the same rows the CI shards (scripts/ci/gate.mjs) and
372
385
  // publish.yml's native verify run, so a broken or private intra-doc link fails this unit stage instead of a later verify.
@@ -385,7 +398,7 @@ export function localTestPlan({ kind, unit, dir, config, manifest, batch = false
385
398
  * names two crates: one `-p <crate>` run per crate and feature set, in layer order. Crates with a different
386
399
  * RIGHTKIT_REQUIRE_ASSETS (asset-gated) keep their own environment.
387
400
  */
388
- export function batchClippyInvocations(layer, { featuresOf = () => [], featureSetsOf = (unit) => [featuresOf(unit)], assetGatedOf = () => false } = {}) {
401
+ export function batchClippyInvocations(layer, { featuresOf = () => [], featureSetsOf = (unit) => [featuresOf(unit)], assetGatedOf = () => false, crossTargetsOf = () => [] } = {}) {
389
402
  const invocations = [];
390
403
  for (const unit of layer) {
391
404
  // Every tested feature set of a crate is linted, in order; identical sets of one crate collapse to one run.
@@ -397,6 +410,14 @@ export function batchClippyInvocations(layer, { featuresOf = () => [], featureSe
397
410
  crates: [unit], assetGated: assetGatedOf(unit),
398
411
  args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"],
399
412
  });
413
+ for (const target of crossTargetsOf(unit)) {
414
+ const env = crossCompileEnv(target);
415
+ if (!env) throw new Error(`RK_CROSS_CC_UNPROVISIONED: the zig C toolchain for ${target} is not provisioned; run node scripts/ci/provision-zig.mjs <managed data root>/zig (or set RIGHTKIT_CROSS_CC_DIR)`);
416
+ invocations.push({
417
+ crates: [unit], assetGated: assetGatedOf(unit), ...(env ? { env } : {}),
418
+ args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", "--target", target, ...features, "--", "-D", "warnings"],
419
+ });
420
+ }
400
421
  }
401
422
  }
402
423
  return invocations;
@@ -670,7 +691,7 @@ async function* publishUnitStages(parsed, deps, batch) {
670
691
  // A crate's own steps run in its Cargo workspace (its own Cargo.lock and target directory, #1285); a consumer row names its
671
692
  // separate workspace by --manifest-path relative to the repository root.
672
693
  const stepCwd = kind === "cargo" && step.command === "cargo" && !step.args.includes("--manifest-path") ? unitPath : workspaceRoot;
673
- const result = await run(step.command, step.command === "cargo" ? withCargoJobs(step.args, parsed.jobs) : step.args, { cwd: stepCwd, env: step.env ? { ...env, ...step.env } : env });
694
+ const result = await run(step.command, step.command === "cargo" ? oneOutput(withCargoJobs(step.args, parsed.jobs), { platform }) : step.args, { cwd: stepCwd, env: step.env ? { ...env, ...step.env } : env });
674
695
  const row = { command: commandText, ok: false, isTest: step.test && TEST_COMMAND.test(commandText) };
675
696
  if (result.error || result.status !== 0) {
676
697
  steps.push(row);
@@ -890,7 +911,7 @@ async function runLocalPublishBatch(parsed, deps) {
890
911
  const text = `${command} ${args.join(" ")}`;
891
912
  log(`[right-release] batch ${name}: ${text}`);
892
913
  for (const cwd of cwds) {
893
- const result = await run(command, args, { cwd, env: runEnv });
914
+ const result = await run(command, command === "cargo" ? oneOutput(args, { platform }) : args, { cwd, env: runEnv });
894
915
  if (result.error || result.status !== 0) refuse(`batch ${name} failed (${result.error?.message ?? `exit ${result.status}`}): ${text}${cwd === workspaceRoot ? "" : ` (in ${path.relative(workspaceRoot, cwd)})`}\n${tail(`${result.stdout ?? ""}\n${result.stderr ?? ""}`)}`);
895
916
  }
896
917
  checks.push({ name, command: text, ok: true, ...stamp, ...(crateNames ? { crates: crateNames } : {}) });
@@ -906,8 +927,9 @@ async function runLocalPublishBatch(parsed, deps) {
906
927
  commands.push(scanText);
907
928
  const featureSetsOf = (unit) => gateConfig?.rust?.[unit]?.featureSets ?? [[]];
908
929
  for (const layer of layers) {
909
- for (const invocation of batchClippyInvocations(layer, { featureSetsOf, assetGatedOf: isAssetGated })) {
910
- await batchCheck("clippy", "cargo", invocation.args, invocation.crates, testEnv(deps, invocation.assetGated), invocation.crates.map((unit) => path.join(workspaceRoot, "crates", unit)));
930
+ for (const invocation of batchClippyInvocations(layer, { featureSetsOf, assetGatedOf: isAssetGated, crossTargetsOf: (unit) => crossTargetsFor(gateConfig?.rust?.[unit], platform) })) {
931
+ // A cross-target invocation carries the zig C toolchain environment for its target (crossCompileEnv).
932
+ await batchCheck("clippy", "cargo", invocation.args, invocation.crates, { ...testEnv(deps, invocation.assetGated), ...(invocation.env ?? {}) }, invocation.crates.map((unit) => path.join(workspaceRoot, "crates", unit)));
911
933
  }
912
934
  }
913
935
  const receipt = { id: `batch-${head.slice(0, 12)}-${sha256(JSON.stringify(layers)).slice(0, 8)}`, ...stamp, crates, checks };
@@ -23,6 +23,7 @@ import { notarizeMacosArtifact } from "./macos-notarization.mjs";
23
23
  import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
24
24
  import { runMacCodesignCommands, signMacUpdaterFiles } from "./macos-signing-session.mjs";
25
25
  import { discoverNestedCode } from "./swift-mac-app.mjs";
26
+ import { assertUpdaterMatchesShippedKey } from "./updater-shipped-key.mjs";
26
27
 
27
28
  /**
28
29
  * Typed error. Codes: MAC_PACKAGE_CONFIG_INVALID, MAC_PACKAGE_HOST_UNSUPPORTED, MAC_PACKAGE_INPUT_MISSING,
@@ -35,7 +36,7 @@ export class MacPackageRecipeError extends Error {
35
36
 
36
37
  export const MAC_PACKAGE_STEPS = Object.freeze(["universal", "sign-nested", "sign-app", "dmg", "sign-dmg", "notarize", "updater"]);
37
38
  const UNIVERSAL_ARCHS = ["x86_64", "arm64"];
38
- const MACPACKAGE_KEYS = ["universal", "dmg", "notarize", "updater", "nested", "entitlements"];
39
+ const MACPACKAGE_KEYS = ["universal", "dmg", "notarize", "updater", "nested", "entitlements", "requiredEntitlements"];
39
40
  const DMG_KEYS = ["name", "background", "windowSize", "iconPositions"];
40
41
  const ICON_KEYS = ["app", "applications"];
41
42
 
@@ -98,7 +99,20 @@ export function validateMacPackageConfig(macPackage) {
98
99
  if (new Set(nested).size !== nested.length) throw invalid("nested globs must be unique");
99
100
  }
100
101
  if (macPackage.entitlements !== undefined && (typeof macPackage.entitlements !== "string" || !macPackage.entitlements.trim())) throw invalid("entitlements must be a path string");
101
- return { universal, dmg: normalized, notarize, updater, nested, ...(macPackage.entitlements ? { entitlements: macPackage.entitlements } : {}) };
102
+ // rightkit#1443 (hrmac: R2 checked the production APNs entitlement in the signed app): `requiredEntitlements` maps a bundle-relative
103
+ // executable to entitlement keys and the exact value each must carry after signing (a string, or true for a boolean entitlement).
104
+ let requiredEntitlements = null;
105
+ if (macPackage.requiredEntitlements !== undefined) {
106
+ const re = macPackage.requiredEntitlements;
107
+ if (!re || typeof re !== "object" || Array.isArray(re) || !Object.keys(re).length) throw invalid("requiredEntitlements must map bundle-relative executables to {entitlement: value}");
108
+ for (const [file, keys] of Object.entries(re)) {
109
+ validateGlob(file, `requiredEntitlements key ${file}`);
110
+ if (!keys || typeof keys !== "object" || Array.isArray(keys) || !Object.keys(keys).length) throw invalid(`requiredEntitlements.${file} must be a non-empty {entitlement: value} object`);
111
+ for (const [k, v] of Object.entries(keys)) if (!(typeof v === "string" || v === true)) throw invalid(`requiredEntitlements.${file}.${k} must be a string or true`);
112
+ }
113
+ requiredEntitlements = re;
114
+ }
115
+ return { universal, dmg: normalized, notarize, updater, nested, ...(macPackage.entitlements ? { entitlements: macPackage.entitlements } : {}), ...(requiredEntitlements ? { requiredEntitlements } : {}) };
102
116
  }
103
117
 
104
118
  /**
@@ -160,6 +174,35 @@ const defaultInspect = (cmd, args) => {
160
174
  * listFiles(dir) -> bundle-relative posix paths, buildDmg(options), notarize(options), run(cmd, args, opts),
161
175
  * signUpdater(files, options), receiptPath, log(event).
162
176
  */
177
+ /** Every required entitlement is present, with its exact value, in the signed executable (`codesign -d --entitlements :-`, an XML plist). */
178
+ export function macRequiredEntitlementsCheck({ app, required, run = spawnSync, env = process.env }) {
179
+ const esc = (t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
+ for (const [file, keys] of Object.entries(required)) {
181
+ const target = path.join(app, ...file.split("/"));
182
+ const r = run("codesign", ["-d", "--entitlements", ":-", target], { env, encoding: "utf8" });
183
+ if (!r || r.error || r.status !== 0) throw new Error(`codesign -d --entitlements on ${file} failed (${r?.error?.message ?? `exit ${r?.status}`})`);
184
+ const plist = String(r.stdout ?? "");
185
+ for (const [k, v] of Object.entries(keys)) {
186
+ const want = v === true ? "<true\\s*/>" : `<string>${esc(String(v))}</string>`;
187
+ if (!new RegExp(`<key>${esc(k)}</key>\\s*${want}`).test(plist)) throw new Error(`${file} is signed without the required entitlement ${k} = ${JSON.stringify(v)}`);
188
+ }
189
+ }
190
+ }
191
+
192
+ /** Gatekeeper's verdict on the shipped app and DMG, as a user's Mac reaches it: staple validation and spctl assessment. Throws on any refusal. */
193
+ export function macGatekeeperCheck({ app, dmg, run = spawnSync, env = process.env }) {
194
+ const checks = [
195
+ ["xcrun", ["stapler", "validate", app]],
196
+ ["xcrun", ["stapler", "validate", dmg]],
197
+ ["spctl", ["--assess", "--type", "execute", "-v", app]],
198
+ ["spctl", ["--assess", "--type", "open", "--context", "context:primary-signature", "-v", dmg]],
199
+ ];
200
+ for (const [cmd, args] of checks) {
201
+ const r = run(cmd, args, { env, encoding: "utf8" });
202
+ if (!r || r.error || r.status !== 0) throw new Error(`${cmd} ${args.join(" ")} refused (${r?.error?.message ?? `exit ${r?.status}`}): ${String(r?.stderr ?? r?.stdout ?? "").trim().split(/\r?\n/).slice(-2).join(" | ")}`);
203
+ }
204
+ }
205
+
163
206
  export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} }) {
164
207
  let artifactPaths = null;
165
208
  const { recipe, app: declaredApp, dmgFile } = validateMacPackageTarget(target);
@@ -254,6 +297,10 @@ export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} })
254
297
  }
255
298
  try { signing.push(...signMacosEntries([{ file: app, hardenedRuntime: true, options: [], ...(entitlements ? { entitlements } : {}) }], { identity, ...signOptions })); }
256
299
  catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "app signing", error); }
300
+ if (recipe.requiredEntitlements) {
301
+ try { (deps.entitlementsCheck ?? macRequiredEntitlementsCheck)({ app, required: recipe.requiredEntitlements, run, env }); }
302
+ catch (error) { throw wrap("MAC_PACKAGE_SIGN_FAILED", "required entitlements", error); }
303
+ }
257
304
  record("sign-app");
258
305
 
259
306
  // 4) branded DMG from the signed app.
@@ -280,6 +327,10 @@ export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} })
280
327
  try { receipt = notarize({ archive: dmg, app, receiptPath, env, run }); }
281
328
  catch (error) { throw wrap("MAC_PACKAGE_NOTARIZE_FAILED", "notarization", error); }
282
329
  if (!receipt || receipt.notarization?.status !== "Accepted") throw new MacPackageRecipeError("MAC_PACKAGE_NOTARIZE_FAILED", `macPackage notarization did not report Accepted for ${dmg}`);
330
+ // rightkit#1443 (hrmac: R2's verify-macos-release-artifacts.mjs did this; the gate had dropped it): what a user's Mac will check, checked
331
+ // here: the staple of the app and the DMG validates, and Gatekeeper accepts the app (execute) and the DMG (open, primary signature).
332
+ try { (deps.gatekeeper ?? macGatekeeperCheck)({ app, dmg, run, env }); }
333
+ catch (error) { throw wrap("MAC_PACKAGE_NOTARIZE_FAILED", "Gatekeeper and staple validation", error); }
283
334
  record("notarize");
284
335
  }
285
336
 
@@ -296,6 +347,8 @@ export function runMacPackageRecipe({ appRoot, config = {}, target, deps = {} })
296
347
  try { signUpdater([updater], { cwd: appRoot, env }); }
297
348
  catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater signing", error); }
298
349
  for (const file of updaterFiles) if (!fs.existsSync(file)) throw missing("updater artifact", file);
350
+ try { (deps.verifyUpdater ?? assertUpdaterMatchesShippedKey)({ appRoot, file: updater }); }
351
+ catch (error) { throw wrap("MAC_PACKAGE_UPDATER_FAILED", "updater signature against the shipped key", error); }
299
352
  record("updater");
300
353
  }
301
354
  } catch (error) {
package/managed-cargo.mjs CHANGED
@@ -156,7 +156,31 @@ export function realCargoOf(candidates, readHead = fileHead, readMap = (file) =>
156
156
  * RIGHTKIT_MANAGED_AGENT_BIN, or a script that routes through the broker, read 4 KiB at most), and the first real cargo left on it.
157
157
  * Plain `cargo` then can only be a real toolchain; with none, a cargo launch fails as missing, never through a shim.
158
158
  */
159
- export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead, platform = process.platform, gateWrappers = false } = {}) {
159
+ /**
160
+ * The broker's own registered toolchain on a broker host (Mac, Dell): `{ cargo, dirs }` from its config's `tools` (cargo's toolchain bin first,
161
+ * then the registered cargo plugins cargo-deny, cargo-audit, ...), or null when no broker config is readable (Linux, CI). The gate's direct,
162
+ * non-compiling cargo (GATE-CONTRACT GC-A11) runs exactly the broker's pinned toolchain and plugins, never whatever PATH or rustup's
163
+ * default holds (the Dell has no cargo on PATH; rustup's default there is another toolchain). The config is the live one at the managed
164
+ * control root, else the data root's (a host whose control-root move has not happened yet).
165
+ */
166
+ const BROKER_PLUGIN_TOOLS = Object.freeze(["cargo-deny", "cargo-audit", "cargo-about", "cargo-outdated", "cargo-machete", "cargo-upgrade", "cargo-set-version"]);
167
+ export function brokerToolchain({ env = process.env, platform = process.platform, readFile = (f) => readFileSync(f, "utf8"), exists = existsSync } = {}) {
168
+ let roots; try { roots = resolveManagedRoots({ env, platform }); } catch { return null; }
169
+ const paths = platform === "win32" ? path.win32 : path.posix;
170
+ for (const file of [roots.configPath, roots.dataRoot ? paths.join(roots.dataRoot, "config.json") : null].filter(Boolean)) {
171
+ let config; try { if (!exists(file)) continue; config = JSON.parse(readFile(file)); } catch { continue; }
172
+ const tools = Array.isArray(config?.tools) ? config.tools : [];
173
+ const byId = new Map(tools.filter((t) => t && typeof t.id === "string" && typeof t.path === "string").map((t) => [t.id, t]));
174
+ const cargo = byId.get("cargo");
175
+ if (!cargo || !exists(cargo.path)) continue;
176
+ const dirOf = (t) => (typeof t.root === "string" && t.root ? t.root : paths.dirname(t.path));
177
+ const dirs = [dirOf(cargo), ...BROKER_PLUGIN_TOOLS.map((id) => byId.get(id)).filter((t) => t && exists(t.path)).map(dirOf)];
178
+ return { cargo: cargo.path, dirs: [...new Set(dirs)], config: file };
179
+ }
180
+ return null;
181
+ }
182
+
183
+ export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead, platform = process.platform, gateWrappers = false, broker = brokerToolchain({ env, platform, exists }) } = {}) {
160
184
  // `platform` only matters for a forced direct run on Windows (a recorded broker exception): ";" separators, backslash paths, cargo.exe.
161
185
  const win = platform === "win32";
162
186
  const norm = (d) => (win ? String(d).replaceAll("\\", "/") : d);
@@ -173,6 +197,8 @@ export function directCargoPath({ env = process.env, exists = existsSync, readHe
173
197
  kept.push(dir);
174
198
  if (!cargo && found) cargo = found;
175
199
  }
200
+ // A broker host: its registered toolchain and plugins come first, and its cargo is the one used.
201
+ if (broker) return { path: [...broker.dirs, ...kept.filter((d) => !broker.dirs.includes(d))].join(win ? ";" : ":"), dropped, cargo: broker.cargo, broker: broker.config };
176
202
  return { path: kept.join(win ? ";" : ":"), dropped, cargo };
177
203
  }
178
204
 
package/managed-roots.mjs CHANGED
@@ -27,17 +27,23 @@ export const envNamed = (env, name) => {
27
27
  * way (rustright 07829c05, pinned in the shared fixtures as "linux no-env defaults to the account state directory"). The broker's answering roots, not either
28
28
  * resolver's guess, say where a running broker keeps its state.
29
29
  */
30
- export function resolveManagedRoots({ env = process.env, platform = process.platform, home = homedir(), controlRoot, dataRoot, runtimeRoot } = {}) {
30
+ export function resolveManagedRoots({ env = process.env, platform = process.platform, home = homedir(), controlRoot, dataRoot, runtimeRoot, windowsControlDefault = 'localappdata' } = {}) {
31
31
  const paths = platform === 'win32' ? win32 : posix;
32
32
  const legacyRoot = platform === 'win32' ? 'D:\\.rightkit-managed' : '/Volumes/D/.rightkit-managed';
33
33
  const managedPlatform = platform === 'darwin' || platform === 'win32';
34
34
  dataRoot ??= envNamed(env, 'DATA_ROOT') || envNamed(env, 'MANAGED_ROOT') || envNamed(env, 'EXTERNAL_ROOT') || (managedPlatform ? legacyRoot : paths.join(home, '.local', 'state', 'rightkit'));
35
+ // rustright#54 (Adrian, 2026-11-02: "all apps are installed in internal drives ... doesn't rr belong there? the dev workspace sits on d"): the
36
+ // Windows control root (config, logs, policy, agent-bin, attestation) is %LOCALAPPDATA%\RightKit\build-control, as the Mac's is under Application
37
+ // Support; build data (ws, sccache, cargo home, snapshots) stays on the data root. Flipped together with rustright-config WindowsControlDefault.
38
+ const localAppData = () => env.LOCALAPPDATA || paths.join(home, 'AppData', 'Local');
35
39
  controlRoot ??= envNamed(env, 'CONTROL_ROOT') || (platform === 'darwin'
36
- ? paths.join(home, 'Library', 'Application Support', 'RightKit', 'build-control') : dataRoot);
37
- runtimeRoot ??= envNamed(env, 'RUNTIME_ROOT') || (platform === 'darwin' ? paths.join(home, 'Library', 'Application Support', 'RustRight') : platform === 'win32' ? 'D:\\.rustright' : controlRoot);
40
+ ? paths.join(home, 'Library', 'Application Support', 'RightKit', 'build-control')
41
+ : platform === 'win32' && windowsControlDefault === 'localappdata' ? paths.join(localAppData(), 'RightKit', 'build-control') : dataRoot);
42
+ runtimeRoot ??= envNamed(env, 'RUNTIME_ROOT') || (platform === 'darwin' ? paths.join(home, 'Library', 'Application Support', 'RustRight') : platform === 'win32' ? (windowsControlDefault === 'localappdata' ? paths.join(localAppData(), 'RustRight') : 'D:\\.rustright') : controlRoot);
38
43
  return Object.freeze({ controlRoot, dataRoot, legacyRoot, runtimeCurrent: paths.join(controlRoot, 'runtime'), runtimeRoot, liveRuntime: paths.join(runtimeRoot, 'runtime'),
39
44
  configPath: paths.join(controlRoot, 'config.json'), socket: platform === 'win32' ? null : paths.join(controlRoot, 'broker.sock'),
40
- logs: paths.join(controlRoot, 'logs'), agentBin: paths.join(controlRoot, 'agent-bin'),
45
+ // rustright 303c6c6: on Windows agent-bin stays on the data root (agents' PATH and broker job PATH use D:\\.rightkit-managed\\agent-bin).
46
+ logs: paths.join(controlRoot, 'logs'), agentBin: platform === 'win32' ? paths.join(dataRoot, 'agent-bin') : paths.join(controlRoot, 'agent-bin'),
41
47
  agentPolicy: paths.join(controlRoot, 'agent-policy'), ownershipInstall: paths.join(controlRoot, 'ownership-install'),
42
48
  // The Mac signing agent already lives on the internal disk; moving it would force a re-provision.
43
49
  signingAgent: platform === 'darwin' ? paths.join(home, 'Library', 'Application Support', 'Orthic Labs', 'shared', 'signing-agent')
@@ -144,6 +150,8 @@ export function managedRuntimeClient(env = process.env, platform = process.platf
144
150
  const roots = managedControlCandidates({ env, platform, home });
145
151
  const configured = resolveManagedRoots({ env, platform, home });
146
152
  const ordered = [paths.join(configured.liveRuntime, exe)];
153
+ // rustright#54: until a Windows host's cutover moves the runtime to %LOCALAPPDATA%\\RustRight, its live runtime is still the legacy one.
154
+ if (platform === 'win32') ordered.push(paths.join(resolveManagedRoots({ env, platform, home, windowsControlDefault: 'legacy' }).liveRuntime, exe));
147
155
  for (const root of roots) ordered.push(paths.join(root, 'runtime', exe));
148
156
  for (const root of roots) ordered.push(paths.join(root, 'runtime-current', exe)); // the pre-cutover link: last, never preferred over the live runtime
149
157
  if (platform === 'darwin') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.6.24",
3
+ "version": "0.6.26",
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",
@@ -100,12 +100,8 @@
100
100
  "smol-toml": "1.9.1"
101
101
  },
102
102
  "scripts": {
103
- "test": "node test-support/run-test-shards.mjs --class=unit",
104
- "test:broker": "node test-support/run-test-shards.mjs --class=broker",
105
- "test:all": "node test-support/run-test-shards.mjs --class=all",
106
103
  "test:registry-parity": "node registry-parity.mjs --allow-unpublished",
107
104
  "test:signer:windows": "node sign-release-manifest.integration.mjs",
108
- "doctor:all": "node --test right-suite-contract.test.mjs",
109
105
  "verify:standalone": "node standalone-clone-verify.mjs"
110
106
  }
111
107
  }
@@ -40,7 +40,7 @@ export const hasShellMeta = (text) => SHELL_META.test(String(text));
40
40
  /** right-release subcommands a config may invoke (the CLI's own commands; unknown names are refused). */
41
41
  export const OWNER_SUBCOMMANDS = Object.freeze(new Set([
42
42
  "release", "build", "package", "publish", "upload", "deps", "publish-update", "hardening", "harden", "hardeningscan", "sign-windows",
43
- "sign-updater", "sign-dev-artifact", "sign-qa-bundle", "sign-candidate", "engine-locate", "stage-sidecar", "init", "pe", "icons", "recipe-build",
43
+ "sign-updater", "sign-dev-artifact", "sign-candidate", "engine-locate", "stage-sidecar", "init", "pe", "icons", "recipe-build",
44
44
  "signing-agent", "create-mac-updater", "mirror-root-artifact", "test", "web-build", "license-notices", "toolchain", "github",
45
45
  "cargo-target", "qa", "ownership", "ownership-approval", "ownership-report", "ownership-mandatory", "adoption-evidence", "swift-package", "bundle-scan",
46
46
  "check-release-dependencies", "dist-scan", "identity-scan", "apple-profile-create", "ios-credentials", "dev", "doctor", "hooks",