@rightkit/release 0.6.24 → 0.6.25

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 (58) 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-roots.mjs +12 -4
  27. package/package.json +1 -5
  28. package/pipeline-admission.mjs +1 -1
  29. package/pipeline-receipts.mjs +302 -302
  30. package/provenance-verify.mjs +3 -1
  31. package/qa-build-features.mjs +11 -2
  32. package/release-qa-audit.mjs +9 -27
  33. package/release-qa-command.mjs +23 -64
  34. package/release-qa-fixtures.mjs +9 -5
  35. package/release-qa-floor.json +0 -1
  36. package/release-qa-policy.mjs +9 -10
  37. package/release-qa-receipt.mjs +6 -44
  38. package/release-source-identity.mjs +1 -12
  39. package/release-switch-registry.json +4 -1
  40. package/rightkit-registry-resolver.mjs +35 -3
  41. package/rightkit-versions.json +2 -2
  42. package/sign-windows.mjs +1 -1
  43. package/snapshot-broker.mjs +33 -8
  44. package/snapshot-metadata.mjs +32 -0
  45. package/snapshot-request.mjs +6 -3
  46. package/stage-sidecar.mjs +28 -11
  47. package/suite-signing-policy.mjs +8 -58
  48. package/templates/cargo-profiles.toml +6 -25
  49. package/test-command.mjs +40 -140
  50. package/updater-shipped-key.mjs +26 -0
  51. package/web-build.mjs +29 -4
  52. package/qa-journeys-prebuilt-run.mjs +0 -18
  53. package/qa-journeys-prebuilt.mjs +0 -148
  54. package/qa-layout.mjs +0 -229
  55. package/qa-snapshot-run.mjs +0 -53
  56. package/qa-snapshot.mjs +0 -164
  57. package/sign-qa-bundle.mjs +0 -210
  58. 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-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.25",
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",