@rightkit/release 0.6.13 → 0.6.14

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 (109) hide show
  1. package/README.md +49 -1
  2. package/adapter-env.mjs +165 -0
  3. package/android-ndk.mjs +5 -1
  4. package/android-release.mjs +73 -8
  5. package/app-check.mjs +22 -8
  6. package/app-closure.mjs +214 -0
  7. package/assemble-app-command.mjs +24 -7
  8. package/atomic-write.mjs +57 -0
  9. package/bin/darwin/rightkit-swap +0 -0
  10. package/bin/rightkit-gate.mjs +1190 -337
  11. package/broker-client.mjs +106 -0
  12. package/broker-exception.mjs +211 -0
  13. package/broker-handshake.mjs +160 -0
  14. package/broker-pins.mjs +27 -8
  15. package/broker-wire.mjs +262 -0
  16. package/build-launch.mjs +364 -12
  17. package/build-ownership.mjs +117 -49
  18. package/build-release.mjs +3 -2
  19. package/cache-command.mjs +7 -2
  20. package/candidate-build-receipt.mjs +96 -0
  21. package/candidate-receipt-command.mjs +29 -0
  22. package/cargo-contract.mjs +4 -3
  23. package/cargo-ledger.mjs +1 -1
  24. package/cargo-license-notices.mjs +2 -2
  25. package/cargo-shapes.mjs +332 -0
  26. package/cargo-target-shim.mjs +44 -5
  27. package/cargo-target.mjs +5 -0
  28. package/carried-adapter-run.mjs +31 -0
  29. package/carried-cargo-run.mjs +9 -0
  30. package/carried-cargo.mjs +237 -0
  31. package/carrier-census.mjs +124 -0
  32. package/class-a-launch-guard.mjs +266 -33
  33. package/cli/right-release.mjs +2 -0
  34. package/clippy-route.mjs +23 -11
  35. package/compile-inputs.mjs +9 -12
  36. package/control-plane.mjs +18 -0
  37. package/deps-cargo-lock.mjs +208 -0
  38. package/deps.mjs +5 -0
  39. package/deterministic-scans.mjs +273 -44
  40. package/development-signing.mjs +14 -1
  41. package/development.mjs +109 -24
  42. package/direct-origin.mjs +107 -0
  43. package/disk-command.mjs +1 -0
  44. package/disk-policy.mjs +18 -3
  45. package/freshness-census.mjs +206 -58
  46. package/gate-executor.mjs +30 -12
  47. package/gate-stages.mjs +93 -16
  48. package/github-release.mjs +2 -1
  49. package/github-upload-gate.mjs +2 -1
  50. package/install-tauri-authenticode.mjs +346 -0
  51. package/ios-dev.mjs +77 -23
  52. package/ios-package-recipe.mjs +100 -41
  53. package/linux-artifacts.mjs +17 -2
  54. package/linux-qualifier.mjs +83 -0
  55. package/local-publish.mjs +36 -14
  56. package/local-runtime.mjs +2 -2
  57. package/lock-identity.mjs +138 -0
  58. package/lsclean.sh +0 -0
  59. package/managed-cargo.mjs +151 -44
  60. package/managed-roots.mjs +130 -12
  61. package/media-sidecar-catalog.mjs +1 -1
  62. package/native-adapters.mjs +411 -0
  63. package/native-helpers.mjs +26 -18
  64. package/native-tool-planning.mjs +94 -0
  65. package/ownership-mark.mjs +182 -0
  66. package/pack-digest.mjs +122 -0
  67. package/package.json +12 -12
  68. package/platform-dir.mjs +14 -0
  69. package/pnpm-adapter-grammar.mjs +226 -0
  70. package/progress-control.mjs +7 -2
  71. package/provenance-real-receipt.pins.json +7 -0
  72. package/provenance-verify.mjs +262 -1
  73. package/publication-outcomes.mjs +21 -0
  74. package/publish-update.mjs +2 -1
  75. package/qa-journeys-prebuilt-run.mjs +18 -0
  76. package/qa-journeys-prebuilt.mjs +148 -0
  77. package/qa-snapshot-run.mjs +53 -0
  78. package/qa-snapshot.mjs +164 -0
  79. package/qualification-verify.mjs +144 -0
  80. package/qualify-reuse.mjs +38 -0
  81. package/recipe-build.mjs +16 -3
  82. package/release-qa-audit.mjs +2 -2
  83. package/release-qa-command.mjs +2 -1
  84. package/release-qa-receipt.mjs +3 -2
  85. package/release-state.mjs +3 -2
  86. package/release.mjs +14 -3
  87. package/rightkit-drift.mjs +27 -5
  88. package/rightkit-versions.json +14 -10
  89. package/rustright-names.mjs +32 -0
  90. package/scan-tool-enrolment.mjs +167 -0
  91. package/sign-windows.mjs +212 -112
  92. package/signing-files.mjs +35 -0
  93. package/signing-identity.mjs +7 -0
  94. package/snapshot-broker.mjs +528 -57
  95. package/snapshot-check.mjs +95 -11
  96. package/snapshot-metadata.mjs +123 -0
  97. package/snapshot-request.mjs +389 -96
  98. package/stage-binary.mjs +22 -4
  99. package/stage-commands.mjs +56 -13
  100. package/stage-sidecar.mjs +93 -21
  101. package/suite-signing-policy.mjs +657 -0
  102. package/suite-signing.mjs +223 -0
  103. package/swift-scratch.mjs +25 -0
  104. package/tauri-locked.mjs +25 -0
  105. package/test-command.mjs +164 -29
  106. package/unit-gate.mjs +201 -31
  107. package/upload-large.mjs +0 -0
  108. package/web-build.mjs +2 -2
  109. package/win-package-recipe.mjs +26 -2
package/development.mjs CHANGED
@@ -2,7 +2,9 @@ import { discoverBrokerSocket, resolveManagedRoots } from './managed-roots.mjs';
2
2
  import { spawnSync } from "./background-process.mjs";
3
3
  import { createHash } from "node:crypto";
4
4
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, lstatSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
5
- import { buildLaunch } from "./build-launch.mjs";
5
+ import { buildLaunch, createNativeRunner } from "./build-launch.mjs";
6
+ import { carriedOutputDir, carriedOutputDirsSince } from "./carried-cargo.mjs";
7
+ import { lockedTauriArgs } from "./tauri-locked.mjs";
6
8
  import path from "node:path";
7
9
  import { fileURLToPath } from "node:url";
8
10
  import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
@@ -17,8 +19,12 @@ import { stageDevelopmentBinaries, validateStageEntries } from "./stage-binary.m
17
19
  import { assertStableDesignatedRequirement, readBundleExecutable, requestedCiAdhocSigning, signMacosDevelopmentBundle } from "./development-signing.mjs";
18
20
  import { macosCodesignEnvironment } from "./codesign-environment.mjs";
19
21
  import { installTauriDevId, resolveTauriDevIdDescriptor, TAURI_DEVID_KIND } from "./install-tauri-devid.mjs";
22
+ import { TAURI_AUTHENTICODE_KIND, installTauriAuthenticode, resolveAuthenticodeDescriptor, validateAuthenticodeBuild } from "./install-tauri-authenticode.mjs";
23
+ import { signWindowsFiles, verifyWindowsFiles } from "./sign-windows.mjs";
24
+ import { assertSuiteSigningPolicy } from "./suite-signing-policy.mjs";
25
+ import { fsListRustFiles } from "./signing-files.mjs";
20
26
  import { ASSEMBLED_APP_KIND, installAssembledMac, installAssembledWindows, resolveAssembledInstall } from "./install-assembled-app.mjs";
21
- import { assertIosDevelopmentSession, installIosDevelopment, iosUiTestCommands, prepareIosDevelopment, prepareIosUiTests, validateIosDevelopment } from "./ios-dev.mjs";
27
+ import { assertIosDevelopmentSession, installIosDevelopment, iosDevelopmentApp, iosUiTestCommands, prepareIosDevelopment, prepareIosUiTests, runIosDevelopmentCommands, validateIosDevelopment } from "./ios-dev.mjs";
22
28
  import { packLinuxArtifacts, validateLinuxArchive } from "./linux-artifacts.mjs";
23
29
  import { buildAndEmbedNativeHelpers, nativeHelpersFromConfig } from "./native-helpers.mjs";
24
30
  import { androidDevelopmentNdkCommand, androidDevelopmentSerial, installAndroidDevelopment, validateAndroidDevelopment } from "./android-dev.mjs";
@@ -48,9 +54,12 @@ export function developmentEnvironment(env = process.env, platform = process.pla
48
54
  delete result.CARGO_INCREMENTAL;
49
55
  // A dev build is debug-signed: a release signing-properties path never reaches it.
50
56
  delete result.RIGHTKIT_ANDROID_SIGNING_PROPERTIES;
51
- const client = managedRuntimeClient(result);
57
+ // A direct (Linux) host never gets the managed agent-bin on PATH or a discovered socket, even when it DECLARES a broker route: managedRuntimeClient then returns the declared,
58
+ // pinned client, but the agent-bin directory and socket here come from environment-derived managed roots that nothing validated. Whatever the build needs from the broker
59
+ // goes through the executor's validated client (buildLaunch / createNativeRunner), never a PATH lookup, and a second endpoint variable would make the declared route ambiguous.
60
+ const client = directPlatform(platform) ? undefined : managedRuntimeClient(result, platform);
52
61
  if (client) {
53
- const { agentBin } = resolveManagedRoots({ env: result });
62
+ const { agentBin } = resolveManagedRoots({ env: result, platform });
54
63
  if (agentBin && existsSync(agentBin)) {
55
64
  const pathKey = Object.keys(result).find((key) => key.toLowerCase() === "path") ?? "PATH";
56
65
  result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
@@ -58,7 +67,7 @@ export function developmentEnvironment(env = process.env, platform = process.pla
58
67
  // Same default as the agent-bin `rightkit` shim: children that run the managed client directly
59
68
  // (resolveCommand("rustright"), build scripts) must not lose the broker endpoint.
60
69
  const socket = discoverBrokerSocket(result);
61
- if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
70
+ if (!result.RUSTRIGHT_BUILD_BROKER_SOCKET && !result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RUSTRIGHT_BUILD_BROKER_SOCKET = socket;
62
71
  }
63
72
  return result;
64
73
  }
@@ -84,6 +93,10 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
84
93
  if (platform !== "mac") throw new Error(`the ${TAURI_DEVID_KIND} development installer is for --platform mac`);
85
94
  resolveTauriDevIdDescriptor(target.install, { appRoot: root });
86
95
  if (!target.install.targetTriple && !target.install.bundleDirectory) throw new Error(`development.targets.${platform}.install requires targetTriple (or bundleDirectory) to find the built .app`);
96
+ } else if (target.install?.kind === TAURI_AUTHENTICODE_KIND) {
97
+ if (platform !== "win") throw new Error(`the ${TAURI_AUTHENTICODE_KIND} development installer is for --platform win`);
98
+ resolveAuthenticodeDescriptor(target.install, `development.targets.${platform}.install`);
99
+ validateAuthenticodeBuild(target.build, `development.targets.${platform}.build`, (rel) => { try { return readFileSync(path.resolve(root, rel), "utf8"); } catch { return null; } });
87
100
  } else if (target.install?.kind === ASSEMBLED_APP_KIND) {
88
101
  if (platform !== "mac" && platform !== "win") throw new Error(`the ${ASSEMBLED_APP_KIND} development installer is for --platform mac or win`);
89
102
  // The assembly (assembly.<platform>) and the install keys are validated together, before any build.
@@ -101,13 +114,18 @@ export function validateDevelopmentConfig(config, platform, { root = process.cwd
101
114
  }
102
115
 
103
116
  export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync, door = buildLaunch,
104
- codesignEnvironment = macosCodesignEnvironment, serial, uiTests = false }) {
117
+ codesignEnvironment = macosCodesignEnvironment, serial, uiTests = false, hostPlatform = process.platform, nativeHost = process.platform }) {
105
118
  // UI test journeys (`right-release test --platform ios`): the declared XCUITest journeys on the dev device, under the same guards.
106
119
  if (uiTests && platform !== "ios") throw new Error("UI test journeys run for --platform ios only");
107
120
  if (!new Set(["win", "mac", "linux", "android", "ios"]).has(platform)) throw new Error("--platform must be win, mac, linux, android or ios");
108
121
  // iOS: --dry-run validates the config and prints the plan on any host; everything else needs macOS with Xcode.
109
- if (!(platform === "ios" && dryRun)) assertNativeDevelopmentHost(platform);
122
+ // `nativeHost` is the kernel the build runs on (assertNativeDevelopmentHost), `hostPlatform` the host the suite signing policy judges: the same on a real host, apart only in a fixture that
123
+ // runs the Mac or Windows build mechanics on another kernel under a host the signing policy exempts.
124
+ if (!(platform === "ios" && dryRun)) assertNativeDevelopmentHost(platform, nativeHost);
110
125
  const target = validateDevelopmentConfig(config, platform, { root });
126
+ // One signing policy, enforced here whatever the config's format (the route probe reads JSON configs only; a legacy .mjs config is judged now): a dev
127
+ // target that would build ad hoc, unsigned or under another identifier is refused before it builds (rightkit#1303). Mac and Dell only.
128
+ if (!dryRun && !uiTests) assertSuiteSigningPolicy({ config, platform: hostPlatform, env, to: "dev", devPlatform: platform, buildOnly, listFiles: fsListRustFiles(root), readFile: (rel) => { try { return readFileSync(path.resolve(root, rel), "utf8"); } catch { return null; } } });
111
129
  // Inside the door's source split `root` is the snapshot of a commit (no git directory): the repository is the checkout the door
112
130
  // ran in, whose primary-checkout rule and execution policy still apply, and the commit is the split's.
113
131
  const split = readSourceSplit({ env, cwd: root });
@@ -115,7 +133,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
115
133
  assertPrimaryReleaseCheckout(repoRoot, env);
116
134
  if (dryRun && uiTests) {
117
135
  if (!target.uiTests) throw new Error("development.targets.ios.uiTests is not declared: list the journeys in uiTests.onlyTesting");
118
- return { built: false, installed: false, plan: { tests: iosUiTestCommands(target, { root, storage: { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, destinationId: "<device or simulator id>", resultBundle: "<fresh .xcresult per run>" }) } };
136
+ return { built: false, installed: false, plan: { tests: iosUiTestCommands(target, { root, destinationId: "<device or simulator id>" }) } };
119
137
  }
120
138
  if (dryRun) return { built: false, installed: false, plan: { ...(target.ndk ? { ndk: androidDevelopmentNdkCommand(target.ndk) } : {}), build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
121
139
  assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
@@ -138,15 +156,25 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
138
156
  const startedMs = Date.now();
139
157
  // Android: cargo-ndk (Cargo dev profile) fills the generated jniLibs, then the Gradle debug build packages it.
140
158
  if (target.ndk) { rmSync(target.ndk.jniLibs, { recursive: true, force: true }); execute(androidDevelopmentNdkCommand(target.ndk), { root, env: childEnv, dryRun, run, door, label: "native build" }); }
159
+ // RA-09: the iOS compile steps (xcodegen, xcodebuild build / build-for-testing / test-without-building) are broker adapter jobs of the production native runner, planned together before the first
160
+ // launch; there is no execute() of this file's own for them. The app's own non-native `prepare` commands are the only direct steps and keep going through the door. The built app and the
161
+ // result bundle are read back from the verified receipt's managed output roles.
162
+ const iosNative = (commands) => runIosDevelopmentCommands(commands, {
163
+ native: createNativeRunner({ cwd: root, env: childEnv }), root, env: childEnv,
164
+ direct: (command) => { execute(command, { root, env: childEnv, dryRun, run, door, label: "prepare" }); return { status: 0 }; },
165
+ });
141
166
  if (uiTests) {
142
- for (const command of iosPrepared.tests) execute(command, { root, env: childEnv, dryRun, run, door, label: "ui tests" });
143
- console.log(`right-release test: ios journeys passed (${target.uiTests.onlyTesting.join(", ")}); result bundle ${iosPrepared.resultBundle}`);
144
- return { built: true, installed: false, tested: true, resultBundle: iosPrepared.resultBundle };
167
+ const tested = iosNative(iosPrepared.tests);
168
+ console.log(`right-release test: ios journeys passed (${target.uiTests.onlyTesting.join(", ")}); result bundle ${tested.resultBundle ?? "(not reported)"}`);
169
+ return { built: true, installed: false, tested: true, resultBundle: tested.resultBundle };
145
170
  }
146
- for (const command of iosPrepared ? iosPrepared.build : target.build) execute(command, { root, env: childEnv, dryRun, run, door, label: "build" });
171
+ let built = null;
172
+ if (iosPrepared) built = iosNative(iosPrepared.build);
173
+ else for (const command of target.build) execute(command, { root, env: childEnv, dryRun, run, door, label: "build" });
174
+ if (iosPrepared?.install && !dryRun) iosPrepared.install.app = iosDevelopmentApp(target, built.outputs);
147
175
  // Dev and QA binaries run from one stable staged path, copied in place (no backup of the old copy), never from the
148
176
  // broker's per-generation target folder or a hashed dev-targets folder.
149
- const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform }) : [];
177
+ const staged = target.stage?.length ? stageDevelopmentBinaries(target.stage, { root, platform: platform === "win" ? "win32" : process.platform, managedOutputDirs: carriedOutputDirsSince(childEnv) }) : [];
150
178
  for (const file of staged) console.log(`right-release dev: staged ${file}`);
151
179
  // Linux: the declared files become one tarball per architecture plus the digest manifest a guest link pins (rightkit#1230).
152
180
  // Only from a clean checkout: an artifact others install must name the exact commit it was built from.
@@ -174,6 +202,22 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
174
202
  .then((outcome) => ({ built: true, installed: true, ...(staged.length ? { staged } : {}), devid: outcome }))
175
203
  .finally(() => lease.release());
176
204
  }
205
+ if (target.install.kind === TAURI_AUTHENTICODE_KIND) {
206
+ // The unsigned Windows installer is never run: this kind signs the exe, bundles it, signs the installer, then installs (install-tauri-authenticode.mjs).
207
+ const descriptor = resolveAuthenticodeDescriptor(target.install, `development.targets.${platform}.install`);
208
+ const bundleRoot = resolveBundleRoot(descriptor, { root, env: childEnv, runCargo: runCargoSync });
209
+ const debugDir = path.resolve(bundleRoot, "..", "..");
210
+ const bundleStartedMs = Date.now();
211
+ const authenticode = installTauriAuthenticode({ descriptor, debugDir, appRoot: root, releaseConfig: config, deps: {
212
+ signWindows: (files) => signWindowsFiles(files),
213
+ verifyWindows: (files) => verifyWindowsFiles(files),
214
+ bundle: (command) => execute(command, { root, env: childEnv, dryRun, run, door, label: "bundle" }),
215
+ findInstaller: () => resolveTauriBundleDirectory(descriptor, { root, platform, env: childEnv, sinceMs: bundleStartedMs }).installer,
216
+ install: (installer) => installWindowsDevelopment(descriptor, installer, { root, env: childEnv, run, debugDir }),
217
+ installedExe: () => path.resolve(root, descriptor.expectedInstalledPath),
218
+ } });
219
+ return { built: true, installed: true, ...(staged.length ? { staged } : {}), authenticode };
220
+ }
177
221
  if (target.install.kind === ASSEMBLED_APP_KIND) {
178
222
  const { spec, descriptor } = resolveAssembledInstall({ config, platform });
179
223
  if (platform === "win") {
@@ -220,7 +264,15 @@ function install(descriptor, { root, platform, config, env, run, door, sinceMs =
220
264
  if (platform === "win") {
221
265
  if (!descriptor.expectedInstalledPath) throw new Error("Windows Tauri installer requires expectedInstalledPath for installed verification");
222
266
  const { bundleRoot, installer: app } = resolveTauriBundleDirectory(descriptor, { root, platform, env, sinceMs });
223
- const built = path.resolve(bundleRoot, "..", "..", path.basename(descriptor.expectedInstalledPath));
267
+ return installWindowsDevelopment(descriptor, app, { root, env, run, debugDir: path.resolve(bundleRoot, "..", "..") });
268
+ }
269
+ return installMacosDevelopment(descriptor, { root, platform, config, env, run });
270
+ }
271
+
272
+ /** Run the silent NSIS installer of a Windows dev build in place and check the installed exe is this build's. */
273
+ function installWindowsDevelopment(descriptor, app, { root, env, run, debugDir }) {
274
+ {
275
+ const built = path.resolve(debugDir, path.basename(descriptor.expectedInstalledPath));
224
276
  assertNonEmptyExecutable(built);
225
277
  // Tauri's silent installer force-kills a running copy of the app (utils.nsh: IfSilent kill). When it
226
278
  // was running, pass /R so the installer relaunches it as the user (RunAsUser, outside this process tree).
@@ -234,8 +286,10 @@ function install(descriptor, { root, platform, config, env, run, door, sinceMs =
234
286
  if (result.status !== 0) throw new Error(`development installer failed (${result.status}): ${app}`);
235
287
  const installed = path.resolve(root, descriptor.expectedInstalledPath);
236
288
  assertInstalledExecutableMatches(built, installed);
237
- return;
238
289
  }
290
+ }
291
+
292
+ function installMacosDevelopment(descriptor, { root, platform, config, env, run }) {
239
293
  const app = resolveTauriArtifact(descriptor, { root, platform, env });
240
294
  // One fixed install path per app: macOS keys privacy grants to it. No /Applications/<name> guess.
241
295
  if (!descriptor.destination) throw new DevelopmentInstallError("DEV_INSTALL_DESTINATION_REQUIRED", "development.targets.mac.install.destination is required: the one fixed path this app installs to (for example /Applications/<Product>.app)");
@@ -301,11 +355,36 @@ export function resolveTauriArtifact(descriptor, { root, platform, env = process
301
355
  return resolveTauriBundleDirectory(descriptor, { root, platform, env, runCargo }).installer;
302
356
  }
303
357
 
358
+ /**
359
+ * GB-60 (GATE output pinning; E16/E19): a build whose compile is receipt-managed (a broker host's carried build) writes its outputs under the broker's
360
+ * output directory a signed receipt named, never under the source tree or the snapshot. A configured path that reads "target/..." (relative to the Cargo
361
+ * workspace) is therefore migrated EXPLICITLY onto that directory; a source-relative output path that is not under a `target` directory cannot hold what that
362
+ * build wrote and is refused with a typed error. It is never silently ignored, never resolved to a stale same-name file in the snapshot, and never
363
+ * replaced by a scan for the newest match. `declared` is the configured path relative to `root`; `manifestDir` the Cargo manifest's directory relative to it.
364
+ */
365
+ export function receiptRelativeOutputPath({ declared, root, manifestDir = ".", managedOutputDir, label }) {
366
+ const relative = path.relative(path.resolve(root), path.resolve(root, declared)).split(path.sep).join("/");
367
+ const segments = relative.split("/").filter(Boolean);
368
+ const manifestParts = String(manifestDir).split(/[\\/]/).filter((part) => part && part !== ".");
369
+ const at = segments.findIndex((segment, index) => segment === "target" && segments.slice(0, index).every((part, i) => part === manifestParts[i]));
370
+ if (at < 0) {
371
+ const error = new Error(`${label} ${JSON.stringify(declared)} is a source-relative path, but this build is receipt-managed: its outputs are under ${managedOutputDir}, not the source tree or the snapshot. Declare it relative to the Cargo target directory (target/<triple>/debug/bundle/...), or omit it so the receipt's directory is used`);
372
+ error.code = "DEV_OUTPUT_SOURCE_RELATIVE";
373
+ throw error;
374
+ }
375
+ return path.join(managedOutputDir, ...segments.slice(at + 1));
376
+ }
377
+
304
378
  export function resolveTauriBundleDirectory(descriptor, { root, platform, env = process.env, runCargo = runCargoSync, sinceMs = null }) {
305
- const bundleRoot = descriptor.bundleDirectory
306
- ? path.resolve(root, descriptor.bundleDirectory)
307
- : resolveBundleRoot(descriptor, { root, env, runCargo });
308
- if (descriptor.bundleDirectory && !inside(root, bundleRoot)) throw new Error("Tauri bundleDirectory must stay inside app root");
379
+ let bundleRoot;
380
+ if (descriptor.bundleDirectory) {
381
+ const declared = path.resolve(root, descriptor.bundleDirectory);
382
+ if (!inside(root, declared)) throw new Error("Tauri bundleDirectory must stay inside app root");
383
+ const managed = descriptor.manifestPath ? carriedOutputDir(env, path.resolve(root, descriptor.manifestPath), { preferNested: true }) : null;
384
+ bundleRoot = managed
385
+ ? receiptRelativeOutputPath({ declared: descriptor.bundleDirectory, root, manifestDir: path.dirname(descriptor.manifestPath), managedOutputDir: managed, label: "Tauri bundleDirectory" })
386
+ : declared;
387
+ } else bundleRoot = resolveBundleRoot(descriptor, { root, env, runCargo });
309
388
  const entries = readdirSync(bundleRoot, { withFileTypes: true });
310
389
  const candidates = platform === "mac"
311
390
  ? entries.filter((entry) => entry.isDirectory() && entry.name === `${descriptor.productName}.app`).map((entry) => path.join(bundleRoot, entry.name))
@@ -345,6 +424,10 @@ function assertNonEmptyExecutable(file) { if (!existsSync(file) || !statSync(fil
345
424
  function resolveBundleRoot(descriptor, { root, env, runCargo }) {
346
425
  if (!descriptor.manifestPath) throw new Error("Tauri installer requires manifestPath when bundleDirectory is omitted");
347
426
  const manifest = path.resolve(root, descriptor.manifestPath);
427
+ // The bundle root is under the output directory a signed receipt of this stage named, not under a read-only metadata call
428
+ // (the compile the Tauri CLI made nested under the build step's carried adapter job, from the broker's signed list of its children).
429
+ const fromReceipt = carriedOutputDir(env, manifest, { preferNested: true });
430
+ if (fromReceipt) return path.join(fromReceipt, descriptor.targetTriple, cargoProfileDirectory(DEVELOPMENT_PROFILE), "bundle", platformBundleKind(descriptor));
348
431
  const cwd = process.cwd();
349
432
  let metadata;
350
433
  try {
@@ -353,7 +436,9 @@ function resolveBundleRoot(descriptor, { root, env, runCargo }) {
353
436
  } finally { process.chdir(cwd); }
354
437
  if (metadata.error || metadata.status !== 0) throw new Error(`RightKit Cargo metadata failed (${metadata.status ?? "unavailable"})${metadata.error ? `: ${metadata.error.message}` : ""}`);
355
438
  let targetDirectory;
356
- try { targetDirectory = JSON.parse(metadata.stdout).target_directory; } catch { throw new Error("RightKit Cargo metadata returned invalid JSON"); }
439
+ // A carried call (a snapshot stage's, carried-cargo.mjs) reports the signed receipt's output directory, not what metadata prints.
440
+ if (typeof metadata.outputDir === "string") targetDirectory = metadata.outputDir;
441
+ else try { targetDirectory = JSON.parse(metadata.stdout).target_directory; } catch { throw new Error("RightKit Cargo metadata returned invalid JSON"); }
357
442
  if (!targetDirectory || !path.isAbsolute(targetDirectory)) throw new Error("Cargo metadata omitted absolute target_directory");
358
443
  return path.join(targetDirectory, descriptor.targetTriple, cargoProfileDirectory(DEVELOPMENT_PROFILE), "bundle", platformBundleKind(descriptor));
359
444
  }
@@ -364,7 +449,7 @@ function execute(command, { root, env, dryRun, run, label, door = null }) {
364
449
  validateCommand(command, `development ${label}`);
365
450
  const cwd = path.resolve(root, command.cwd ?? ".");
366
451
  if (!inside(root, cwd)) throw new Error(`development ${label} cwd must stay inside app root`);
367
- const args = (command.args ?? []).map(String);
452
+ const args = lockedTauriArgs((command.args ?? []).map(String), env);
368
453
  if (dryRun) return { cmd: command.cmd, args, cwd };
369
454
  // A pnpm step through the door is the broker's rightkit-pnpm job (or plain pnpm on a direct host), launched by name: the door never uses
370
455
  // the node entry resolveCommand would find, so resolving pnpm's JavaScript CLI here would only fail a host that does not need it.
@@ -375,8 +460,8 @@ function execute(command, { root, env, dryRun, run, label, door = null }) {
375
460
  // is the rightkit-pnpm broker job (by its own name, not the node entry resolveCommand finds), anything else as resolved.
376
461
  // A configured command can run code regardless of its label, so installation commands use the door too.
377
462
  const own = resolved.command === process.execPath && resolved.args[0] === OWN_RIGHT_RELEASE;
378
- const launch = own || !door ? resolved : { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, cwd, exists: existsSync }) };
379
- const result = run(launch.command, launch.args, { cwd, env: launch.env ?? env, stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
463
+ const launch = own || !door ? resolved : { ...resolved, ...door(command.cmd === "pnpm" ? "pnpm" : resolved.command, command.cmd === "pnpm" ? args : resolved.args, { env: resolved.env ?? env, stepEnv: command.env, cwd, exists: existsSync }) };
464
+ const result = run(launch.command, launch.args, { cwd, env: launch.env && launch.env !== resolved.env ? { ...(resolved.env ?? env), ...launch.env } : (launch.env ?? env), stdio: "inherit", windowsHide: true, shell: false, timeout: command.timeoutMs });
380
465
  if (result.status !== 0) throw new Error(`development ${label} command failed (${result.status}): ${command.name ?? command.cmd}`);
381
466
  }
382
467
  /** This package's own right-release entry: the door's RightKit, whatever the app's node_modules holds. */
@@ -403,7 +488,7 @@ export function resolveCommand(command, args, { root, env = process.env, platfor
403
488
  const client = managedRuntimeClient(env);
404
489
  if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
405
490
  const socket = discoverBrokerSocket(env);
406
- const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : undefined;
491
+ const childEnv = !env.RUSTRIGHT_BUILD_BROKER_SOCKET && !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: socket } : undefined;
407
492
  return { ...managedRuntimeCommand(client, args), ...(childEnv ? { env: childEnv } : {}) };
408
493
  }
409
494
  return { command, args };
@@ -0,0 +1,107 @@
1
+ // GB-29 (GATE-CONTRACT GC-L01; R-L, A-L, P-L; E3, E4, E5): direct Linux origin trust, with no broker, no new service, no custom signature scheme and no fabricated broker receipt.
2
+ //
3
+ // A Linux origin runs the gate direct. Its compile and package stages leave DIRECT-ORIGIN records: the gate's own evidence (commit, the snapshot tree at start and at exit, the
4
+ // command identity, the output digest), never presented as, and never carrying the fields of, a broker's signed receipt. The record is NOT authority: it is a claim a verifier
5
+ // re-derives. The hosted side recomputes, from the dispatched commit alone, everything it can (the real git tree, the unit's version, the unit's content digest, its own full gate) and
6
+ // admits only a record whose every bound field equals ITS value; what it cannot recompute (that a local compile ran) is recorded as the origin's claim and grants nothing. The
7
+ // candidate identity (repository, commit, tree, unit, version, registry and the exact bytes) is what dispatch and publication join or reject on, so a replay of another candidate, a
8
+ // different artifact under one identity, a broker-shaped or non-Linux record, a stale tree or a digest the hosted pack does not reproduce are each refused by name.
9
+ //
10
+ // Activation is NOT here: widening the publish workflow's `admission` input beyond `broker` is the reserved publication authorization (GC-T22), so this verifier is the model and its tests,
11
+ // and the qualification a direct Linux origin writes (unit-gate.mjs) is verified by it locally exactly as the hosted side will, before anything could be dispatched.
12
+ import { createHash } from "node:crypto";
13
+
14
+ export const DIRECT_ORIGIN_SCHEMA = "rightkit.direct-origin/1";
15
+ /** The stages a direct origin evidences (the attested stages of a broker host). */
16
+ export const DIRECT_ORIGIN_STAGES = Object.freeze(["compile", "package"]);
17
+ export const DIRECT_ORIGIN_REFUSED = "RK_DIRECT_ORIGIN_REFUSED";
18
+ /** The repository whose units a direct origin releases. */
19
+ export const RIGHTKIT_REPOSITORY = "bogusyogi/rightkit";
20
+ /** Fields only a broker's signed job receipt or a broker request carries: a direct record that has any is a fabricated broker receipt and is refused. */
21
+ export const BROKER_ONLY_FIELDS = Object.freeze(["verdict", "refreshed", "reusedFrom", "provenance", "signature", "payload", "jobId", "requestId", "brokerVersion", "keyId", "nonce"]);
22
+ const RECORD_KEYS = ["schema", "executor", "repository", "commit", "tree", "unit", "registry", "artifact", "stages", "candidate"];
23
+ const STAGE_KEYS = ["stage", "ok", "commit", "tree", "treeAtStart", "treeAtExit", "command", "outputSha256"];
24
+ const SHA256 = /^[0-9a-f]{64}$/;
25
+ const SHA1 = /^[0-9a-f]{40}$/;
26
+
27
+ const sha256 = (text) => createHash("sha256").update(text).digest("hex");
28
+ const canonical = (value) => (Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : value !== null && typeof value === "object" ? `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}` : JSON.stringify(value));
29
+ const refuse = (message) => { throw Object.assign(new Error(`${DIRECT_ORIGIN_REFUSED}: ${message}`), { code: DIRECT_ORIGIN_REFUSED }); };
30
+ const same = (a, b) => canonical(a) === canonical(b);
31
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
32
+
33
+ /** The identity dispatch and publication join or reject on: what a candidate IS, independent of who built it or when. */
34
+ export function candidateIdentity({ repository, commit, tree, unit, registry, artifact }) {
35
+ return sha256(canonical({ repository, commit, tree, unit: { kind: unit.kind, name: unit.name, version: unit.version }, registry, artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 ?? null }));
36
+ }
37
+
38
+ /** One stage's evidence on a direct origin: the gate's own facts, taken after the stage guard confirmed the snapshot was the commit's bytes at start and at exit. */
39
+ export function directStageEvidence({ stage, commit, tree, treeAtStart = tree, treeAtExit = tree, command, outputSha256 = null }) {
40
+ return { stage, ok: true, commit, tree, treeAtStart, treeAtExit, command, outputSha256 };
41
+ }
42
+
43
+ /** The record a direct Linux origin writes into its qualification (`directOrigin`). `stages` is `{ compile, package }` of directStageEvidence. */
44
+ export function buildDirectOrigin({ repository, commit, tree, unit, registry, platform, artifact, stages }) {
45
+ const record = { schema: DIRECT_ORIGIN_SCHEMA, executor: { kind: "direct", platform }, repository, commit, tree, unit: { kind: unit.kind, name: unit.name, version: unit.version }, registry, artifact: { artifactSha256: artifact.artifactSha256, contentSha256: artifact.contentSha256 ?? null }, stages: DIRECT_ORIGIN_STAGES.map((stage) => stages[stage]) };
46
+ return { ...record, candidate: candidateIdentity({ repository, commit, tree, unit, registry, artifact: record.artifact }) };
47
+ }
48
+
49
+ function noBrokerFields(value, where) {
50
+ if (Array.isArray(value)) { value.forEach((item, i) => noBrokerFields(item, `${where}[${i}]`)); return; }
51
+ if (!isObject(value)) return;
52
+ for (const [key, inner] of Object.entries(value)) {
53
+ if (BROKER_ONLY_FIELDS.includes(key)) refuse(`${where}.${key} is a broker receipt field: a direct origin has no broker and a direct record never carries or imitates one`);
54
+ noBrokerFields(inner, `${where}.${key}`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The verifier (hosted side, and the origin's own self-check). `expected` is what the verifier derives independently:
60
+ * `{ repository, commit, tree (the real git tree of the commit), unit: { kind, name, version }, registry, contentSha256 (the unit's content digest recomputed from the commit; required for an
61
+ * npm unit), artifactSha256 (a crate's archive digest, when recomputed) }`. Throws RK_DIRECT_ORIGIN_REFUSED naming the first thing that is not so; returns `{ candidate }`.
62
+ */
63
+ export function verifyDirectOrigin(record, expected) {
64
+ if (!isObject(record)) refuse("the direct-origin record is not an object");
65
+ noBrokerFields(record, "directOrigin");
66
+ if (!same(Object.keys(record).sort(), [...RECORD_KEYS].sort())) refuse(`the record has unexpected or missing keys (${Object.keys(record).sort().join(", ")})`);
67
+ if (record.schema !== DIRECT_ORIGIN_SCHEMA) refuse(`schema ${JSON.stringify(record.schema)} is not ${DIRECT_ORIGIN_SCHEMA}`);
68
+ // Mac and the Dell always run through their broker; a direct record is a Linux origin's and nothing else's.
69
+ if (!isObject(record.executor) || !same(Object.keys(record.executor).sort(), ["kind", "platform"]) || record.executor.kind !== "direct" || record.executor.platform !== "linux") refuse(`the executor ${JSON.stringify(record.executor)} is not a direct Linux origin (Mac and Dell builds are broker-attested, never direct)`);
70
+ if (record.repository !== expected.repository) refuse(`the record is for repository ${record.repository}, not ${expected.repository}`);
71
+ if (!SHA1.test(record.commit ?? "") || record.commit !== expected.commit) refuse(`the record is for commit ${String(record.commit).slice(0, 12)}, not ${String(expected.commit).slice(0, 12)}`);
72
+ if (!SHA1.test(record.tree ?? "") || record.tree !== expected.tree) refuse(`the record's tree ${String(record.tree).slice(0, 12)} is not the commit's real tree ${String(expected.tree).slice(0, 12)}`);
73
+ if (!isObject(record.unit) || record.unit.kind !== expected.unit.kind || record.unit.name !== expected.unit.name || record.unit.version !== expected.unit.version) refuse(`the record covers ${JSON.stringify(record.unit)}, not ${JSON.stringify(expected.unit)}`);
74
+ if (record.registry !== expected.registry) refuse(`the record is for registry ${record.registry}, not ${expected.registry}`);
75
+ const artifact = record.artifact;
76
+ if (!isObject(artifact) || !SHA256.test(artifact.artifactSha256 ?? "")) refuse("the record names no artifact sha256");
77
+ if (expected.unit.kind === "npm") {
78
+ if (!SHA256.test(expected.contentSha256 ?? "")) refuse("the verifier has no recomputed content digest for this npm unit");
79
+ if (artifact.contentSha256 !== expected.contentSha256) refuse(`the record's content sha256 ${artifact.contentSha256} is not the ${expected.contentSha256} the verifier recomputed from the commit`);
80
+ } else if (expected.artifactSha256 !== undefined && artifact.artifactSha256 !== expected.artifactSha256) refuse(`the record's artifact sha256 ${artifact.artifactSha256} is not the ${expected.artifactSha256} the verifier recomputed`);
81
+ if (!Array.isArray(record.stages) || record.stages.length !== DIRECT_ORIGIN_STAGES.length) refuse(`the record carries ${Array.isArray(record.stages) ? record.stages.length : "no"} stage records, not ${DIRECT_ORIGIN_STAGES.join(" and ")}`);
82
+ record.stages.forEach((stage, i) => {
83
+ const name = DIRECT_ORIGIN_STAGES[i];
84
+ if (!isObject(stage) || !same(Object.keys(stage).sort(), [...STAGE_KEYS].sort())) refuse(`the ${name} stage record has unexpected or missing keys`);
85
+ if (stage.stage !== name || stage.ok !== true) refuse(`stage ${i + 1} is ${JSON.stringify(stage.stage)} ok=${stage.ok}, not a passed ${name}`);
86
+ if (stage.commit !== expected.commit || stage.tree !== expected.tree) refuse(`the ${name} stage is for another commit or tree`);
87
+ // The snapshot was the commit's bytes when the stage started and when it ended: a stale or mutated tree is no evidence.
88
+ if (stage.treeAtStart !== expected.tree || stage.treeAtExit !== expected.tree) refuse(`the ${name} stage's snapshot tree changed (start ${String(stage.treeAtStart).slice(0, 12)}, exit ${String(stage.treeAtExit).slice(0, 12)}, commit ${expected.tree.slice(0, 12)})`);
89
+ if (typeof stage.command !== "string" || !stage.command) refuse(`the ${name} stage names no command`);
90
+ if (name === "package" && stage.outputSha256 !== artifact.artifactSha256) refuse("the package stage's output is not the record's artifact");
91
+ });
92
+ const candidate = candidateIdentity({ repository: expected.repository, commit: expected.commit, tree: expected.tree, unit: expected.unit, registry: expected.registry, artifact });
93
+ if (record.candidate !== candidate) refuse("the record's candidate identity is not the one its own fields give (a replayed or edited record)");
94
+ return { candidate };
95
+ }
96
+
97
+ /**
98
+ * Dispatch and publication join or reject on the candidate identity (GC-T21): the same identity and bytes JOIN the existing claim; a different candidate or different bytes for the same
99
+ * dispatch key (repository, commit, unit, version, registry) REJECT; no claim is created here. `existing` is the claim already held for the key, or null.
100
+ */
101
+ export function reconcileClaim(existing, record) {
102
+ const key = sha256(canonical({ repository: record.repository, commit: record.commit, unit: record.unit, registry: record.registry }));
103
+ if (existing === null || existing === undefined) return { action: "claim", key, candidate: record.candidate };
104
+ if (existing.key !== key) refuse("the existing claim is for another dispatch key");
105
+ if (existing.candidate === record.candidate && existing.artifactSha256 === record.artifact.artifactSha256) return { action: "join", key, candidate: record.candidate };
106
+ refuse(`a claim for this dispatch key already binds candidate ${String(existing.candidate).slice(0, 12)} (artifact ${String(existing.artifactSha256).slice(0, 12)}); conflicting bytes are rejected, never replaced`);
107
+ }
package/disk-command.mjs CHANGED
@@ -822,6 +822,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
822
822
  if (!purged) {
823
823
  purged = purgeHoldings({ policy, platform, env, ...deps.purge });
824
824
  stdout(`purged ${purged.results.filter((r) => r.ok).length} holding dir(s), ${formatBytes(purged.purgedBytes)}`);
825
+ for (const r of purged.results.filter((x) => !x.ok)) stdout(`purge FAILED: ${r.path}: ${r.error}`);
825
826
  }
826
827
  const after = check();
827
828
  const warn = after.warn;
package/disk-policy.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync, appendFileSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, closeSync, cpSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, statfsSync, appendFileSync, writeFileSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { spawnSync } from "./background-process.mjs";
6
+ import { brokerClientInvocation } from "./broker-client.mjs";
6
7
 
7
8
  export const DISK_EXIT_WARNING = 3;
8
9
  export const DEFAULT_POLICY_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), "disk-policy.json");
@@ -339,7 +340,8 @@ export const BUDGET_PARTS = ["targets", "sccache", "cargoHome", "toolchains", "t
339
340
  * missing, fails, times out or answers with anything else. Nothing is cached and no shell is used. */
340
341
  export function readRustrightBudget({ run = spawnSync } = {}) {
341
342
  try {
342
- const r = run("rustright", ["doctor", "--json", "--no-scan"], { encoding: "utf8", timeout: 20_000, shell: false, maxBuffer: 1 << 24 });
343
+ const launch = brokerClientInvocation("rustright", ["doctor", "--json", "--no-scan"], { encoding: "utf8", timeout: 20_000, shell: false, maxBuffer: 1 << 24 }, { probe: run });
344
+ const r = run(launch.command, launch.args, launch.options);
343
345
  if (!r || r.error || r.status !== 0) return null;
344
346
  const budget = JSON.parse(String(r.stdout)).buildBudget;
345
347
  if (!budget || typeof budget !== "object") return null;
@@ -1029,7 +1031,20 @@ function newestWriteMs(dir) {
1029
1031
  * moved into or written in the dir within the last `days` either, so every item is held at least `days` even though the
1030
1032
  * folder name carries only a date. Only dirs this tool created: the base must carry the holding marker (or, for the legacy
1031
1033
  * heavy dir, the tool's own log), the child must be a real directory named YYYY-MM-DD, never a symlink. */
1032
- export function purgeHoldings({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), days = policy.holdingDays ?? 3, dryRun = false, bases, remove = (p) => rmSync(p, { recursive: true, force: true }), size = walkSize } = {}) {
1034
+ /** Remove a held tree whose items arrived read-only (frozen sources, Go module caches): make every real directory owner-writable
1035
+ * first, never following symlinks, since `rmSync` cannot unlink entries inside a directory it may not write. */
1036
+ function removeHeld(dir) {
1037
+ const walk = (d) => {
1038
+ try { chmodSync(d, lstatSync(d).mode | 0o700); } catch { return; }
1039
+ let names = [];
1040
+ try { names = readdirSync(d); } catch { return; }
1041
+ for (const n of names) { const c = path.join(d, n); try { if (lstatSync(c).isDirectory()) walk(c); } catch { /* vanished */ } }
1042
+ };
1043
+ walk(dir);
1044
+ rmSync(dir, { recursive: true, force: true });
1045
+ }
1046
+
1047
+ export function purgeHoldings({ policy, platform = process.platform, env = process.env, home = os.homedir(), now = Date.now(), days = policy.holdingDays ?? 3, dryRun = false, bases, remove = removeHeld, size = walkSize } = {}) {
1033
1048
  const key = platformKey(platform);
1034
1049
  const legacy = policy.heavyVolume?.[key];
1035
1050
  const list = bases ?? [...knownHoldingBases({ policy, platform, env, home }), ...(legacy ? [legacy] : [])];