@rightkit/release 0.6.15 → 0.6.20

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 (65) hide show
  1. package/README.md +7 -3
  2. package/admission-host.mjs +9 -5
  3. package/app-closure.mjs +9 -3
  4. package/bin/darwin/rightkit-swap +0 -0
  5. package/bin/rightkit-gate.mjs +469 -102
  6. package/bin/rightkit-linux-setup.mjs +15 -0
  7. package/broker-admission.mjs +184 -0
  8. package/broker-wire.mjs +15 -3
  9. package/build-launch.mjs +4 -1
  10. package/build-ownership.mjs +125 -36
  11. package/build-release.mjs +39 -7
  12. package/cargo-contract.mjs +4 -1
  13. package/cargo-guard.mjs +25 -5
  14. package/cargo-target-shim.mjs +56 -7
  15. package/clippy-route.mjs +8 -7
  16. package/crate-graph.mjs +5 -1
  17. package/credential-provider.mjs +174 -0
  18. package/deterministic-scans.mjs +256 -41
  19. package/development.mjs +8 -0
  20. package/disk-policy.mjs +2 -2
  21. package/gate-executor.mjs +5 -3
  22. package/gate-stages.mjs +34 -3
  23. package/gradle-lock.mjs +206 -0
  24. package/heavy-command.mjs +33 -2
  25. package/linux-artifacts.mjs +2 -1
  26. package/linux-setup.mjs +106 -0
  27. package/local-publish.mjs +21 -10
  28. package/local-qualify.mjs +9 -3
  29. package/managed-cargo.mjs +54 -18
  30. package/npm-license-notices.mjs +49 -1
  31. package/ownership-lockfiles.mjs +27 -4
  32. package/ownership.mjs +5 -1
  33. package/package.json +17 -11
  34. package/protected-credentials.mjs +13 -5
  35. package/publication-gate.mjs +9 -4
  36. package/python-lock.mjs +209 -0
  37. package/qa-admission-policy.json +32 -0
  38. package/qa-layout.mjs +229 -0
  39. package/qa-runner.mjs +156 -0
  40. package/release-artifact-path.mjs +29 -3
  41. package/release-config.mjs +1 -1
  42. package/release-qa-command.mjs +58 -8
  43. package/release-qa-policy.mjs +5 -3
  44. package/release-source-identity.mjs +1 -0
  45. package/release.mjs +10 -2
  46. package/rightkit-ownership.json +1465 -79
  47. package/rightkit-registry-resolver.mjs +7 -3
  48. package/rightkit-versions.json +173 -36
  49. package/runtime-budget-classes.json +4 -0
  50. package/runtime-budgets.mjs +5 -1
  51. package/shell-contract.mjs +1 -1
  52. package/snapshot-broker.mjs +107 -9
  53. package/snapshot-check.mjs +2 -0
  54. package/snapshot-request.mjs +20 -3
  55. package/snapshot.mjs +11 -4
  56. package/stage-commands.mjs +2 -1
  57. package/stage-ledger.mjs +15 -0
  58. package/stage-sidecar.mjs +17 -3
  59. package/suite-signing.mjs +40 -4
  60. package/swift-lock.mjs +334 -0
  61. package/test-command.mjs +13 -3
  62. package/tool-build.mjs +156 -0
  63. package/unit-gate.mjs +9 -2
  64. package/windows-identity-plan.mjs +119 -0
  65. package/windows-identity.mjs +136 -0
package/build-release.mjs CHANGED
@@ -46,7 +46,7 @@ import { collectSwiftToolVersions, planSwiftBuild } from "./swift-build-context.
46
46
  import { planIosNativeBuild, resolveIosBuildContext } from "./ios-build-context.mjs";
47
47
  import { packageCommandOptional, requiredBuildExecutables, resolveProjectType, targetUsesMsix } from "./project-type.mjs";
48
48
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
49
- import { acquireHeavyWorkSlot, heavyCommandEnvironment, summarizePreBrokerWaits, terminateProcessTree } from "./heavy-command.mjs";
49
+ import { acquireRunHeavySlot, heavyCommandEnvironment, summarizePreBrokerWaits, terminateProcessTree, withLocalHeavySlot } from "./heavy-command.mjs";
50
50
  import { processGroupCpuSeconds } from "./process-liveness.mjs";
51
51
  import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
52
52
  import { beginArtifactLease, completeArtifactLease, materializeArtifacts, retainArtifacts, resolveArtifactDir, resolveManagedRoot } from "./artifact-storage.mjs";
@@ -171,6 +171,8 @@ let child = null;
171
171
  let suiteSlot;
172
172
  let cacheLease;
173
173
  let heavySlot;
174
+ // The run's slot decision (heavySlotDecision): on a broker host the run holds no slot and takes one only around its own local steps (runLocalStep).
175
+ let heavyDecisionOfRun = null;
174
176
  let interrupted;
175
177
  let requestActiveRunStop = null;
176
178
 
@@ -385,19 +387,43 @@ try {
385
387
  delete env[name];
386
388
  }
387
389
  }
388
- // A split's Cargo target is the one the gate bound and handed this process (row 7.09, R-T1): Cache V2's shared target does not replace it, or
389
- // the bound target the seal reads would not be the one Cargo wrote.
390
- if (splitSource && !managedCargoTarget && typeof process.env.CARGO_TARGET_DIR === "string" && path.isAbsolute(process.env.CARGO_TARGET_DIR)) env.CARGO_TARGET_DIR = process.env.CARGO_TARGET_DIR;
390
+ // A split's Cargo environment is the one the gate bound and handed this process (row 7.09, R-T1): Cache V2's target, cargo home and compiler wrapper
391
+ // do not replace it, or the bound target the seal reads would not be the one Cargo wrote, and a prepare step that re-runs a build the compile stage
392
+ // already staged (stage-sidecar) would resolve crates from another cargo home (`--frozen` cannot fetch them) and rebuild the staged bytes
393
+ // (HeardRight Windows package, 2026-11-01: STAGE_SIDECAR_CATALOG_UNREADABLE, then "a staged entry is immutable for the door run").
394
+ if (splitSource && !managedCargoTarget) {
395
+ for (const name of ["CARGO_HOME", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "SCCACHE_IGNORE_SERVER_IO_ERROR"]) {
396
+ if (typeof process.env[name] === "string" && process.env[name] !== "") env[name] = process.env[name];
397
+ else delete env[name];
398
+ }
399
+ // The target only when the gate bound one process-wide: with several workspaces on a direct POSIX host the gate's per-call wrapper binds
400
+ // each and hands no CARGO_TARGET_DIR, and the target bridge below still needs Cache V2's.
401
+ if (typeof process.env.CARGO_TARGET_DIR === "string" && path.isAbsolute(process.env.CARGO_TARGET_DIR)) env.CARGO_TARGET_DIR = process.env.CARGO_TARGET_DIR;
402
+ }
391
403
  if (cacheMode !== "shared") delete env.RIGHT_RELEASE_CACHE_OWNER;
392
404
  if (cacheMode === "shared" && env.RIGHT_RELEASE_CACHE_OWNER !== "rightkit-v2") fail("shared cache ownership token was not configured");
393
405
  if (cacheMode !== "shared" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
394
- if (process.env.RIGHTSUITE_HEAVY_WORK_OWNER !== "1") heavySlot = acquireHeavyWorkSlot();
406
+ // The host-wide heavy-work slot (rightkit#1367 step 2). A direct host takes it for the whole run. On a broker host every hook the build runs (cargo, SwiftPM, xcodebuild,
407
+ // pnpm, node, QA) goes through build-launch and becomes a broker job, which the broker's scheduler and resource budget admit: no slot and no memory/CPU wait for the run.
408
+ // What this process runs itself, outside build-launch, is local work the broker never sees: today its own `pnpm install` (prepare, below), which takes the slot around just
409
+ // that step (runLocalStep). A child of a skipped slot is not told it has one (no RIGHTSUITE_HEAVY_WORK_OWNER), so a heavy-command it starts takes its own. The worker's
410
+ // signing, notarization and disk-image steps (sign-macos, notarize-macos, dmg-build) are not builds and hold no slot on a broker host.
411
+ const { decision: heavyDecision, slot: runSlot } = acquireRunHeavySlot({ env: process.env, brokerHost: brokerManagedHost(process.env) });
412
+ heavyDecisionOfRun = heavyDecision;
413
+ if (runSlot) heavySlot = runSlot;
414
+ else if (heavyDecision.skipReason) {
415
+ delete env.RIGHTSUITE_HEAVY_WORK_OWNER;
416
+ console.error(`[right-release] heavy-work slot not held for the run (${heavyDecision.skipReason})`);
417
+ }
418
+ // The shared-cache suite slot (acquireSuiteBuildSlot, below) is one-at-a-time per cache layout, not a build count of the broker's. It runs only in cache mode "shared", which a
419
+ // broker host never has: a Cargo build there is forced to "broker" (managedCargoTarget above) and SwiftPM and native iOS plans are always "legacy" (swift-build-context,
420
+ // ios-build-context). So on a broker host it is skipped, with no exception; a direct host keeps it.
395
421
  if (cacheMode === "shared") {
396
422
  suiteSlot = acquireSuiteBuildSlot({ layout: sharedLayout, pid: process.pid, argv: process.argv, waitMs: Number(process.env.BUILD_GUARD_WAIT_SECS ?? 900) * 1000 });
397
423
  cacheLease = acquireCacheLease({ layout: sharedLayout, pid: process.pid, argv: process.argv });
398
424
  }
399
425
  // Pre-broker wait attribution: every layer, ms, and blocker identity (Step 1 instrumentation).
400
- const preBrokerWaits = summarizePreBrokerWaits({ heavySlot, suiteSlot, heavyOwnerInherited: process.env.RIGHTSUITE_HEAVY_WORK_OWNER === "1" });
426
+ const preBrokerWaits = summarizePreBrokerWaits({ heavySlot, suiteSlot, heavyOwnerInherited: heavyDecision.inherited, heavySkipped: heavyDecision.skipReason });
401
427
  console.error(`[right-release] pre-broker waits: ${preBrokerWaits.summary}`);
402
428
  throwIfInterrupted();
403
429
  const managedRoot = resolveManagedRoot({ env: process.env });
@@ -540,7 +566,7 @@ try {
540
566
  if (linked.action === "moved-aside") console.error(`[right-release] hosted CI: moved existing ${targetLink} aside to ${linked.aside}`);
541
567
  }
542
568
  // A Cargo-less native iOS app installs Node dependencies only when it has a package.json.
543
- if (nativeIos ? hasPackageJson : resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager) await runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, env, cacheTarget, path.join(stateRoot, "stall-install.log"));
569
+ if (nativeIos ? hasPackageJson : resolveProjectType({ appRoot, config, target }) !== "swiftpm" || config.packageManager) await runLocalStep(() => runProgress(config.packageManager ?? "pnpm", ["install", "--frozen-lockfile"], appRoot, heavyDecisionOfRun?.localSteps ? { ...env, RIGHTSUITE_HEAVY_WORK_OWNER: "1" } : env, cacheTarget, path.join(stateRoot, "stall-install.log")));
544
570
  },
545
571
  build: async () => {
546
572
  throwIfInterrupted();
@@ -917,6 +943,12 @@ function collectToolVersions(packageManager, probeDir = process.cwd()) {
917
943
  };
918
944
  }
919
945
 
946
+ /** On a broker host: the heavy-work slot, held around just the local step `run` (the release's own `pnpm install`), never around broker jobs. Elsewhere `run` runs as it is (the run holds the slot or inherits it). */
947
+ function runLocalStep(run) {
948
+ const outer = heavySlot;
949
+ return withLocalHeavySlot(heavyDecisionOfRun, run, { onSlot: (slot) => { heavySlot = slot ?? outer; } });
950
+ }
951
+
920
952
  async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null, { livenessOwnerTimeoutMs = null } = {}) {
921
953
  const { inactivityMs, absoluteMs } = releaseProgressLimits(env, { livenessOwnerTimeoutMs });
922
954
  let lastProgress = Date.now();
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { spawnSync } from "./background-process.mjs";
5
- import { directPlatform, linuxBrokerDeclared, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
5
+ import { directPlatform, gateNoBroker, linuxBrokerDeclared, managedRuntimeClient, runCargoSync } from "./managed-cargo.mjs";
6
6
 
7
7
  const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".right-release", ".worktrees", "node_modules", "target", "vendor"]);
8
8
  const CRATES_IO_SOURCES = new Set([
@@ -30,6 +30,9 @@ export function managedCargoShimOnPath(env = process.env, platform = process.pla
30
30
  }
31
31
 
32
32
  export function brokerManagedHost(env = process.env, platform = process.platform, exists = existsSync) {
33
+ // A gate run under Adrian's recorded no-broker exception runs Cargo directly (managed-cargo gateNoBroker): the broker owns no
34
+ // target or cache for it, so nothing may drop the target the gate pinned, whatever RustRight is installed.
35
+ if (gateNoBroker(env)) return false;
33
36
  // A Linux host is broker-managed only through its explicit declared route (managed-cargo linuxBrokerDeclared), whatever socket or control root the env names.
34
37
  if (directPlatform(platform)) return linuxBrokerDeclared(env, platform);
35
38
  return Boolean(managedRuntimeClient(env, platform, exists)) || Boolean(env.RUSTRIGHT_BUILD_BROKER_SOCKET || env.RIGHTKIT_BUILD_BROKER_SOCKET) || managedCargoShimOnPath(env, platform, exists);
package/cargo-guard.mjs CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
 
9
9
  import { resolveSharedCacheRoot, resolveStableRoot } from "./cache-policy.mjs";
10
10
  import { brokerManagedHost } from "./cargo-contract.mjs";
11
+ import { brokerClientRoute } from "./managed-cargo.mjs";
11
12
  import { runHeavyCommand } from "./heavy-command.mjs";
12
13
 
13
14
  const LIGHT_COMMANDS = new Set(["fmt", "metadata", "tree", "fetch", "search", "locate-project", "read-manifest", "help", "version", "--version", "-V"]);
@@ -137,16 +138,25 @@ export function resolveManagedCargoShim({ env = process.env, platform = process.
137
138
  const win = platform === "win32" || platform === "win";
138
139
  const delimiter = win ? ";" : ":";
139
140
  const agentBin = /(?:^|[\\/])(?:\.rightkit-managed|rightkitmanagedagent|rightkit[\\/]build-control)[\\/]agent-bin[\\/]?$/i;
141
+ // Every extension cargo-contract.mjs managedCargoShimOnPath accepts (PATHEXT, else .COM;.EXE;.BAT;.CMD), so a shim that predicate sees is a shim this finds.
142
+ const extensions = win ? String(env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((extension) => extension.toLowerCase()) : [""];
140
143
  for (const directory of String(env.PATH ?? "").split(delimiter).filter(Boolean)) {
141
144
  if (!agentBin.test(directory)) continue;
142
- for (const name of win ? ["cargo.exe", "cargo.cmd"] : ["cargo"]) {
143
- const candidate = path.join(directory, name);
145
+ for (const extension of extensions) {
146
+ const candidate = path.join(directory, `cargo${extension}`);
144
147
  if (exists(candidate)) return candidate;
145
148
  }
146
149
  }
147
150
  return null;
148
151
  }
149
152
 
153
+ /**
154
+ * The broker's own client for a broker host whose cargo shim is not on PATH (a Dell socket or managed client with no agent-bin directory, a non-login shell, the declared
155
+ * Linux route): the shared route managed-cargo.mjs runCargoSync takes (`brokerClientRoute`): `{ client, env }`, run as `<client> cargo <args>`; `{ refusal }` when the declared
156
+ * route is invalid or the Mac's socket is unavailable; null when no client is reachable.
157
+ */
158
+ export const resolveBrokerClientRoute = brokerClientRoute;
159
+
150
160
  function pathApi(platform) {
151
161
  return platform === "win32" || platform === "win" ? path.win32 : path.posix;
152
162
  }
@@ -229,7 +239,7 @@ function spawnCargo(command, args, env) {
229
239
  });
230
240
  }
231
241
 
232
- export async function runCargoGuard(args, { env = process.env, policyOptions = {}, resolveCargo = resolveRealCargo, resolveRustc = resolveRealRustc, runHeavy = runHeavyCommand, runLight = spawnCargo, resolveShim = resolveManagedCargoShim, isBrokerHost = brokerManagedHost } = {}) {
242
+ export async function runCargoGuard(args, { env = process.env, policyOptions = {}, resolveCargo = resolveRealCargo, resolveRustc = resolveRealRustc, runHeavy = runHeavyCommand, runLight = spawnCargo, resolveShim = resolveManagedCargoShim, resolveClientRoute = resolveBrokerClientRoute, isBrokerHost = brokerManagedHost } = {}) {
233
243
  assertComputePolicyAllowsCargo(args, { ...policyOptions, env });
234
244
  // On a broker-managed host the build broker owns the toolchain, cache, and
235
245
  // quotas. Resolving the real Cargo here and running it via runHeavy would
@@ -240,8 +250,18 @@ export async function runCargoGuard(args, { env = process.env, policyOptions = {
240
250
  if (isBrokerHost(env)) {
241
251
  const shim = resolveShim({ env });
242
252
  if (shim) return runLight(shim, args, env);
243
- // No shim found despite a broker signal: fall through to legacy resolution
244
- // rather than silently dropping the guard.
253
+ // No shim on PATH (a Dell socket or managed client with no agent-bin, a non-login shell, the declared Linux route): hand the call to the managed or declared runtime
254
+ // client, the client brokerManagedHost resolves, as `<client> cargo <args>`. Running the real cargo here would be a build outside the broker, which neither counts nor
255
+ // admits it (the heavy-work slot is not taken on a broker host, rightkit#1367 step 2), so a call that guards (a build) is refused when no client is reachable.
256
+ // Only a command that guards (a build) goes to the client; a light command (fmt, metadata, --version, ...) builds nothing and runs the real cargo as it always did.
257
+ if (shouldGuardCargo(args)) {
258
+ const route = resolveClientRoute({ env });
259
+ if (route && !route.refusal) return runLight(route.client, ["cargo", ...args], route.env);
260
+ console.error(route?.refusal
261
+ ? `cargo-guard: this machine is broker-managed and no broker shim is on PATH, and the RustRight client route is refused (${route.refusal}); refusing to run cargo outside the broker. Start or repair the broker, then run again.`
262
+ : "cargo-guard: this machine is broker-managed but neither the broker's cargo shim nor a RustRight client was found; refusing to run cargo outside the broker. Start or repair the broker, then run again.");
263
+ return 1;
264
+ }
245
265
  }
246
266
  const cargo = resolveCargo({ env });
247
267
  if (shouldGuardCargo(args)) {
@@ -28,7 +28,7 @@
28
28
  // overridden the value the gate passed: a residual of the trusted per-user model, not refused (map 7.12).
29
29
  import fs from "node:fs";
30
30
  import path from "node:path";
31
- import { createHash } from "node:crypto";
31
+ import { createHash, randomBytes } from "node:crypto";
32
32
  import { spawnSync } from "./background-process.mjs";
33
33
  import { spawnBrokerClient } from "./broker-client.mjs";
34
34
  import { fileURLToPath } from "node:url";
@@ -158,9 +158,25 @@ export function decideCargoInvocation({ args, cwd, env, map, locate, realpath =
158
158
  * Broker mode (`broker: { checkoutDirs, section }`, the Mac): `checkoutDirs` is the gate's { [cargoDir]: absolute path of that
159
159
  * workspace in the primary checkout } (the snapshot-to-checkout map, the only source of a request's primaryCwd) and `section` the
160
160
  * public part of the gate's snapshot broker (brokerSection). `realCargo` then only runs `version` and `help`; it may be null.
161
+ *
162
+ * Windows, broker mode only (rightkit#1350, rustright#215): a child started without a shell finds only an .exe, so `bin/cargo.exe` is a
163
+ * byte copy of the live RustRight client (`broker.section.client`) and `bin/gate-trampoline.json` beside it pins node, this module and the
164
+ * map by path and sha256. The client finds that sidecar beside its own executable and runs `node <module> --map <map> -- <args>` and
165
+ * nothing else (gate-trampoline mode). The client must answer `trampoline-capability` with the schema and the copy must hash to it before
166
+ * the sidecar is written; the client's digest is part of the identity, and so is the run's ownership handle (broker section), so two live
167
+ * runs never share a wrapper directory. A direct Windows host still has no wrapper.
161
168
  */
162
- export function installCargoShim({ base, snapshotRoot, targetDirs, broker = null, realCargo, node = process.execPath, platform = process.platform, realpath = (p) => fs.realpathSync.native(p), onDir = () => {}, write = (file, text, mode) => fs.writeFileSync(file, text, { mode }) }) {
163
- if (platform === "win32") throw Object.assign(new Error("the runtime cargo wrapper needs a POSIX host: a Windows child finds only an .exe on PATH"), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
169
+ export const TRAMPOLINE_SCHEMA = "rustright.gate-trampoline/1";
170
+ const sha256File = (file) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
171
+ const probeTrampoline = (exe) => {
172
+ const r = spawnSync(exe, ["trampoline-capability"], { encoding: "utf8", windowsHide: true });
173
+ return r.status === 0 ? String(r.stdout ?? "").trim() : `exit ${r.status ?? r.signal ?? r.error?.message}`;
174
+ };
175
+ export function installCargoShim({ base, snapshotRoot, targetDirs, broker = null, realCargo, node = process.execPath, platform = process.platform, realpath = (p) => fs.realpathSync.native(p), onDir = () => {}, write = (file, text, mode) => fs.writeFileSync(file, text, { mode }), copyFile = (from, to) => fs.copyFileSync(from, to), hashFile = sha256File, probe = probeTrampoline }) {
176
+ if (platform === "win32" && !broker) throw Object.assign(new Error("the runtime cargo wrapper needs a POSIX host or a Windows broker host: a Windows child finds only an .exe on PATH"), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
177
+ const trampoline = platform === "win32";
178
+ if (trampoline && (typeof broker.section?.client !== "string" || !path.win32.isAbsolute(broker.section.client))) throw Object.assign(new Error(`a Windows broker wrapper needs the absolute RustRight client (${JSON.stringify(broker.section?.client)})`), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
179
+ const clientSha256 = trampoline ? hashFile(broker.section.client) : null;
164
180
  const mode = broker ? "broker" : "direct";
165
181
  if (mode === "direct" && (typeof realCargo !== "string" || !path.isAbsolute(realCargo))) throw Object.assign(new Error(`no absolute cargo to wrap (${JSON.stringify(realCargo)})`), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
166
182
  if (mode === "broker" && ((realCargo !== null && realCargo !== undefined && !path.isAbsolute(realCargo)) || !broker.section || !broker.checkoutDirs)) throw Object.assign(new Error("a broker wrapper needs the gate's checkout map and broker section"), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
@@ -171,12 +187,15 @@ export function installCargoShim({ base, snapshotRoot, targetDirs, broker = null
171
187
  const identityHash = createHash("sha256").update(JSON.stringify({ realCargo: realCargo ?? null, node, workspaces: sorted, ...(broker ? { mode, section: broker.section } : {}) })).update(fs.readFileSync(self));
172
188
  // The broker half of the wrapper is part of its identity: any change to the code that carries and verifies a call is another wrapper.
173
189
  if (broker) for (const name of ["snapshot-broker.mjs", "snapshot-request.mjs", "snapshot-metadata.mjs", "provenance-verify.mjs", "cargo-ledger.mjs"]) identityHash.update(fs.readFileSync(new URL(`./${name}`, import.meta.url)));
190
+ if (trampoline) identityHash.update(`\0client:${clientSha256}`);
174
191
  const identity = identityHash.digest("hex");
175
192
  const dir = path.join(base, `rk-cargo-shim-${identity.slice(0, 16)}`);
176
193
  const bin = path.join(dir, "bin");
177
- const shim = path.join(bin, "cargo");
194
+ const shim = path.join(bin, trampoline ? "cargo.exe" : "cargo");
178
195
  const mapFile = path.join(dir, "cargo-target-map.json");
179
- const ledgerDir = path.join(dir, "ledger");
196
+ // The wrapper directory is identity-keyed and outlives a killed run, so each install gets its own ledger: a dead run's
197
+ // pending record is never read as this run's call.
198
+ const ledgerDir = path.join(dir, `ledger-${process.pid}-${randomBytes(6).toString("hex")}`);
180
199
  // The caller learns the directory before anything is written into it (its cleanup owns it from here), and a failed install removes
181
200
  // what it wrote before it throws.
182
201
  onDir(dir);
@@ -188,7 +207,17 @@ export function installCargoShim({ base, snapshotRoot, targetDirs, broker = null
188
207
  const sentinel = mode === "broker" || Object.keys(sorted).length > 1 ? path.join(dir, "unbound") : null;
189
208
  const section = broker ? { ...broker.section, sentinel } : null;
190
209
  put(mapFile, `${JSON.stringify({ schema: SHIM_MAP_SCHEMA, mode, identity, realCargo: realCargo ?? null, shim, ledgerDir, workspaces: sorted, ...(section ? { broker: section } : {}) }, null, 2)}\n`, 0o644);
191
- put(shim, `#!/bin/sh\nexec ${quote(node)} ${quote(self)} --map ${quote(mapFile)} -- "$@"\n`, 0o755);
210
+ if (trampoline) {
211
+ // The probe asks the client under its own name: a copy named cargo.exe is a managed cargo shim to RustRight (shims.rs), which would send the
212
+ // probe to the broker as a cargo request instead of answering it.
213
+ const answer = probe(broker.section.client);
214
+ if (answer !== TRAMPOLINE_SCHEMA) throw Object.assign(new Error(`the RustRight client ${broker.section.client} has no gate-trampoline mode (trampoline-capability answered ${JSON.stringify(answer)}, not ${TRAMPOLINE_SCHEMA}); install a RustRight runtime with rustright#215`), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
215
+ copyFile(broker.section.client, shim);
216
+ const copied = hashFile(shim);
217
+ if (copied !== clientSha256) throw Object.assign(new Error(`the copy ${shim} hashes to ${copied}, not the client's ${clientSha256}`), { code: "RK_CARGO_SHIM_UNSUPPORTED" });
218
+ const pin = (file) => ({ path: file, sha256: hashFile(file) });
219
+ put(path.join(bin, "gate-trampoline.json"), `${JSON.stringify({ schema: TRAMPOLINE_SCHEMA, node: pin(node), module: pin(self), map: pin(mapFile) }, null, 2)}\n`, 0o644);
220
+ } else put(shim, `#!/bin/sh\nexec ${quote(node)} ${quote(self)} --map ${quote(mapFile)} -- "$@"\n`, 0o755);
192
221
  return { dir, bin, shim, mapFile, ledgerDir, mode, workspaces: sorted, identity, sentinel };
193
222
  } catch (error) {
194
223
  fs.rmSync(dir, { recursive: true, force: true });
@@ -230,12 +259,32 @@ export function locateWorkspaceByWalk(manifestPath, cwd, { read = (f) => fs.read
230
259
  }
231
260
 
232
261
  /** The wrapper itself: `cargo-target-shim.mjs --map <file> -- <cargo args>`. Returns the exit status. */
262
+ /** The wrapper's own reader of its map file (runShim reads it through this): throws when the file is unreadable or is another schema. */
263
+ export function readShimMap(file) {
264
+ const map = JSON.parse(fs.readFileSync(file, "utf8"));
265
+ if (map?.schema !== SHIM_MAP_SCHEMA) throw new Error(`schema ${JSON.stringify(map?.schema)}`);
266
+ return map;
267
+ }
268
+
269
+ /**
270
+ * The per-call wrapper this process runs under, from its environment: CARGO names the wrapper (`<dir>/bin/cargo`) and the map it reads
271
+ * is `<dir>/cargo-target-map.json`. Returns the map only for a direct-mode wrapper whose own `shim` is that CARGO (a stale or foreign CARGO
272
+ * is no wrapper), else null. `map.workspaces` is { [real snapshot workspace]: the checkout's target directory }.
273
+ */
274
+ export function shimMapOfEnv(env = process.env, { read = readShimMap } = {}) {
275
+ const cargo = env?.CARGO;
276
+ if (typeof cargo !== "string" || !path.isAbsolute(cargo)) return null;
277
+ let map;
278
+ try { map = read(path.join(path.dirname(path.dirname(cargo)), "cargo-target-map.json")); } catch { return null; }
279
+ return map?.mode === "direct" && map.shim === cargo && map.workspaces && typeof map.workspaces === "object" ? map : null;
280
+ }
281
+
233
282
  export function runShim(argv, { env = process.env, cwd = process.cwd(), spawn = spawnSync, err = (m) => process.stderr.write(`${m}\n`) } = {}) {
234
283
  const sep = argv.indexOf("--");
235
284
  const mapAt = argv.indexOf("--map");
236
285
  if (mapAt < 0 || sep < 0 || mapAt > sep) { err("rightkit-gate: RK_CARGO_SHIM_ERROR: usage: cargo-target-shim.mjs --map <file> -- <cargo args>"); return SHIM_REFUSAL_STATUS; }
237
286
  let map;
238
- try { map = JSON.parse(fs.readFileSync(argv[mapAt + 1], "utf8")); if (map?.schema !== SHIM_MAP_SCHEMA) throw new Error(`schema ${JSON.stringify(map?.schema)}`); }
287
+ try { map = readShimMap(argv[mapAt + 1]); }
239
288
  catch (error) { err(`rightkit-gate: RK_CARGO_SHIM_ERROR: the cargo target map cannot be read (${error.message}); nothing ran`); return SHIM_REFUSAL_STATUS; }
240
289
  const words = argv.slice(sep + 1);
241
290
  const token = env[SCAN_TOKEN_ENV];
package/clippy-route.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  // The Clippy route: `rightkit-gate clippy [<cargo clippy args>]` (GATE-CONTRACT §1 "Clippy route & batching", E13 to E15; item 8 of
2
2
  // the joint plan, rows 8.1 to 8.16 of the signed map). A gate-owned destination for a request such as
3
3
  // `cargo clippy --workspace --all-targets -- -D warnings`: it builds HEAD's commit (never the checkout) from the gate-owned
4
- // snapshot, under the host's build ownership, after FULL class A (D8-2), and stops after diagnostics. It has no stage chain, runs no
4
+ // snapshot, under the repository's build ownership, after FULL class A (D8-2), and stops after diagnostics. It has no stage chain, runs no
5
5
  // package, sign, publish or release stage, and writes nothing a door receipt, qualification or the full gate could take as stage
6
6
  // evidence (row 8.5).
7
7
  //
@@ -28,7 +28,7 @@ import fs from "node:fs";
28
28
  import os from "node:os";
29
29
  import path from "node:path";
30
30
  import { resolveExecutor as defaultResolveExecutor } from "./gate-executor.mjs";
31
- import { directPlatform } from "./managed-cargo.mjs";
31
+ import { directPlatform, realCargoOf } from "./managed-cargo.mjs";
32
32
  import { defaultSpawnAsync, sidecarBoundCrate } from "./deterministic-scans.mjs";
33
33
  import { gateStateDir } from "./gate-stages.mjs";
34
34
  import { ProvenanceError, verifyAttributionDetail } from "./provenance-verify.mjs";
@@ -362,11 +362,12 @@ export async function clippyCommand(o, args, deps = {}) {
362
362
  const snap = deps.snapshot ?? await import("./snapshot.mjs");
363
363
  const sc = deps.snapshotCheck ?? await import("./snapshot-check.mjs");
364
364
  const ownershipApi = deps.ownership ?? await import("./build-ownership.mjs");
365
- let selected; let snapshotsAt; let snapshotDir;
365
+ let selected; let snapshotsAt; let snapshotDir; let repoKey;
366
366
  try {
367
367
  selected = snap.resolveCommit(root, "HEAD");
368
368
  snapshotsAt = o.snapshotRoot ?? snap.snapshotRoot({ platform: o.platform, env: o.env });
369
- snapshotDir = path.join(snapshotsAt, snap.repositoryOf(root).key);
369
+ repoKey = snap.repositoryOf(root).key;
370
+ snapshotDir = path.join(snapshotsAt, repoKey);
370
371
  } catch (e) { o.err(`${tag} cannot select HEAD's commit: ${e.message}; nothing ran`); return REFUSED_EXIT; }
371
372
 
372
373
  // The output directory is the checkout's, resolved by the gate (row 8.12): a caller's CARGO_TARGET_DIR that points into the snapshot would
@@ -399,9 +400,9 @@ export async function clippyCommand(o, args, deps = {}) {
399
400
  if (preBound.length) { o.err(sidecarRefusal(tag, preBound, "read from the commit")); resultLine(o, tag, { result: "BLOCKED", code: ROUTE_CODES.sidecarBound, blockedOn: ["item7:7.05"], bound: preBound, launched: false }); return BLOCKED; }
400
401
 
401
402
  const timings = {}; const lap = (name, since) => { timings[name] = Date.now() - since; };
402
- // Build ownership first (E6): held from before the sync until the diagnostics are written. A busy host runs nothing and syncs nothing.
403
+ // Build ownership first (E6): held from before the sync until the diagnostics are written. A busy repository runs nothing and syncs nothing.
403
404
  let own;
404
- try { own = (deps.acquireOwnership ?? ownershipApi.acquireBuildOwnership)({ file: ownershipApi.ownershipFile({ platform: o.platform, beside: o.snapshotRoot }), stage: "clippy", snapshot: snapshotDir, commit: selected.commit, platform: o.platform, env: o.env }); }
405
+ try { own = (deps.acquireOwnership ?? ownershipApi.acquireBuildOwnership)({ file: ownershipApi.ownershipFile({ platform: o.platform, beside: o.snapshotRoot, repository: repoKey }), stage: "clippy", snapshot: snapshotDir, commit: selected.commit, platform: o.platform, env: o.env }); }
405
406
  catch (e) { if (e.code !== "busy" && e.code !== "refused") throw e; o.err(`${tag} BLOCKED: ${e.message}; ${e.code === "refused" ? "nothing ran, nothing created" : "the route never re-syncs a snapshot another request holds; nothing ran"}`); return BLOCKED; }
406
407
  o.out(`${tag} build ownership ${own.joined ? "joined" : "held"} ${own.handle}`);
407
408
  const abort = o.abort ?? new AbortController();
@@ -432,7 +433,7 @@ export async function clippyCommand(o, args, deps = {}) {
432
433
  // The snapshot's working directory for the request, and the Cargo workspace it names, resolved from the commit's own manifests.
433
434
  const snapCwd = relCwd ? path.join(synced.path, ...relCwd.split("/")) : synced.path;
434
435
  if (!fs.existsSync(snapCwd)) { o.err(`${tag} BLOCKED: the working directory ${relCwd} is not in commit ${synced.commit.slice(0, 12)}; run from a directory the commit holds; nothing compiled`); return BLOCKED; }
435
- const realCargo = which("cargo").find((c) => path.isAbsolute(c));
436
+ const realCargo = realCargoOf(which("cargo"));
436
437
  if (!realCargo) { o.err(`${tag} BLOCKED: no cargo on PATH to wrap for the snapshot compile; nothing compiled`); return BLOCKED; }
437
438
  const located = o.spawn("cargo", ["locate-project", "--workspace", "--message-format", "plain", ...(parsed.manifestPath ? ["--manifest-path", parsed.manifestPath] : [])], { cwd: snapCwd, env: o.env });
438
439
  if (failed(located) || !path.isAbsolute(text(located).trim())) { o.err(`${tag} BLOCKED: the request's Cargo workspace cannot be resolved in the snapshot (${why(located) || "no manifest"}); nothing compiled`); return BLOCKED; }
package/crate-graph.mjs CHANGED
@@ -2,7 +2,11 @@
2
2
  // closure and Cargo.lock dependency closure. Pure TOML parsing (smol-toml); nothing shells out to cargo.
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { parse } from "smol-toml";
5
+ import { createRequire } from "node:module";
6
+ // smol-toml loads on first parse, not at import: CI jobs whose closure reaches this module without installing
7
+ // @rightkit/release dependencies (publish.yml crates pack, bind) never parse TOML here (rightkit#1341).
8
+ let smolTomlParse;
9
+ const parse = (text) => (smolTomlParse ??= createRequire(import.meta.url)("smol-toml").parse)(text);
6
10
 
7
11
  const DEP_TABLES = ["dependencies", "build-dependencies", "dev-dependencies"];
8
12
 
@@ -0,0 +1,174 @@
1
+ // The OS credential provider (`rightkit-credentials`, crates/rightkit-credentials) that updater signing runs, obtained the one sanctioned way: the gate's
2
+ // tool route (`rightkit-gate --to tool --unit cargo:rightkit-credentials`, tool-build.mjs), never a PATH binary on a Mac or Windows host. It is the
3
+ // pattern of qa-runner.mjs:
4
+ //
5
+ // record `<managed data root>/credential-provider/<mac|win>/record.json`, `{schema, platform, providers: {<version>: {path, sha256, version}}, enrolledAt}`,
6
+ // and the binary at `<managed data root>/credential-provider/<platform>/<version>/rightkit-credentials[.exe]`: a COPY the enrol operation made, so a
7
+ // later `cargo clean` cannot change what signing executes. Outside every repository and snapshot.
8
+ // enrol `enrollCredentialProvider`: the gate-built file of the crate's committed version (>= MIN_PROVIDER_VERSION). A second enrolment of the same
9
+ // version is a rotation and needs `rotate`.
10
+ // use `resolveCredentialProvider`: the NEWEST enrolled version; its file must still be a regular file inside the managed directory whose sha256 IS
11
+ // the recorded one. Anything else is a typed refusal naming the sanctioned install.
12
+ //
13
+ // The binary has NO `--version` (its CLI is `status|set|delete|ensure-key|exec`; any other first word, with no `--namespace`, answers
14
+ // `{"ok":false,"error":"invalid --namespace: required"}` and exits 1). So the version is not attested by the binary: it is the version of the commit the gate built
15
+ // it from (the tool route passes the crate's committed version), bound to the copy by its sha256. What enrolment does check is that the file is this CLI
16
+ // (the identity probe below), which is a regular executable that touches no credential store.
17
+ import crypto from "node:crypto";
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { spawnSync } from "./background-process.mjs";
22
+ import { resolveManagedRoots } from "./managed-roots.mjs";
23
+
24
+ export const CREDENTIAL_PROVIDER_SCHEMA = "rightkit.credential-provider/1";
25
+ export const CREDENTIAL_PROVIDER_DIR = "credential-provider";
26
+ export const CREDENTIAL_PROVIDER_PLATFORMS = Object.freeze(["mac", "win"]);
27
+ export const MIN_PROVIDER_VERSION = "0.2.2";
28
+ /** The one sanctioned way to install the provider; every refusal names it. */
29
+ export const CREDENTIAL_PROVIDER_INSTALL = "rightkit-gate --to tool --unit cargo:rightkit-credentials";
30
+ const HOST_PLATFORM = { darwin: "mac", win32: "win" };
31
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
32
+ const SHA256 = /^[0-9a-f]{64}$/;
33
+
34
+ export class CredentialProviderError extends Error {
35
+ constructor(code, message) { super(message); this.name = "CredentialProviderError"; this.code = code; }
36
+ }
37
+ const fail = (code, message) => { throw new CredentialProviderError(code, message); };
38
+
39
+ /** The provider platform of a `process.platform` value, or null when the host has no managed provider (Linux, CI). */
40
+ export const providerPlatformOf = (hostPlatform) => HOST_PLATFORM[hostPlatform] ?? null;
41
+ export const providerExecutableName = (platform) => (platform === "win" ? "rightkit-credentials.exe" : "rightkit-credentials");
42
+
43
+ const parse = (version) => { const m = SEMVER.exec(String(version ?? "")); return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null; };
44
+ const compare = (a, b) => { for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] - b[i]; return 0; };
45
+ const checkPlatform = (platform) => { if (!CREDENTIAL_PROVIDER_PLATFORMS.includes(platform)) fail("RK_CRED_PROVIDER_PLATFORM", `platform must be ${CREDENTIAL_PROVIDER_PLATFORMS.join(" or ")}, not ${JSON.stringify(platform)}`); };
46
+ const checkDataRoot = (dataRoot) => { if (!dataRoot || !path.isAbsolute(dataRoot)) fail("RK_CRED_PROVIDER_ROOT", "the managed data root must be an absolute path"); };
47
+ const checkVersion = (version) => {
48
+ const v = parse(version);
49
+ if (!v) fail("RK_CRED_PROVIDER_VERSION", `${JSON.stringify(version)} is not a rightkit-credentials version (major.minor.patch)`);
50
+ if (compare(v, parse(MIN_PROVIDER_VERSION)) < 0) fail("RK_CRED_PROVIDER_VERSION", `rightkit-credentials ${version} is below ${MIN_PROVIDER_VERSION}, the first release with the \`exec\` child-environment command updater signing needs`);
51
+ };
52
+
53
+ export const credentialProviderDir = ({ dataRoot, platform }) => { checkDataRoot(dataRoot); checkPlatform(platform); return path.join(dataRoot, CREDENTIAL_PROVIDER_DIR, platform); };
54
+ export const credentialProviderRecordFile = (args) => path.join(credentialProviderDir(args), "record.json");
55
+ export const credentialProviderFile = ({ dataRoot, platform, version }) => path.join(credentialProviderDir({ dataRoot, platform }), String(version), providerExecutableName(platform));
56
+
57
+ const sha256OfFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
58
+ const isInside = (child, parent) => { const rel = path.relative(parent, child); return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel); };
59
+
60
+ /** The identity probe: with no arguments the CLI answers a JSON refusal naming `--namespace` and exits 1, and reads no store. A file that answers anything else is not rightkit-credentials. */
61
+ function assertIsCredentialCli(file, spawn) {
62
+ const run = spawn(file, [], { encoding: "utf8", windowsHide: true, timeout: 30000, stdio: ["ignore", "pipe", "pipe"] });
63
+ let answer = null;
64
+ try { answer = JSON.parse(String(run?.stdout ?? "").trim()); } catch { /* not JSON */ }
65
+ if (run?.status !== 1 || answer?.ok !== false || !String(answer?.error ?? "").includes("--namespace")) {
66
+ fail("RK_CRED_PROVIDER_IDENTITY", `${file} did not answer like rightkit-credentials (exit ${run?.status ?? run?.error?.message ?? "none"}, ${JSON.stringify(String(run?.stdout ?? "").slice(0, 80))}); it has no --version, so its identity is its refusal of a call with no --namespace`);
67
+ }
68
+ }
69
+
70
+ function readRecord({ dataRoot, platform }) {
71
+ const file = credentialProviderRecordFile({ dataRoot, platform });
72
+ let meta;
73
+ try { meta = fs.lstatSync(file); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
74
+ if (meta.isSymbolicLink() || !meta.isFile()) fail("RK_CRED_PROVIDER_RECORD", `${file} is not a regular file`);
75
+ if (process.platform !== "win32" && (meta.mode & 0o022)) fail("RK_CRED_PROVIDER_RECORD", `${file} is writable by group or others`);
76
+ let record;
77
+ try { record = JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { fail("RK_CRED_PROVIDER_RECORD", `${file} is not JSON (${error.message})`); }
78
+ if (record?.schema !== CREDENTIAL_PROVIDER_SCHEMA || record.platform !== platform || record.providers === null || typeof record.providers !== "object" || Array.isArray(record.providers)) {
79
+ fail("RK_CRED_PROVIDER_RECORD", `${file} is not a ${CREDENTIAL_PROVIDER_SCHEMA} record for ${platform}`);
80
+ }
81
+ return { file, record };
82
+ }
83
+
84
+ /**
85
+ * Enrols a gate-built rightkit-credentials (`binary`, an absolute path) of `version`: a COPY under the managed directory, its sha256 recorded. Enrols on the
86
+ * host whose platform it is (the bytes are executed here for the identity probe). Returns `{ file, runnerFile, providerFile, entry, rotated }`.
87
+ */
88
+ export function enrollCredentialProvider({ dataRoot, platform, binary, version, rotate = false, spawn = spawnSync, hostPlatform = process.platform, now = () => new Date() }) {
89
+ checkPlatform(platform);
90
+ if (HOST_PLATFORM[hostPlatform] !== platform) fail("RK_CRED_PROVIDER_PLATFORM", `a ${platform} provider is enrolled on a ${platform} host (this is ${hostPlatform}): its bytes are executed to check what they are`);
91
+ checkVersion(version);
92
+ const target = credentialProviderFile({ dataRoot, platform, version });
93
+ if (!binary || !path.isAbsolute(String(binary))) fail("RK_CRED_PROVIDER_PATH", "the provider must be named by an absolute path");
94
+ let source; let meta;
95
+ try { source = fs.realpathSync.native(binary); meta = fs.statSync(source); } catch (error) { fail("RK_CRED_PROVIDER_PATH", `the provider ${binary} cannot be resolved (${error.code ?? error.message})`); }
96
+ if (!meta.isFile()) fail("RK_CRED_PROVIDER_PATH", `${source} is not a regular file`);
97
+ // The execute bit is the real file system's, so it is read only where that file system has one (never on Windows, whatever host is simulated).
98
+ if (platform !== "win" && process.platform !== "win32" && (meta.mode & 0o111) === 0) fail("RK_CRED_PROVIDER_PATH", `${source} is not executable`);
99
+ assertIsCredentialCli(source, spawn);
100
+ const existing = readRecord({ dataRoot, platform });
101
+ const had = existing?.record.providers?.[version] ?? null;
102
+ if (had && !rotate) fail("RK_CRED_PROVIDER_EXISTS", `${existing.file} already enrols rightkit-credentials ${version} (sha256 ${String(had.sha256).slice(0, 12)}); replacing it is a rotation (pass rotate)`);
103
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
104
+ const staging = `${target}.${crypto.randomUUID()}.tmp`;
105
+ try {
106
+ fs.copyFileSync(source, staging, fs.constants.COPYFILE_EXCL);
107
+ fs.chmodSync(staging, 0o755);
108
+ assertIsCredentialCli(staging, spawn);
109
+ fs.renameSync(staging, target);
110
+ } finally { fs.rmSync(staging, { force: true }); }
111
+ const entry = { path: target, sha256: sha256OfFile(target), version };
112
+ const record = { schema: CREDENTIAL_PROVIDER_SCHEMA, platform, providers: { ...(existing?.record.providers ?? {}), [version]: entry }, enrolledAt: now().toISOString() };
113
+ const file = credentialProviderRecordFile({ dataRoot, platform });
114
+ const tmp = `${file}.${crypto.randomUUID()}.tmp`;
115
+ try { fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, { flag: "wx", mode: 0o600 }); fs.renameSync(tmp, file); } finally { fs.rmSync(tmp, { force: true }); }
116
+ return { file, runnerFile: target, providerFile: target, entry, rotated: Boolean(had) };
117
+ }
118
+
119
+ /**
120
+ * The enrolled provider: the NEWEST recorded version, `{ file, sha256, version, source: "managed" }`, or a typed refusal. The file the record names must be the
121
+ * managed copy (inside the managed directory, not a link) and its bytes must still hash to the recorded sha256. It is never executed here and there is no
122
+ * fallback: a missing enrolment names the install.
123
+ */
124
+ export function resolveCredentialProvider({ dataRoot, platform }) {
125
+ checkPlatform(platform);
126
+ const read = readRecord({ dataRoot, platform });
127
+ const versions = Object.keys(read?.record.providers ?? {}).filter((v) => parse(v)).sort((a, b) => compare(parse(b), parse(a)));
128
+ if (!versions.length) fail("RK_CRED_PROVIDER_MISSING", `OS credential provider unavailable: rightkit-credentials >=${MIN_PROVIDER_VERSION} is not enrolled for ${platform} (${credentialProviderRecordFile({ dataRoot, platform })}); install it with \`${CREDENTIAL_PROVIDER_INSTALL}\``);
129
+ const version = versions[0];
130
+ const entry = read.record.providers[version];
131
+ const expected = credentialProviderFile({ dataRoot, platform, version });
132
+ if (entry.version !== version || !SHA256.test(String(entry.sha256)) || entry.path !== expected) fail("RK_CRED_PROVIDER_RECORD", `${read.file}: the entry for ${version} is malformed or names a file other than ${expected}`);
133
+ let meta;
134
+ try { meta = fs.lstatSync(expected); } catch (error) { fail("RK_CRED_PROVIDER_MISSING", `OS credential provider unavailable: the enrolled rightkit-credentials ${version} is gone (${expected}: ${error.code ?? error.message}); install it again with \`${CREDENTIAL_PROVIDER_INSTALL}\``); }
135
+ if (meta.isSymbolicLink() || !meta.isFile()) fail("RK_CRED_PROVIDER_RECORD", `${expected} is not a regular file`);
136
+ if (!isInside(fs.realpathSync.native(expected), fs.realpathSync.native(path.join(dataRoot, CREDENTIAL_PROVIDER_DIR)))) fail("RK_CRED_PROVIDER_RECORD", `${expected} resolves outside ${path.join(dataRoot, CREDENTIAL_PROVIDER_DIR)}`);
137
+ const actual = sha256OfFile(expected);
138
+ if (actual !== entry.sha256) fail("RK_CRED_PROVIDER_DIGEST", `the enrolled rightkit-credentials ${version} at ${expected} has sha256 ${actual}, not the recorded ${entry.sha256}; it changed after enrolment, so it is not run (install it again with \`${CREDENTIAL_PROVIDER_INSTALL}\`, then rotate)`);
139
+ return { file: expected, sha256: actual, version, source: "managed" };
140
+ }
141
+
142
+ /**
143
+ * The provider updater signing runs on a host: the enrolled one on a Mac or Windows host (its managed data root, `hostPlatform` decides), the bare PATH name
144
+ * only where no managed data root exists (Linux, CI). Returns `{ file, managed }`; a managed host with no usable enrolment throws the typed refusal.
145
+ */
146
+ export function updaterCredentialProvider({ env = process.env, hostPlatform = process.platform, dataRoot, resolve = resolveCredentialProvider } = {}) {
147
+ const platform = providerPlatformOf(hostPlatform);
148
+ if (!platform) return { file: "rightkit-credentials", managed: false };
149
+ return { file: resolve({ dataRoot: dataRoot ?? resolveManagedRoots({ env, platform: hostPlatform }).dataRoot, platform }).file, managed: true };
150
+ }
151
+
152
+ /** `node credential-provider.mjs enroll --platform mac|win --binary <built rightkit-credentials> --version <v> [--data-root <dir>] [--rotate]` or `show`. */
153
+ export function main(argv, { out = (m) => process.stdout.write(`${m}\n`), err = (m) => process.stderr.write(`${m}\n`), env = process.env, spawn, program = "credential-provider.mjs" } = {}) {
154
+ const [command, ...rest] = argv;
155
+ const opt = (name) => { const i = rest.indexOf(name); return i < 0 ? undefined : rest[i + 1]; };
156
+ try {
157
+ const platform = opt("--platform") ?? HOST_PLATFORM[process.platform];
158
+ const dataRoot = opt("--data-root") ?? resolveManagedRoots({ env }).dataRoot;
159
+ if (command === "enroll") {
160
+ const r = enrollCredentialProvider({ dataRoot, platform, binary: opt("--binary") && path.resolve(opt("--binary")), version: opt("--version"), rotate: rest.includes("--rotate"), ...(spawn ? { spawn } : {}) });
161
+ out(`enrolled rightkit-credentials ${r.entry.version} for ${platform} (sha256 ${r.entry.sha256}) at ${r.providerFile}${r.rotated ? " (rotation)" : ""}`);
162
+ return 0;
163
+ }
164
+ if (command === "show") {
165
+ const read = readRecord({ dataRoot, platform });
166
+ out(read ? JSON.stringify(read.record) : "no enrolment");
167
+ return read ? 0 : 1;
168
+ }
169
+ err(`usage: ${program} enroll --platform mac|win --binary <built rightkit-credentials> --version <v> [--data-root <dir>] [--rotate]\n ${program} show [--platform mac|win] [--data-root <dir>]`);
170
+ return 2;
171
+ } catch (error) { err(`${error.code ?? "error"}: ${error.message}`); return 1; }
172
+ }
173
+
174
+ if (process.argv[1] && (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })()) process.exit(main(process.argv.slice(2)));