@rightkit/release 0.2.65 → 0.2.66

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/build-release.mjs CHANGED
@@ -26,7 +26,8 @@ import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInpu
26
26
  import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
27
27
  import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
28
28
  import { createTargetBridge } from "./target-bridge.mjs";
29
- import { managedCargoShimOnPath } from "./cargo-contract.mjs";
29
+ import { brokerManagedHost } from "./cargo-contract.mjs";
30
+ import { resolveTargetRoot } from "./cargo-target.mjs";
30
31
  import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
31
32
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
32
33
  import { assertCleanSource } from "./source-gate.mjs";
@@ -174,13 +175,23 @@ try {
174
175
  // shells take the Cache V2 branch, bridge src-tauri/target into the release cache,
175
176
  // and then lose CARGO_TARGET_DIR to the broker's unconditional override — so the
176
177
  // app bundle landed in the broker workspace and packaging failed on an empty cache.
177
- const managedCargoTarget = (process.env.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(process.env))
178
- ? resolveManagedCargoTarget(cargoToml)
179
- : null;
178
+ const managedCargoTarget = brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
180
179
  const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
181
180
  const cacheKey = hashFileText(JSON.stringify({ cache: cacheIdentity.fingerprint, signingIdentity })).slice(0, 16);
182
- const cacheMode = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
183
- if (cacheMode !== "legacy" && cacheMode !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
181
+ const cacheModeRequested = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
182
+ if (cacheModeRequested !== "legacy" && cacheModeRequested !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
183
+ // On a broker-managed host, Cache V2's lease/slot/prune/floor/mark chain is not
184
+ // running against the directory that will actually hold build output — the
185
+ // broker owns CARGO_TARGET_DIR unconditionally (see managedCargoTarget above).
186
+ // Leaving cacheMode at "shared" there ran that whole chain against an
187
+ // essentially unrelated cache root: acquireSuiteBuildSlot/acquireCacheLease
188
+ // guarded a lock nothing else respects, and assertWriteVolumeFloors could
189
+ // fail() the build outright on a volume that isn't where the real ~18 GB of
190
+ // output lives. Force broker so none of that machinery runs.
191
+ const cacheMode = managedCargoTarget ? "broker" : cacheModeRequested;
192
+ if (managedCargoTarget && cacheModeRequested === "shared") {
193
+ console.error("[right-release] broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 shared-cache lease/prune/floor checks skipped");
194
+ }
184
195
  const sharedCacheRoot = cacheMode === "shared" ? resolveSharedCacheRoot({ platform, env: process.env }) : undefined;
185
196
  const sharedLayout = cacheMode === "shared"
186
197
  ? resolveCacheLayout({ cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, fingerprint: cacheKey, kind: "release" })
@@ -216,9 +227,9 @@ try {
216
227
  delete env[name];
217
228
  }
218
229
  }
219
- if (cacheMode === "legacy") delete env.RIGHT_RELEASE_CACHE_OWNER;
230
+ if (cacheMode !== "shared") delete env.RIGHT_RELEASE_CACHE_OWNER;
220
231
  if (cacheMode === "shared" && env.RIGHT_RELEASE_CACHE_OWNER !== "rightkit-v2") fail("shared cache ownership token was not configured");
221
- if (cacheMode === "legacy" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
232
+ if (cacheMode !== "shared" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
222
233
  if (process.env.RIGHTSUITE_HEAVY_WORK_OWNER !== "1") heavySlot = acquireHeavyWorkSlot();
223
234
  if (cacheMode === "shared") {
224
235
  suiteSlot = acquireSuiteBuildSlot({ layout: sharedLayout, pid: process.pid, argv: process.argv, waitMs: Number(process.env.BUILD_GUARD_WAIT_SECS ?? 900) * 1000 });
@@ -250,6 +261,7 @@ try {
250
261
  link: targetLink,
251
262
  target: managedCargoTarget ?? env.CARGO_TARGET_DIR,
252
263
  ownedRoot: path.dirname(managedCargoTarget ?? env.CARGO_TARGET_DIR),
264
+ brokerManaged: Boolean(managedCargoTarget),
253
265
  });
254
266
 
255
267
  const result = await targetBridge.run(async () => runBuildStateMachine({
@@ -285,7 +297,13 @@ try {
285
297
  throwIfInterrupted();
286
298
  const cacheTarget = managedCargoTarget ?? env.CARGO_TARGET_DIR;
287
299
  mkdirSync(cacheTarget, { recursive: true });
288
- if (cacheMode === "shared") targetBridge.ensure();
300
+ // Broker-managed hosts route through the same target bridge as shared
301
+ // mode even though cacheMode is forced to "broker" above: the naive
302
+ // else-branch below fail()s outright when targetLink is a real
303
+ // directory, which is exactly the "corruption" misreading that nearly
304
+ // cost ~5.9 GB of warm broker intermediates. The bridge's brokerManaged
305
+ // flag already knows to leave a real directory untouched instead.
306
+ if (cacheMode === "shared" || managedCargoTarget) targetBridge.ensure();
289
307
  else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
290
308
  else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
291
309
  fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
@@ -427,18 +445,6 @@ function collectToolVersions(packageManager) {
427
445
  }
428
446
  }
429
447
 
430
- function resolveManagedCargoTarget(manifestPath) {
431
- if (!manifestPath) fail("managed release requires a Cargo.toml build input");
432
- const output = commandOutputAt(
433
- "cargo",
434
- ["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath],
435
- path.dirname(manifestPath),
436
- );
437
- const target = JSON.parse(output).target_directory;
438
- if (!target || !path.isAbsolute(target)) fail("managed Cargo metadata returned no absolute target directory");
439
- return target;
440
- }
441
-
442
448
  async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null) {
443
449
  const inactivityMs = Number(process.env.RIGHT_RELEASE_STALL_MS ?? 10 * 60 * 1000);
444
450
  const absoluteMs = Number(process.env.RIGHT_RELEASE_ABSOLUTE_MS ?? 90 * 60 * 1000);
@@ -6,7 +6,7 @@ import { execFileSync, spawnSync } from "node:child_process";
6
6
  import test from "node:test";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseCpuSeconds, processGroupCpuSeconds } from "./process-liveness.mjs";
9
- import { managedCargoShimOnPath } from "./cargo-contract.mjs";
9
+ import { brokerManagedHost } from "./cargo-contract.mjs";
10
10
 
11
11
  const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
12
12
  const build = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs");
@@ -27,7 +27,7 @@ test("macOS and Windows builds default to Cache V2 while explicit legacy mode re
27
27
  assert.match(source, /right-release build/);
28
28
  assert.match(source, /RIGHT_RELEASE_CACHE_MODE/);
29
29
  assert.match(source, /platform === "mac" \|\| platform === "win" \? "shared" : "legacy"/);
30
- assert.match(source, /cacheMode !== "legacy" && cacheMode !== "shared"/);
30
+ assert.match(source, /cacheModeRequested !== "legacy" && cacheModeRequested !== "shared"/);
31
31
  assert.match(source, /resolveSharedCacheRoot/);
32
32
  assert.match(source, /acquireSuiteBuildSlot/);
33
33
  assert.match(source, /acquireHeavyWorkSlot/);
@@ -74,7 +74,30 @@ test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", ()
74
74
  // next build until someone deletes the link by hand.
75
75
  assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\)/);
76
76
  assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
77
- assert.match(source, /if \(cacheMode === "shared"\) targetBridge\.ensure\(\)/);
77
+ // A broker-managed host also routes through the bridge even though cacheMode
78
+ // is forced to "broker" there — its brokerManaged flag is what keeps a real
79
+ // (non-symlink) target directory from being treated as corruption.
80
+ assert.match(source, /if \(cacheMode === "shared" \|\| managedCargoTarget\) targetBridge\.ensure\(\)/);
81
+ });
82
+
83
+ test("a broker-managed host forces broker cacheMode so Cache V2's lease/prune/floor chain never runs against an unrelated volume", () => {
84
+ assert.match(
85
+ source,
86
+ /const cacheMode = managedCargoTarget \? "broker" : cacheModeRequested;/,
87
+ );
88
+ // Every Cache V2 lease/slot/prune/floor/mark call stays gated on cacheMode,
89
+ // never on cacheModeRequested directly, so forcing cacheMode to "broker" on a
90
+ // broker host is sufficient to skip all of them without a second gate.
91
+ for (const guarded of [
92
+ /acquireSuiteBuildSlot\(\{ layout: sharedLayout/,
93
+ /cacheLease = acquireCacheLease\(\{ layout: sharedLayout/,
94
+ /planCachePrune\(\{ snapshot, policy, protectedEntryIds: new Set\(\[sharedLayout\.id\]\) \}\)/,
95
+ /assertWriteVolumeFloors\(\{ volumes, policy,/,
96
+ /markCacheEntrySuccessful\(\{ layout: sharedLayout \}\)/,
97
+ ]) {
98
+ assert.match(source, guarded);
99
+ }
100
+ assert.doesNotMatch(source, /if \(cacheModeRequested === "shared"\) \{\s*\n\s*suiteSlot/);
78
101
  });
79
102
 
80
103
  test("Windows seal and cache identity bind the signing contract, config, and receipts", () => {
@@ -122,16 +145,17 @@ test("CPU sampling degrades to the old behaviour rather than inventing a verdict
122
145
  // bridged src-tauri/target into the release cache, and then lost CARGO_TARGET_DIR
123
146
  // to the broker's unconditional override. The app built into the broker workspace
124
147
  // and packaging failed on an empty cache before notarize/staple.
125
- const managedHostDetected = (env, platform, exists) =>
126
- Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(env, platform, exists));
148
+ const managedHostDetected = (env, platform, exists) => brokerManagedHost(env, platform, exists);
127
149
 
128
150
  test("managed-host gate fires from the managed shim on PATH when no broker socket is exported", () => {
129
- // The build gate is exactly the dual detection cargo-contract.mjs already uses.
151
+ // The build gate is exactly the shared brokerManagedHost predicate cargo-contract.mjs exports,
152
+ // so there is exactly one definition of "is this a broker-managed host" in the codebase.
130
153
  assert.match(
131
154
  source,
132
- /const managedCargoTarget = \(process\.env\.RIGHTKIT_BUILD_BROKER_SOCKET \|\| managedCargoShimOnPath\(process\.env\)\)\n\s*\? resolveManagedCargoTarget\(cargoToml\)\n\s*: null;/,
155
+ /const managedCargoTarget = brokerManagedHost\(process\.env\) \? resolveTargetRoot\(cargoToml\) : null;/,
133
156
  );
134
- assert.match(source, /import \{ managedCargoShimOnPath \} from "\.\/cargo-contract\.mjs";/);
157
+ assert.match(source, /import \{ brokerManagedHost \} from "\.\/cargo-contract\.mjs";/);
158
+ assert.match(source, /import \{ resolveTargetRoot \} from "\.\/cargo-target\.mjs";/);
135
159
 
136
160
  const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
137
161
  const exists = (candidate) => candidate === shim;
@@ -151,6 +175,23 @@ test("managed-host gate fires from the managed shim on PATH when no broker socke
151
175
  assert.equal(managedHostDetected({ PATH: `/usr/local/bin:${path.dirname(shim)}` }, "darwin", (c) => both.has(c)), false);
152
176
  });
153
177
 
178
+ test("brokerManagedHost is the single shared predicate: env var alone, shim alone, both, or neither", () => {
179
+ const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
180
+ const exists = (candidate) => candidate === shim;
181
+
182
+ // Broker socket alone (login shell), no shim resolvable.
183
+ assert.equal(brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: "/usr/bin" }, "darwin", () => false), true);
184
+ // Shim on PATH alone (agent/non-login shell), no socket exported.
185
+ assert.equal(brokerManagedHost({ PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists), true);
186
+ // Both signals present.
187
+ assert.equal(
188
+ brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists),
189
+ true,
190
+ );
191
+ // Neither signal: unmanaged host.
192
+ assert.equal(brokerManagedHost({ PATH: "/usr/bin:/usr/local/bin" }, "darwin", () => false), false);
193
+ });
194
+
154
195
  test("the managed branch hands the Rust target directory to the broker while the unmanaged branch keeps Cache V2", () => {
155
196
  // Managed: CARGO_TARGET_DIR (and the rest of the Cache V2 environment) is deleted
156
197
  // from the child environment, so the broker's own override is the only one.
@@ -164,7 +205,9 @@ test("the managed branch hands the Rust target directory to the broker while the
164
205
  assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\),/);
165
206
  // The managed target is read from crate metadata through the shim, which is what
166
207
  // makes the bridge target equal to the directory the broker actually builds into.
167
- assert.match(source, /function resolveManagedCargoTarget\(manifestPath\)[\s\S]*?JSON\.parse\(output\)\.target_directory/);
208
+ // resolveTargetRoot (cargo-target.mjs) is the single shared implementation —
209
+ // build-release.mjs no longer defines its own copy.
210
+ assert.doesNotMatch(source, /function resolveManagedCargoTarget/);
168
211
  // Unmanaged (managedCargoTarget === null): Cache V2 still supplies CARGO_TARGET_DIR.
169
212
  assert.match(source, /\.\.\.releaseEnvironment\(\{ root: vaultRoot, cacheRoot: sharedCacheRoot,/);
170
213
  });
package/cache-command.mjs CHANGED
@@ -13,12 +13,28 @@ import {
13
13
  resolveSharedCacheRoot,
14
14
  } from "./cache-policy.mjs";
15
15
  import { resolveReleaseLayout } from "./release-state.mjs";
16
+ import { brokerManagedHost } from "./cargo-contract.mjs";
17
+
18
+ // On a broker-managed host, CARGO_TARGET_DIR is not Cache V2's to own: the build
19
+ // broker unconditionally overrides it to its own per-namespace workspace, so the
20
+ // Cache V2 target layout under cacheRoot is legitimately empty (marker files only)
21
+ // while the real ~18 GB of build output lives in the broker's workspace. Inspecting,
22
+ // migrating, or pruning against that layout on a broker host reads real output as
23
+ // missing/corrupt and can recommend destroying it — never phrase this as a problem.
24
+ const BROKER_MANAGED_MESSAGE = "broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 target checks skipped";
25
+ const BROKER_GATED_COMMANDS = new Set(["status", "prune", "migrate"]);
16
26
 
17
27
  export async function runCacheCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, stderr = console.error } = {}) {
18
28
  const [command, ...rest] = args;
19
29
  const json = rest.includes("--json");
20
30
  const platform = platformFrom(env);
21
31
  const cacheRoot = resolveSharedCacheRoot({ platform, env });
32
+ if (BROKER_GATED_COMMANDS.has(command) && brokerManagedHost(env)) {
33
+ const result = { schema: 1, command, cacheRoot, brokerManaged: true, skipped: true, message: BROKER_MANAGED_MESSAGE };
34
+ if (json) stdout(JSON.stringify(result));
35
+ else stdout(BROKER_MANAGED_MESSAGE);
36
+ return result;
37
+ }
22
38
  if (command === "status") {
23
39
  rejectUnknown(rest, new Set(["--json"]));
24
40
  const snapshot = inspectCache({ cacheRoot });
@@ -9,8 +9,24 @@ import { fileURLToPath } from "node:url";
9
9
  const root = path.dirname(fileURLToPath(import.meta.url));
10
10
  const cli = path.join(root, "cli", "right-release.mjs");
11
11
 
12
+ // This suite must exercise the non-broker cache-command path deterministically,
13
+ // regardless of whether the machine running the tests is itself broker-managed
14
+ // (this workspace's own agent shells are: the managed cargo shim sits on PATH).
15
+ // Strip both broker signals from the inherited environment so "unmanaged host"
16
+ // tests stay unmanaged; individual tests opt back into broker signals explicitly.
17
+ function unmanagedEnv(base = process.env) {
18
+ const sanitized = { ...base };
19
+ delete sanitized.RIGHTKIT_BUILD_BROKER_SOCKET;
20
+ const delimiter = process.platform === "win32" ? ";" : ":";
21
+ sanitized.PATH = String(base.PATH ?? "")
22
+ .split(delimiter)
23
+ .filter((entry) => !/(?:^|[/\\])(?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin$/i.test(entry))
24
+ .join(delimiter);
25
+ return sanitized;
26
+ }
27
+
12
28
  function run(args, cacheRoot, env = {}) {
13
- return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_CACHE_ROOT: cacheRoot, ...env } });
29
+ return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...unmanagedEnv(), RIGHT_RELEASE_CACHE_ROOT: cacheRoot, ...env } });
14
30
  }
15
31
 
16
32
  test("cache status has human and schema-stable JSON output", () => {
@@ -73,3 +89,30 @@ test("cache migrate uses the repository vault Cargo home, not an app-local looka
73
89
  assert.equal(readFileSync(path.join(cacheRoot, "cargo-home", "registry"), "utf8"), "vault");
74
90
  assert.equal(readFileSync(path.join(appCargoHome, "registry"), "utf8"), "app-local");
75
91
  });
92
+
93
+ test("status, prune, and migrate short-circuit cleanly on a broker-managed host without proposing a repair", () => {
94
+ const cacheRoot = mkdtempSync(path.join(os.tmpdir(), "rightkit-cache-command-"));
95
+ const brokerEnv = { RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" };
96
+
97
+ const status = run(["status", "--json"], cacheRoot, brokerEnv);
98
+ assert.equal(status.status, 0, status.stderr);
99
+ const statusBody = JSON.parse(status.stdout);
100
+ assert.equal(statusBody.brokerManaged, true);
101
+ assert.match(statusBody.message, /broker-managed host/);
102
+ assert.doesNotMatch(status.stdout + status.stderr, /warn|repair|corrupt/i);
103
+
104
+ const prune = run(["prune", "--dry-run", "--json"], cacheRoot, brokerEnv);
105
+ assert.equal(prune.status, 0, prune.stderr);
106
+ assert.equal(JSON.parse(prune.stdout).brokerManaged, true);
107
+
108
+ // migrate short-circuits before even requiring --config, since a broker host has
109
+ // nothing under Cache V2's target layout for it to migrate into.
110
+ const migrate = run(["migrate", "--json"], cacheRoot, brokerEnv);
111
+ assert.equal(migrate.status, 0, migrate.stderr);
112
+ assert.equal(JSON.parse(migrate.stdout).brokerManaged, true);
113
+
114
+ // Non-broker host: unchanged behaviour.
115
+ const unmanaged = run(["status", "--json"], cacheRoot);
116
+ assert.equal(unmanaged.status, 0, unmanaged.stderr);
117
+ assert.equal(JSON.parse(unmanaged.stdout).brokerManaged, undefined);
118
+ });
package/cache-policy.mjs CHANGED
@@ -30,21 +30,59 @@ export const CACHE_LOCK_ORDER = ["release", "suite-build-slot", "gc", "entry-lea
30
30
 
31
31
  const MARKER = ".rightkit-cache-entry.json";
32
32
 
33
- export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir(), xdgCacheHome } = {}) {
34
- const override = env.RIGHT_RELEASE_CACHE_ROOT;
35
- if (override) {
36
- if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
37
- return resolveForPlatform(override, platform);
38
- }
33
+ export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
39
34
  if (platform === "darwin" || platform === "mac") {
35
+ const override = env.RIGHT_RELEASE_CACHE_ROOT;
36
+ if (override) {
37
+ if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
38
+ return resolveForPlatform(override, platform);
39
+ }
40
40
  throw new Error("RIGHT_RELEASE_CACHE_ROOT is required on macOS; set it to external storage");
41
41
  }
42
+ return resolveStableRoot({
43
+ platform,
44
+ env,
45
+ home,
46
+ explicitOverride: env.RIGHT_RELEASE_CACHE_ROOT,
47
+ overrideName: "RIGHT_RELEASE_CACHE_ROOT",
48
+ win32EnvVar: "LOCALAPPDATA",
49
+ win32Segments: ["RightSuite", "Cache", "release"],
50
+ otherSegments: ["rightsuite", "release"],
51
+ });
52
+ }
53
+
54
+ // resolveStableRoot centralizes platform-native root derivation for both cache roots
55
+ // (waste-tolerant, may split) and lock roots (must never split, or mutual exclusion
56
+ // silently breaks). Route every such root through this one primitive.
57
+ export function resolveStableRoot({
58
+ platform = process.platform,
59
+ env = process.env,
60
+ home = os.homedir(),
61
+ explicitOverride,
62
+ overrideName,
63
+ darwinSegments,
64
+ win32EnvVar = "LOCALAPPDATA",
65
+ win32Segments,
66
+ otherSegments,
67
+ otherBaseSegments = [".cache"],
68
+ } = {}) {
69
+ if (explicitOverride) {
70
+ if (!isAbsoluteForPlatform(explicitOverride, platform)) throw new Error(`${overrideName} must be an absolute path`);
71
+ return resolveForPlatform(explicitOverride, platform);
72
+ }
42
73
  if (platform === "win32" || platform === "win") {
43
- const local = env.LOCALAPPDATA;
44
- if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error("LOCALAPPDATA must be an absolute path for the RightSuite cache");
45
- return path.win32.resolve(local, "RightSuite", "Cache", "release");
74
+ const local = env[win32EnvVar];
75
+ if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error(`${win32EnvVar} must be an absolute path for RightSuite`);
76
+ return path.win32.resolve(local, ...win32Segments);
77
+ }
78
+ if (platform === "darwin" || platform === "mac") {
79
+ return path.posix.join(home, ...darwinSegments);
46
80
  }
47
- return path.posix.resolve(xdgCacheHome || env.XDG_CACHE_HOME || path.posix.join(home, ".cache"), "rightsuite", "release");
81
+ // Never read a shell-profile-scoped variable (XDG_CACHE_HOME, XDG_CONFIG_HOME,
82
+ // npm_config_cache, etc.) as an implicit default here: those differ between login
83
+ // and non-login shells, so the same process can resolve to two different roots
84
+ // depending on how it was launched. Always derive from the OS home directory instead.
85
+ return path.posix.resolve(path.posix.join(home, ...otherBaseSegments), ...otherSegments);
48
86
  }
49
87
 
50
88
  export function resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint, kind = "release" } = {}) {
@@ -27,6 +27,7 @@ import {
27
27
  resolveCacheLayout,
28
28
  resolveSharedCacheIdentity,
29
29
  resolveSharedCacheRoot,
30
+ resolveStableRoot,
30
31
  } from "./cache-policy.mjs";
31
32
 
32
33
  function root() { return path.join(os.tmpdir(), `rightkit-cache-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`); }
@@ -61,11 +62,52 @@ function migrationFixture() {
61
62
  test("cache roots are platform-native and overrides must be absolute", () => {
62
63
  assert.throws(() => resolveSharedCacheRoot({ platform: "mac", home: "/Users/test", env: {} }), /RIGHT_RELEASE_CACHE_ROOT is required/);
63
64
  assert.equal(resolveSharedCacheRoot({ platform: "win", env: { LOCALAPPDATA: "C:/Users/test/AppData/Local" } }), "C:\\Users\\test\\AppData\\Local\\RightSuite\\Cache\\release");
64
- assert.equal(resolveSharedCacheRoot({ platform: "linux", home: "/home/test", xdgCacheHome: "/tmp/xdg", env: {} }), "/tmp/xdg/rightsuite/release");
65
+ assert.equal(resolveSharedCacheRoot({ platform: "linux", home: "/home/test", env: {} }), "/home/test/.cache/rightsuite/release");
65
66
  assert.equal(resolveSharedCacheRoot({ platform: "mac", env: { RIGHT_RELEASE_CACHE_ROOT: "/tmp/cache" } }), "/tmp/cache");
66
67
  assert.throws(() => resolveSharedCacheRoot({ platform: "mac", env: { RIGHT_RELEASE_CACHE_ROOT: "relative" } }), /absolute/i);
67
68
  });
68
69
 
70
+ test("shared cache root ignores XDG_CACHE_HOME regardless of value", () => {
71
+ assert.equal(
72
+ resolveSharedCacheRoot({ platform: "linux", env: { XDG_CACHE_HOME: "/one/path" }, home: "/home/u" }),
73
+ resolveSharedCacheRoot({ platform: "linux", env: {}, home: "/home/u" }),
74
+ );
75
+ assert.equal(resolveSharedCacheRoot({ platform: "linux", env: { XDG_CACHE_HOME: "/one/path" }, home: "/home/u" }), "/home/u/.cache/rightsuite/release");
76
+ // resolveSharedCacheRoot no longer accepts an xdgCacheHome option; passing one has no effect.
77
+ assert.equal(
78
+ resolveSharedCacheRoot({ platform: "linux", env: {}, home: "/home/u", xdgCacheHome: "/should/be/ignored" }),
79
+ "/home/u/.cache/rightsuite/release",
80
+ );
81
+ });
82
+
83
+ test("resolveStableRoot: explicit override wins, win32 requires the env var, other platforms ignore arbitrary env", () => {
84
+ const opts = {
85
+ darwinSegments: ["Library", "Caches", "Example"],
86
+ win32Segments: ["Example"],
87
+ otherSegments: ["example"],
88
+ };
89
+ assert.equal(
90
+ resolveStableRoot({ ...opts, platform: "linux", explicitOverride: "/explicit/path", overrideName: "EXAMPLE_ROOT", env: { EXAMPLE_ROOT: "ignored-by-caller-arg" } }),
91
+ "/explicit/path",
92
+ );
93
+ assert.throws(
94
+ () => resolveStableRoot({ ...opts, platform: "linux", explicitOverride: "relative/path", overrideName: "EXAMPLE_ROOT" }),
95
+ /EXAMPLE_ROOT must be an absolute path/,
96
+ );
97
+ assert.throws(
98
+ () => resolveStableRoot({ ...opts, platform: "win32", env: {} }),
99
+ /LOCALAPPDATA must be an absolute path for RightSuite/,
100
+ );
101
+ assert.equal(
102
+ resolveStableRoot({ ...opts, platform: "win32", env: { LOCALAPPDATA: "C:/Users/test/AppData/Local" } }),
103
+ "C:\\Users\\test\\AppData\\Local\\Example",
104
+ );
105
+ assert.equal(
106
+ resolveStableRoot({ ...opts, platform: "linux", env: { XDG_CACHE_HOME: "/ignored", npm_config_cache: "/ignored" }, home: "/home/u" }),
107
+ "/home/u/.cache/example",
108
+ );
109
+ });
110
+
69
111
  test("layout isolates app, platform, kind, and fingerprint", () => {
70
112
  const base = root();
71
113
  const release = layout(base);
@@ -27,10 +27,14 @@ export function managedCargoShimOnPath(env = process.env, platform = process.pla
27
27
  return false;
28
28
  }
29
29
 
30
+ export function brokerManagedHost(env = process.env, platform = process.platform, exists = existsSync) {
31
+ return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET) || managedCargoShimOnPath(env, platform, exists);
32
+ }
33
+
30
34
  export function isolatedCargoMetadataEnv(cargoHome, env = process.env, platform = process.platform, exists = existsSync) {
31
35
  const metadataEnv = { ...env };
32
36
  for (const name of CACHE_V2_ENVIRONMENT) delete metadataEnv[name];
33
- if (metadataEnv.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(metadataEnv, platform, exists)) return metadataEnv;
37
+ if (brokerManagedHost(metadataEnv, platform, exists)) return metadataEnv;
34
38
  return { ...metadataEnv, CARGO_HOME: cargoHome };
35
39
  }
36
40
 
package/cargo-guard.mjs CHANGED
@@ -6,7 +6,8 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
- import { resolveSharedCacheRoot } from "./cache-policy.mjs";
9
+ import { resolveSharedCacheRoot, resolveStableRoot } from "./cache-policy.mjs";
10
+ import { brokerManagedHost } from "./cargo-contract.mjs";
10
11
  import { runHeavyCommand } from "./heavy-command.mjs";
11
12
 
12
13
  const LIGHT_COMMANDS = new Set(["fmt", "metadata", "tree", "fetch", "search", "locate-project", "read-manifest", "help", "version", "--version", "-V"]);
@@ -34,7 +35,14 @@ export function computePolicyPath({ env = process.env, platform = process.platfo
34
35
  return path.win32.join(env.LOCALAPPDATA || path.win32.join(home, "AppData", "Local"), "RightSuite", "compute-policy.json");
35
36
  }
36
37
  if (platform === "darwin" || platform === "mac") return path.join(home, "Library", "Application Support", "RightSuite", "compute-policy.json");
37
- return path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "rightsuite", "compute-policy.json");
38
+ const base = resolveStableRoot({
39
+ platform,
40
+ env,
41
+ home,
42
+ otherBaseSegments: [".config"],
43
+ otherSegments: ["rightsuite"],
44
+ });
45
+ return path.join(base, "compute-policy.json");
38
46
  }
39
47
 
40
48
  export function loadComputePolicy(options = {}) {
@@ -164,10 +172,18 @@ export function cargoCacheEnvironment(args, {
164
172
  const targetOption = args.find((value) => value.startsWith("--target-dir="));
165
173
  const targetIndex = args.findIndex((value) => value === "--target-dir");
166
174
  const explicitTarget = targetOption?.slice("--target-dir=".length) ?? (targetIndex >= 0 ? args[targetIndex + 1] : null);
167
- if (explicitTarget) assertInsideCache(explicitTarget, cacheRoot, "--target-dir", { cwd, platform });
168
- const targetDir = env.CARGO_TARGET_DIR
169
- ? assertInsideCache(env.CARGO_TARGET_DIR, cacheRoot, "CARGO_TARGET_DIR", { cwd, platform })
170
- : defaultTarget;
175
+ // On a broker-managed host, CARGO_TARGET_DIR is not this cache's to police: the
176
+ // build broker unconditionally overrides it to its own per-namespace workspace
177
+ // outside the shared cache root, so asserting it stays inside cacheRoot is a
178
+ // spurious failure, not a real violation. Pass the broker's own value through
179
+ // untouched instead of rejecting it.
180
+ const broker = brokerManagedHost(env, platform, exists);
181
+ if (explicitTarget && !broker) assertInsideCache(explicitTarget, cacheRoot, "--target-dir", { cwd, platform });
182
+ const targetDir = broker
183
+ ? (env.CARGO_TARGET_DIR ?? explicitTarget ?? defaultTarget)
184
+ : env.CARGO_TARGET_DIR
185
+ ? assertInsideCache(env.CARGO_TARGET_DIR, cacheRoot, "CARGO_TARGET_DIR", { cwd, platform })
186
+ : defaultTarget;
171
187
  const sccacheDir = env.SCCACHE_DIR
172
188
  ? assertInsideCache(env.SCCACHE_DIR, cacheRoot, "SCCACHE_DIR", { cwd, platform })
173
189
  : defaultSccache;
@@ -2,7 +2,15 @@ import assert from "node:assert/strict";
2
2
  import path from "node:path";
3
3
  import test from "node:test";
4
4
 
5
- import { assertComputePolicyAllowsCargo, cargoCacheEnvironment, cargoProjectRoot, cargoSubcommand, checkHookInput, resolveRealCargo, resolveRealRustc, runCargoGuard, shellRunsCargoTest, shouldGuardCargo } from "./cargo-guard.mjs";
5
+ import { assertComputePolicyAllowsCargo, cargoCacheEnvironment, cargoProjectRoot, cargoSubcommand, checkHookInput, computePolicyPath, resolveRealCargo, resolveRealRustc, runCargoGuard, shellRunsCargoTest, shouldGuardCargo } from "./cargo-guard.mjs";
6
+
7
+ test("compute policy path ignores XDG_CONFIG_HOME regardless of value", () => {
8
+ assert.equal(
9
+ computePolicyPath({ platform: "linux", env: { XDG_CONFIG_HOME: "/one/path" }, home: "/home/u" }),
10
+ computePolicyPath({ platform: "linux", env: {}, home: "/home/u" }),
11
+ );
12
+ assert.equal(computePolicyPath({ platform: "linux", env: {}, home: "/home/u" }), path.join("/home/u/.config/rightsuite", "compute-policy.json"));
13
+ });
6
14
 
7
15
  test("Cargo guard serializes compiling commands but bypasses inspection and formatting", () => {
8
16
  for (const args of [["build"], ["test"], ["check"], ["clippy"], ["nextest", "run"], ["clean"], ["+stable", "bench"]]) {
@@ -122,6 +130,30 @@ test("Cargo guard rejects cache bypasses and permits RightKit-owned cache paths"
122
130
  assert.equal(env.CARGO_TARGET_DIR, "/cache/test-targets/app");
123
131
  });
124
132
 
133
+ test("Cargo guard passes a broker-owned CARGO_TARGET_DIR through instead of rejecting it", () => {
134
+ const base = { cwd: "/repo", platform: "mac", exists: () => false };
135
+ // Outside the shared cache root, which would be rejected on a non-broker host
136
+ // (see the previous test) — but the broker owns this value, not Cache V2.
137
+ const env = cargoCacheEnvironment(["test"], {
138
+ ...base,
139
+ env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", CARGO_TARGET_DIR: "/broker/workspace/target" },
140
+ });
141
+ assert.equal(env.CARGO_TARGET_DIR, "/broker/workspace/target");
142
+
143
+ // Same for an explicit --target-dir outside the cache root.
144
+ const explicit = cargoCacheEnvironment(["test", "--target-dir", "/broker/workspace/target"], {
145
+ ...base,
146
+ env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" },
147
+ });
148
+ assert.doesNotThrow(() => explicit);
149
+
150
+ // Non-broker host: unchanged, still rejects the cache bypass.
151
+ assert.throws(
152
+ () => cargoCacheEnvironment(["test"], { ...base, env: { RIGHT_RELEASE_CACHE_ROOT: "/cache", CARGO_TARGET_DIR: "/tmp/target" } }),
153
+ /CARGO_TARGET_DIR must stay inside/,
154
+ );
155
+ });
156
+
125
157
  test("Cargo guard derives native Windows cache paths", () => {
126
158
  const env = cargoCacheEnvironment(["build"], {
127
159
  cwd: "D:\\Claude\\citadel", platform: "win", exists: (file) => file === "D:\\Claude\\citadel\\Cargo.toml",
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // Resolves the Cargo target directory for a manifest by asking Cargo itself.
3
+ //
4
+ // `cargo metadata` is authoritative on BOTH broker-managed and unmanaged
5
+ // hosts: through a managed shim it reports the broker's own workspace target
6
+ // directory; without one it reports the local `target/` directory. LOCATING
7
+ // the target directory therefore never needs broker detection — the metadata
8
+ // command already reflects whichever host it runs on. `brokerManagedHost()`
9
+ // (see cargo-contract.mjs) remains a separate, cache-policy-only decision:
10
+ // whether Cache V2's lease/slot/prune/floor machinery should run at all, not
11
+ // where the target directory lives.
12
+ import { execFileSync } from "node:child_process";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ export function resolveTargetRoot(manifestPath) {
17
+ if (!manifestPath) throw new Error("resolveTargetRoot requires a Cargo.toml manifest path");
18
+ let output;
19
+ try {
20
+ output = execFileSync(
21
+ "cargo",
22
+ ["metadata", "--offline", "--format-version", "1", "--no-deps", "--manifest-path", manifestPath],
23
+ { cwd: path.dirname(manifestPath), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
24
+ );
25
+ } catch (error) {
26
+ throw new Error(`metadata failed for ${manifestPath}: ${error.message}`);
27
+ }
28
+ const { target_directory: directory } = JSON.parse(output);
29
+ if (typeof directory !== "string" || directory.length === 0 || !path.isAbsolute(directory)) {
30
+ throw new Error(`metadata for ${manifestPath} did not report an absolute target_directory`);
31
+ }
32
+ return path.resolve(directory);
33
+ }
34
+
35
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
36
+ const manifestPath = process.argv[2];
37
+ if (!manifestPath) {
38
+ console.error("usage: cargo-target.mjs <path-to-Cargo.toml>");
39
+ process.exit(1);
40
+ }
41
+ try {
42
+ process.stdout.write(`${resolveTargetRoot(manifestPath)}\n`);
43
+ } catch (error) {
44
+ console.error(error.message);
45
+ process.exit(1);
46
+ }
47
+ }
@@ -0,0 +1,133 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmodSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import { resolveTargetRoot } from "./cargo-target.mjs";
10
+
11
+ const here = path.dirname(fileURLToPath(import.meta.url));
12
+ const cli = path.join(here, "cargo-target.mjs");
13
+
14
+ // A fake `cargo` on PATH, ahead of the real one, so metadata success/failure
15
+ // shapes are deterministic and don't depend on the host's actual crate graph.
16
+ // It branches on a marker embedded in the manifest path so each test controls
17
+ // its own outcome independently.
18
+ function fakeCargoBin() {
19
+ const bin = mkdtempSync(path.join(os.tmpdir(), "rightkit-fake-cargo-"));
20
+ const script = path.join(bin, process.platform === "win32" ? "cargo.cmd" : "cargo");
21
+ const body = process.platform === "win32"
22
+ ? [
23
+ "@echo off",
24
+ "setlocal enabledelayedexpansion",
25
+ "set ARGS=%*",
26
+ "echo %ARGS% | findstr /C:\"fail-manifest\" >nul && (echo boom 1>&2 & exit /b 1)",
27
+ "echo %ARGS% | findstr /C:\"bad-target-manifest\" >nul && (echo {\"target_directory\":\"relative/target\"} & exit /b 0)",
28
+ "echo {\"target_directory\":\"" + path.join(bin, "target").replaceAll("\\", "\\\\") + "\"}",
29
+ ].join("\r\n")
30
+ : [
31
+ "#!/bin/sh",
32
+ 'case "$*" in',
33
+ ' *fail-manifest*) echo boom 1>&2; exit 1 ;;',
34
+ ' *bad-target-manifest*) echo \'{"target_directory":"relative/target"}\'; exit 0 ;;',
35
+ ` *) echo '{"target_directory":"${path.join(bin, "target")}"}'; exit 0 ;;`,
36
+ "esac",
37
+ ].join("\n");
38
+ writeFileSync(script, body);
39
+ if (process.platform !== "win32") chmodSync(script, 0o755);
40
+ return bin;
41
+ }
42
+
43
+ function withFakeCargo(fn) {
44
+ const bin = fakeCargoBin();
45
+ const env = { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH ?? ""}` };
46
+ return fn({ bin, env });
47
+ }
48
+
49
+ test("resolveTargetRoot returns the absolute target_directory Cargo metadata reports", () => {
50
+ withFakeCargo(({ bin, env }) => {
51
+ const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
52
+ const manifest = path.join(manifestDir, "Cargo.toml");
53
+ writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
54
+ const previousPath = process.env.PATH;
55
+ process.env.PATH = env.PATH;
56
+ try {
57
+ const result = resolveTargetRoot(manifest);
58
+ assert.equal(result, path.resolve(path.join(bin, "target")));
59
+ } finally {
60
+ process.env.PATH = previousPath;
61
+ }
62
+ });
63
+ });
64
+
65
+ test("resolveTargetRoot fails closed, naming the manifest, when target_directory is not absolute", () => {
66
+ withFakeCargo(({ env }) => {
67
+ const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
68
+ const manifest = path.join(manifestDir, "bad-target-manifest-Cargo.toml");
69
+ writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
70
+ const previousPath = process.env.PATH;
71
+ process.env.PATH = env.PATH;
72
+ try {
73
+ assert.throws(
74
+ () => resolveTargetRoot(manifest),
75
+ new RegExp(`metadata for .*bad-target-manifest-Cargo\\.toml did not report an absolute target_directory`),
76
+ );
77
+ } finally {
78
+ process.env.PATH = previousPath;
79
+ }
80
+ });
81
+ });
82
+
83
+ test("resolveTargetRoot fails closed, naming the manifest, when the metadata command exits non-zero", () => {
84
+ withFakeCargo(({ env }) => {
85
+ const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
86
+ const manifest = path.join(manifestDir, "fail-manifest-Cargo.toml");
87
+ writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
88
+ const previousPath = process.env.PATH;
89
+ process.env.PATH = env.PATH;
90
+ try {
91
+ assert.throws(
92
+ () => resolveTargetRoot(manifest),
93
+ new RegExp(`metadata failed for .*fail-manifest-Cargo\\.toml`),
94
+ );
95
+ } finally {
96
+ process.env.PATH = previousPath;
97
+ }
98
+ });
99
+ });
100
+
101
+ test("resolveTargetRoot throws when no manifest path is given", () => {
102
+ assert.throws(() => resolveTargetRoot(), /resolveTargetRoot requires a Cargo.toml manifest path/);
103
+ assert.throws(() => resolveTargetRoot(""), /resolveTargetRoot requires a Cargo.toml manifest path/);
104
+ });
105
+
106
+ test("CLI mode writes only the resolved path to stdout on success", () => {
107
+ withFakeCargo(({ bin, env }) => {
108
+ const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
109
+ const manifest = path.join(manifestDir, "Cargo.toml");
110
+ writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
111
+ const result = spawnSync(process.execPath, [cli, manifest], { encoding: "utf8", env });
112
+ assert.equal(result.status, 0, result.stderr);
113
+ assert.equal(result.stdout.trim(), path.resolve(path.join(bin, "target")));
114
+ });
115
+ });
116
+
117
+ test("CLI mode writes a message to stderr and exits 1 on failure", () => {
118
+ withFakeCargo(({ env }) => {
119
+ const manifestDir = mkdtempSync(path.join(os.tmpdir(), "rightkit-cargo-target-fixture-"));
120
+ const manifest = path.join(manifestDir, "fail-manifest-Cargo.toml");
121
+ writeFileSync(manifest, "[package]\nname = \"fixture\"\nversion = \"0.0.0\"\n");
122
+ const result = spawnSync(process.execPath, [cli, manifest], { encoding: "utf8", env });
123
+ assert.equal(result.status, 1);
124
+ assert.equal(result.stdout, "");
125
+ assert.match(result.stderr, /metadata failed for .*fail-manifest-Cargo\.toml/);
126
+ });
127
+ });
128
+
129
+ test("CLI mode requires a manifest argument", () => {
130
+ const result = spawnSync(process.execPath, [cli], { encoding: "utf8" });
131
+ assert.equal(result.status, 1);
132
+ assert.match(result.stderr, /usage: cargo-target\.mjs/);
133
+ });
@@ -20,6 +20,7 @@ const commands = new Map([
20
20
  ["create-mac-updater", "create-mac-updater.mjs"],
21
21
  ["mirror-root-artifact", "mirror-root-artifact.mjs"],
22
22
  ["github", "github-release.mjs"],
23
+ ["cargo-target", "cargo-target.mjs"],
23
24
  ]);
24
25
 
25
26
  const args = process.argv.slice(2);
package/heavy-command.mjs CHANGED
@@ -6,17 +6,23 @@ import { spawn, spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { randomUUID } from "node:crypto";
8
8
 
9
+ import { resolveStableRoot } from "./cache-policy.mjs";
10
+
9
11
  const GB = 1024 ** 3;
10
12
  const SELF = fileURLToPath(import.meta.url);
11
13
 
12
14
  export function heavyWorkRoot({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
13
- if (env.RIGHTSUITE_HEAVY_WORK_ROOT) return path.resolve(env.RIGHTSUITE_HEAVY_WORK_ROOT);
14
- if (platform === "win32" || platform === "win") {
15
- if (!env.LOCALAPPDATA) throw new Error("LOCALAPPDATA is required for the RightSuite heavy-work guard");
16
- return path.win32.resolve(env.LOCALAPPDATA, "RightSuite", "heavy-work");
17
- }
18
- if (platform === "darwin" || platform === "mac") return path.posix.join(home, "Library", "Caches", "RightSuite", "heavy-work");
19
- return path.posix.resolve(env.XDG_CACHE_HOME || path.posix.join(home, ".cache"), "rightsuite", "heavy-work");
15
+ return resolveStableRoot({
16
+ platform,
17
+ env,
18
+ home,
19
+ explicitOverride: env.RIGHTSUITE_HEAVY_WORK_ROOT,
20
+ overrideName: "RIGHTSUITE_HEAVY_WORK_ROOT",
21
+ darwinSegments: ["Library", "Caches", "RightSuite", "heavy-work"],
22
+ win32EnvVar: "LOCALAPPDATA",
23
+ win32Segments: ["RightSuite", "heavy-work"],
24
+ otherSegments: ["rightsuite", "heavy-work"],
25
+ });
20
26
  }
21
27
 
22
28
  export function heavyCommandEnvironment(env = process.env) {
@@ -26,6 +26,14 @@ test("heavy-work root is machine-wide rather than repository-local", () => {
26
26
  assert.equal(heavyWorkRoot({ platform: "win", env: { LOCALAPPDATA: "C:\\Users\\test\\AppData\\Local" } }), "C:\\Users\\test\\AppData\\Local\\RightSuite\\heavy-work");
27
27
  });
28
28
 
29
+ test("heavy-work root ignores XDG_CACHE_HOME regardless of value", () => {
30
+ assert.equal(
31
+ heavyWorkRoot({ platform: "linux", env: { XDG_CACHE_HOME: "/one/path" }, home: "/home/u" }),
32
+ heavyWorkRoot({ platform: "linux", env: {}, home: "/home/u" }),
33
+ );
34
+ assert.equal(heavyWorkRoot({ platform: "linux", env: {}, home: "/home/u" }), "/home/u/.cache/rightsuite/heavy-work");
35
+ });
36
+
29
37
  test("heavy commands default compiler and test concurrency to two", () => {
30
38
  const env = heavyCommandEnvironment({});
31
39
  assert.equal(env.CARGO_BUILD_JOBS, "2");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.65",
3
+ "version": "0.2.66",
4
4
  "description": "Portable Right Suite release CLI/SDK: native-host signed installers, updater artifacts, hardening, immutable GitHub Release upload, and add-on adoption.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "type": "module",
package/preflight.mjs CHANGED
@@ -21,6 +21,7 @@
21
21
  import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
22
22
  import path from "node:path";
23
23
  import { spawnSync } from "node:child_process";
24
+ import { brokerManagedHost } from "./cargo-contract.mjs";
24
25
 
25
26
  export const PREFLIGHT_OK = "ok";
26
27
  export const PREFLIGHT_WARN = "warn";
@@ -186,11 +187,18 @@ export function collectPreflight({
186
187
  checks.push(
187
188
  !entry || entry.isSymbolicLink()
188
189
  ? ok("target-bridge", entry ? "src-tauri/target is a symbolic link" : "src-tauri/target is ready for the shared cache bridge")
189
- : fail(
190
- "target-bridge",
191
- `${target} is a real directory; bridge setup will refuse it`,
192
- `run right-release cache migrate --config ${JSON.stringify(configPath ?? path.join(appRoot, "right-release.config.mjs"))} --dry-run, then repeat with --apply before building`,
193
- ),
190
+ // A real directory here is not corruption on a broker-managed host: the
191
+ // build broker owns CARGO_TARGET_DIR and this is its own output (or a
192
+ // leftover from before it took ownership), not Cache V2's bridge target.
193
+ // Recommending `cache migrate` here is how an agent nearly renamed ~5.9 GB
194
+ // of warm broker intermediates out from under it.
195
+ : brokerManagedHost(env)
196
+ ? ok("target-bridge", `${target} is a real directory; broker-managed host, left untouched`)
197
+ : fail(
198
+ "target-bridge",
199
+ `${target} is a real directory; bridge setup will refuse it`,
200
+ `run right-release cache migrate --config ${JSON.stringify(configPath ?? path.join(appRoot, "right-release.config.mjs"))} --dry-run, then repeat with --apply before building`,
201
+ ),
194
202
  );
195
203
  }
196
204
 
package/release-state.mjs CHANGED
@@ -82,7 +82,7 @@ function watchDirectoryTree(root, onProgress, watchers, fallback, { watchFactory
82
82
 
83
83
  export function releaseEnvironment({ root, cacheRoot, platform, architecture, app, cacheKey, kind = "release", appRoot, mode = "legacy", env = process.env }) {
84
84
  if (kind !== "release" && kind !== "test") throw new Error(`invalid target kind: ${kind}`);
85
- if (mode !== "legacy" && mode !== "shared") throw new Error(`invalid cache mode: ${mode}`);
85
+ if (mode !== "legacy" && mode !== "shared" && mode !== "broker") throw new Error(`invalid cache mode: ${mode}`);
86
86
  if (mode === "shared") {
87
87
  if (!cacheRoot || !platform || !architecture || !app || !cacheKey || !appRoot) throw new Error("shared release environment requires cacheRoot, platform, architecture, app, cacheKey, and appRoot");
88
88
  const layout = resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint: cacheKey, kind });
package/release.test.mjs CHANGED
@@ -134,21 +134,39 @@ function lockFixture() {
134
134
  return { dir, config };
135
135
  }
136
136
 
137
+ // This suite must exercise the non-broker path deterministically regardless of
138
+ // whether the machine running the tests is itself broker-managed (this
139
+ // workspace's own agent shells are: the managed cargo shim sits on PATH).
140
+ // Strip both broker signals from the inherited environment before spawning.
141
+ function unmanagedEnv() {
142
+ const sanitized = { ...process.env };
143
+ delete sanitized.RIGHTKIT_BUILD_BROKER_SOCKET;
144
+ const delimiter = process.platform === "win32" ? ";" : ":";
145
+ sanitized.PATH = String(process.env.PATH ?? "")
146
+ .split(delimiter)
147
+ .filter((entry) => !/(?:^|[/\\])(?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin$/i.test(entry))
148
+ .join(delimiter);
149
+ return sanitized;
150
+ }
151
+
137
152
  function run(config, ...args) {
138
153
  return spawnSync(process.execPath, [release, "--config", config, "--platform", "win", "--dry-run", ...args], {
139
154
  encoding: "utf8",
155
+ env: unmanagedEnv(),
140
156
  });
141
157
  }
142
158
 
143
159
  function runRaw(config, ...args) {
144
160
  return spawnSync(process.execPath, [release, "--config", config, ...args], {
145
161
  encoding: "utf8",
162
+ env: unmanagedEnv(),
146
163
  });
147
164
  }
148
165
 
149
166
  function runDoctor(config, ...args) {
150
167
  return spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform, ...args], {
151
168
  encoding: "utf8",
169
+ env: unmanagedEnv(),
152
170
  });
153
171
  }
154
172
 
@@ -284,6 +302,18 @@ test("doctor rejects a real src-tauri target before release work", () => {
284
302
  assert.match(`${result.stdout}\n${result.stderr}`, /target-bridge[\s\S]*real directory[\s\S]*cache migrate/i);
285
303
  });
286
304
 
305
+ test("doctor accepts a real src-tauri target on a broker-managed host instead of proposing cache migrate", () => {
306
+ const config = fixture({ platform: hostPlatform });
307
+ mkdirSync(path.join(path.dirname(config), "src-tauri", "target"));
308
+ const result = spawnSync(process.execPath, [cli, "doctor", "--config", config, "--platform", hostPlatform], {
309
+ encoding: "utf8",
310
+ env: { ...unmanagedEnv(), RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" },
311
+ });
312
+ assert.equal(result.status, 0, result.stderr);
313
+ assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /cache migrate/i);
314
+ assert.match(result.stdout, /target-bridge[\s\S]*broker-managed host, left untouched/i);
315
+ });
316
+
287
317
  test("doctor permits unrelated dirt", () => {
288
318
  const config = fixture({ platform: hostPlatform });
289
319
  const root = path.dirname(config);
@@ -47,6 +47,24 @@ const macOnlyApps = [
47
47
  { key: "screenright", root: "tools/screenright", releaseFiles: ["scripts/release-package.mjs"] },
48
48
  ];
49
49
 
50
+ // Consuming suite apps whose packaging/build scripts must resolve Cargo build
51
+ // output from `cargo metadata`'s `target_directory`, never by reading
52
+ // CARGO_TARGET_DIR (or a shell/PowerShell equivalent) directly to locate it.
53
+ // This defect recurred three times across the suite (broker-managed hosts own
54
+ // CARGO_TARGET_DIR, so a stale/wrong value silently reads the wrong tree).
55
+ // rightsites and tools/screenright are intentionally out of scope here.
56
+ const cargoTargetDirGuardApps = [
57
+ "coderight/apps/coderight-tauri",
58
+ "cutright/apps/studio",
59
+ "genright",
60
+ "heardright/tauri-app-next",
61
+ "mailright",
62
+ "membrane/apps/membrane-hub",
63
+ "orthic",
64
+ "scraperight",
65
+ "viewright",
66
+ ];
67
+
50
68
  function assertReleasePackageScripts(scripts, label) {
51
69
  for (const platform of ["mac", "win"]) {
52
70
  assert.equal(scripts[`release:build:${platform}`], `right-release build --platform ${platform}`, `${label} must use the tier-neutral ${platform} build entry point`);
@@ -468,12 +486,12 @@ test("RightKit exposes one current version manifest", () => {
468
486
  "@rightkit/legal": "0.3.0",
469
487
  "@rightkit/legal-ui": "0.1.1",
470
488
  "@rightkit/license": "0.1.6",
471
- "@rightkit/release": "0.2.65",
489
+ "@rightkit/release": "0.2.66",
472
490
  "@rightkit/qa": "0.2.0",
473
491
  });
474
492
  assert.deepEqual(versions.legacyNpm, {
475
493
  "@rightkit/legal-ui": ["0.1.0"],
476
- "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64"],
494
+ "@rightkit/release": ["0.2.22", "0.2.29", "0.2.30", "0.2.31", "0.2.41", "0.2.42", "0.2.43", "0.2.44", "0.2.45", "0.2.46", "0.2.49", "0.2.50", "0.2.51", "0.2.53", "0.2.54", "0.2.55", "0.2.56", "0.2.61", "0.2.62", "0.2.63", "0.2.64", "0.2.65"],
477
495
  "@rightkit/qa": ["0.1.0"],
478
496
  });
479
497
  assert.ok(
@@ -769,6 +787,113 @@ function readCanonicalCrateVersions() {
769
787
  );
770
788
  }
771
789
 
790
+ const CARGO_TARGET_DIR_GUARD_EXTENSIONS = new Set([".mjs", ".js", ".ts", ".sh", ".ps1"]);
791
+ const CARGO_TARGET_DIR_GUARD_ALLOW_MARKER = /rightkit-allow-cargo-target-dir:\s*\S/;
792
+ // Test/contract files assert *about* the CARGO_TARGET_DIR pattern (e.g. inside
793
+ // a regex literal passed to assert.match/assert.doesNotMatch) rather than
794
+ // locating build output with it; they are not packaging/build scripts.
795
+ const CARGO_TARGET_DIR_GUARD_SKIP_BASENAME = /(?:^test-.*|\.test)\.(?:mjs|js|ts)$/;
796
+
797
+ function cargoTargetDirGuardPatternFor(extension) {
798
+ if (extension === ".ps1") return /\$env:CARGO_TARGET_DIR\b/;
799
+ if (extension === ".sh") return /\$\{?CARGO_TARGET_DIR\b/;
800
+ return /process\.env(?:\.CARGO_TARGET_DIR\b|\[["']CARGO_TARGET_DIR["']\])/;
801
+ }
802
+
803
+ function findCargoTargetDirGuardFiles(root) {
804
+ const found = [];
805
+ const visit = (dir) => {
806
+ if (!existsSync(dir)) return;
807
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
808
+ if (entry.isDirectory() && ["node_modules", "target", "vendor", ".git", ".cache", ".right-release"].includes(entry.name)) continue;
809
+ const full = path.join(dir, entry.name);
810
+ if (entry.isDirectory()) visit(full);
811
+ else if (entry.isFile() && CARGO_TARGET_DIR_GUARD_EXTENSIONS.has(path.extname(entry.name))) found.push(full);
812
+ }
813
+ };
814
+ const scriptsDir = path.join(root, "scripts");
815
+ if (existsSync(scriptsDir)) visit(scriptsDir);
816
+ for (const topLevel of ["package.sh", "package.ps1"]) {
817
+ const candidate = path.join(root, topLevel);
818
+ if (existsSync(candidate)) found.push(candidate);
819
+ }
820
+ return found;
821
+ }
822
+
823
+ test("consuming suite apps resolve Cargo build output from cargo metadata, never CARGO_TARGET_DIR directly", () => {
824
+ for (const appRoot of cargoTargetDirGuardApps) {
825
+ const root = path.join(workspace, appRoot);
826
+ if (!existsSync(root)) continue;
827
+ for (const filePath of findCargoTargetDirGuardFiles(root)) {
828
+ const basename = path.basename(filePath);
829
+ if (CARGO_TARGET_DIR_GUARD_SKIP_BASENAME.test(basename)) continue;
830
+ const extension = path.extname(filePath);
831
+ const pattern = cargoTargetDirGuardPatternFor(extension);
832
+ const lines = readFileSync(filePath, "utf8").split("\n");
833
+ const relativePath = path.relative(workspace, filePath);
834
+ lines.forEach((line, index) => {
835
+ if (!pattern.test(line)) return;
836
+ if (/^\s*(?:\/\/|#)/.test(line)) return; // prose describing the pattern, not code reading it
837
+ const previousLine = index > 0 ? lines[index - 1] : "";
838
+ const allowed = CARGO_TARGET_DIR_GUARD_ALLOW_MARKER.test(line) || CARGO_TARGET_DIR_GUARD_ALLOW_MARKER.test(previousLine);
839
+ assert.ok(
840
+ allowed,
841
+ `${relativePath}:${index + 1} reads CARGO_TARGET_DIR directly to locate Cargo build output: \`${line.trim()}\`\n`
842
+ + "resolve build output from `cargo metadata`'s `target_directory`; the environment is never authoritative for locating output.\n"
843
+ + "add a `rightkit-allow-cargo-target-dir: <reason>` marker on this line (or the line above) only if this use is not locating output.",
844
+ );
845
+ });
846
+ }
847
+ }
848
+ });
849
+
850
+ // The shared resolver helpers themselves must keep asking cargo metadata for
851
+ // target_directory. Guarding only the wrong pattern's reintroduction is not
852
+ // enough — gutting/deleting the correct implementation must also fail here.
853
+ const cargoTargetRootHelpers = [
854
+ "coderight/apps/coderight-tauri/scripts/lib/target-root.mjs",
855
+ "heardright/tauri-app-next/scripts/lib/target-root.mjs",
856
+ "mailright/scripts/lib/target-root.mjs",
857
+ "orthic/scripts/lib/target-root.mjs",
858
+ "viewright/scripts/lib/target-root.mjs",
859
+ "membrane/apps/membrane-hub/scripts/lib/target-root.mjs",
860
+ ];
861
+
862
+ test("shared target-root helpers still resolve build output via cargo metadata's target_directory", () => {
863
+ for (const helperPath of cargoTargetRootHelpers) {
864
+ const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
865
+ if (!existsSync(appRoot)) continue;
866
+ const fullPath = path.join(workspace, helperPath);
867
+ assert.ok(existsSync(fullPath), `${helperPath} is missing; the app must keep its shared cargo-metadata target resolver`);
868
+ const source = readFileSync(fullPath, "utf8");
869
+ assert.match(source, /cargo/, `${helperPath} must invoke cargo`);
870
+ assert.match(source, /metadata/, `${helperPath} must call cargo metadata`);
871
+ assert.match(source, /target_directory/, `${helperPath} must read target_directory from cargo metadata's output`);
872
+ }
873
+ });
874
+
875
+ // After the app migration lands, every consuming app's target-root helper must
876
+ // become a thin re-export of the shared tools/rightkit resolveTargetRoot
877
+ // (cargo-target.mjs) rather than its own copy of the cargo-metadata call. Until
878
+ // that migration lands, cargoTargetRootHelpers above still point at full local
879
+ // implementations, so this stays skipped to avoid failing the still-unmigrated
880
+ // app repos. Enable it once every app's target-root.mjs is a re-export shim.
881
+ test("consuming app target-root helpers are thin re-export shims, not local resolver re-implementations", { skip: true }, () => {
882
+ const LOCAL_RESOLVER_PATTERN = /function\s+(?:cargoTargetRoot|resolveManagedCargoTarget)\s*\([^)]*\)\s*\{[^}]*cargo[^}]*metadata/s;
883
+ for (const helperPath of cargoTargetRootHelpers) {
884
+ const appRoot = path.join(workspace, helperPath.split("/scripts/lib/target-root.mjs")[0]);
885
+ if (!existsSync(appRoot)) continue;
886
+ const fullPath = path.join(workspace, helperPath);
887
+ if (!existsSync(fullPath)) continue;
888
+ const source = readFileSync(fullPath, "utf8");
889
+ assert.doesNotMatch(
890
+ source,
891
+ LOCAL_RESOLVER_PATTERN,
892
+ `${helperPath} must re-export @rightkit/release's resolveTargetRoot instead of reimplementing the cargo metadata call locally`,
893
+ );
894
+ }
895
+ });
896
+
772
897
  test("Right Suite has no hosted workflow files", () => {
773
898
  for (const root of ["viewright", "scraperight", "heardright", "mailright", "coderight", "genright", "voiceright", "tools/rightkit"]) {
774
899
  const workflowDir = path.join(workspace, root, ".github", "workflows");
@@ -19,7 +19,7 @@
19
19
  "@rightkit/legal": "0.3.0",
20
20
  "@rightkit/legal-ui": "0.1.1",
21
21
  "@rightkit/license": "0.1.6",
22
- "@rightkit/release": "0.2.65",
22
+ "@rightkit/release": "0.2.66",
23
23
  "@rightkit/qa": "0.2.0"
24
24
  },
25
25
  "legacyNpm": {
@@ -47,7 +47,8 @@
47
47
  "0.2.61",
48
48
  "0.2.62",
49
49
  "0.2.63",
50
- "0.2.64"
50
+ "0.2.64",
51
+ "0.2.65"
51
52
  ],
52
53
  "@rightkit/qa": [
53
54
  "0.1.0"
package/target-bridge.mjs CHANGED
@@ -9,12 +9,21 @@ import path from "node:path";
9
9
  * fingerprint change — i.e. every Cargo.lock or version bump — left a stale
10
10
  * link that hard-stopped the next build until someone deleted it by hand.
11
11
  * Omit it to keep the strict refuse-everything behavior.
12
+ * @param brokerManaged True when a build broker owns CARGO_TARGET_DIR on this
13
+ * host. A real (non-symlink) directory at `link` is then not corruption to
14
+ * repair — it is the broker's own build output, or a leftover from before the
15
+ * broker took ownership, and either way is not this bridge's to touch. The
16
+ * non-broker path used to name that same directory "corrupt" and point at
17
+ * `cache migrate`, which renames it away; on a broker host that came within
18
+ * one step of moving ~5.9 GB of warm intermediates the broker still expects
19
+ * to find in place. Leave it untouched instead.
12
20
  */
13
21
  export function createTargetBridge({
14
22
  link,
15
23
  target,
16
24
  ownedRoot,
17
25
  platform = process.platform,
26
+ brokerManaged = false,
18
27
  lstat = lstatSync,
19
28
  mkdir = mkdirSync,
20
29
  realpath = realpathSync,
@@ -29,6 +38,7 @@ export function createTargetBridge({
29
38
  ensure() {
30
39
  let entry = readEntry(link, lstat);
31
40
  if (entry && !entry.isSymbolicLink()) {
41
+ if (brokerManaged) return { created: false, link, target, brokerManaged: true };
32
42
  throw new Error(
33
43
  `primary checkout target is not a symbolic link; refusing to replace it: ${link}\n` +
34
44
  ` It is a real directory holding build output. Run right-release cache migrate before building.`,
@@ -160,6 +160,41 @@ test("rejecting a real target does not create the shared cache destination", ()
160
160
  assert.equal(existsSync(target), false);
161
161
  });
162
162
 
163
+ test("broker-managed: a real target directory is left untouched instead of treated as corruption", async () => {
164
+ const fx = fixture();
165
+ mkdirSync(fx.link);
166
+ writeFileSync(path.join(fx.link, "user.txt"), "keep\n");
167
+ const result = await createTargetBridge({ ...fx, brokerManaged: true }).run(async (bridge) => bridge.ensure());
168
+ assert.equal(result.brokerManaged, true);
169
+ assert.equal(result.created, false);
170
+ // Nothing was removed, replaced, or relinked: the real directory and its
171
+ // content are exactly as they were, and no shared-cache destination was
172
+ // fabricated in its place.
173
+ assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
174
+ assert.equal(lstatSync(fx.link).isSymbolicLink(), false);
175
+ });
176
+
177
+ test("broker-managed: release() is a safe no-op after leaving a real target untouched", () => {
178
+ const fx = fixture();
179
+ mkdirSync(fx.link);
180
+ writeFileSync(path.join(fx.link, "user.txt"), "keep\n");
181
+ const bridge = createTargetBridge({ ...fx, brokerManaged: true });
182
+ bridge.ensure();
183
+ assert.equal(bridge.release(), false);
184
+ assert.equal(readFileSync(path.join(fx.link, "user.txt"), "utf8"), "keep\n");
185
+ });
186
+
187
+ test("broker-managed has no effect on the non-broker path: a symlink bridge still behaves exactly as before", async () => {
188
+ const fx = fixture();
189
+ const result = await createTargetBridge({ ...fx, brokerManaged: true }).run(async (bridge) => {
190
+ const ensured = bridge.ensure();
191
+ assert.equal(lstatSync(fx.link).isSymbolicLink(), true);
192
+ return ensured;
193
+ });
194
+ assert.equal(result.created, true);
195
+ assert.equal(result.brokerManaged, undefined);
196
+ });
197
+
163
198
  test("cleanup is idempotent and reports process errors without masking a build failure", async () => {
164
199
  const fx = fixture();
165
200
  const bridge = createTargetBridge(fx);