@rightkit/release 0.6.13 → 0.6.15

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 (111) 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/rightkit-gate.mjs +1193 -337
  10. package/broker-client.mjs +106 -0
  11. package/broker-exception.mjs +211 -0
  12. package/broker-handshake.mjs +160 -0
  13. package/broker-pins.mjs +27 -8
  14. package/broker-wire.mjs +262 -0
  15. package/build-launch.mjs +364 -12
  16. package/build-ownership.mjs +117 -49
  17. package/build-release.mjs +3 -2
  18. package/cache-command.mjs +7 -2
  19. package/candidate-build-receipt.mjs +96 -0
  20. package/candidate-receipt-command.mjs +29 -0
  21. package/cargo-contract.mjs +4 -3
  22. package/cargo-ledger.mjs +1 -1
  23. package/cargo-license-notices.mjs +2 -2
  24. package/cargo-shapes.mjs +332 -0
  25. package/cargo-target-shim.mjs +44 -5
  26. package/cargo-target.mjs +5 -0
  27. package/carried-adapter-run.mjs +31 -0
  28. package/carried-cargo-run.mjs +9 -0
  29. package/carried-cargo.mjs +237 -0
  30. package/carrier-census.mjs +124 -0
  31. package/class-a-launch-guard.mjs +266 -33
  32. package/cli/right-release.mjs +2 -0
  33. package/clippy-route.mjs +23 -11
  34. package/compile-inputs.mjs +9 -12
  35. package/control-plane.mjs +18 -0
  36. package/deps-cargo-lock.mjs +208 -0
  37. package/deps.mjs +5 -0
  38. package/deterministic-scans.mjs +273 -44
  39. package/development-signing.mjs +14 -1
  40. package/development.mjs +112 -26
  41. package/direct-origin.mjs +107 -0
  42. package/disk-command.mjs +1 -0
  43. package/disk-policy.mjs +18 -3
  44. package/freshness-census.mjs +206 -58
  45. package/gate-executor.mjs +30 -12
  46. package/gate-stages.mjs +93 -16
  47. package/github-release.mjs +2 -1
  48. package/github-upload-gate.mjs +2 -1
  49. package/install-tauri-authenticode.mjs +346 -0
  50. package/ios-dev.mjs +77 -23
  51. package/ios-package-recipe.mjs +100 -41
  52. package/linux-artifacts.mjs +17 -2
  53. package/linux-qualifier.mjs +83 -0
  54. package/local-publish.mjs +36 -14
  55. package/local-runtime.mjs +2 -2
  56. package/lock-identity.mjs +138 -0
  57. package/lsclean.sh +0 -0
  58. package/managed-cargo.mjs +161 -44
  59. package/managed-roots.mjs +130 -12
  60. package/media-sidecar-catalog.mjs +1 -1
  61. package/native-adapters.mjs +411 -0
  62. package/native-helpers.mjs +26 -18
  63. package/native-tool-planning.mjs +94 -0
  64. package/ownership-mark.mjs +182 -0
  65. package/pack-digest.mjs +122 -0
  66. package/package.json +12 -12
  67. package/pipeline-receipts.mjs +302 -302
  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 +15 -11
  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
  110. package/bin/darwin/rightkit-swap +0 -0
  111. package/bin/darwin/rightkit-swap.sign.json +0 -6
@@ -6,6 +6,9 @@
6
6
  // runBrokerCargo one cargo call reached by a test on the Mac, through the runtime wrapper's broker mode: its own fresh
7
7
  // nonce and carrier, `primaryCwd` from the gate's snapshot-to-checkout map, `callingGate` inherited and never
8
8
  // forged, the carrier removed after the call, the signed receipt verified, and a ledger record either way.
9
+ // carryBrokerAdapter one pnpm, node or QA step of a snapshot stage (a broker ADAPTER job, rustright#190 snapshot-adapter-v1), carried
10
+ // the same way: its own carrier and nonce, the cwd of the primary checkout that matches the step's snapshot cwd,
11
+ // the signed receipt and its signed list of nested children (the Tauri CLI's compiles) verified, a ledger record.
9
12
  // verifyBrokerLedger the gate's own re-verification of every ledgered call from the broker's signed receipt, with the gate's
10
13
  // own commit, snapshot, ownership handle and pins (never the wrapper's say-so), and one use per carrier.
11
14
  import crypto from "node:crypto";
@@ -13,31 +16,64 @@ import fs from "node:fs";
13
16
  import os from "node:os";
14
17
  import path from "node:path";
15
18
  import { spawnSync } from "./background-process.mjs";
19
+ import { BROKER_CLIENT_UNRESOLVED, resolveBrokerClient, spawnBrokerClient } from "./broker-client.mjs";
16
20
  import { loadBrokerPins } from "./broker-pins.mjs";
21
+ import { ATTEST_STAGE_NONCE_ENV, canonicalJson, carriedCallNonce } from "./provenance-verify.mjs";
17
22
  import { resolveManagedRoots } from "./managed-roots.mjs";
23
+ import { forwardedAdapterEnv, withoutSigningEnv } from "./adapter-env.mjs";
24
+ import { carriedShapeDecision } from "./cargo-shapes.mjs";
25
+ import { swiftScratchRefusal } from "./swift-scratch.mjs";
26
+ import { removeCarrier, sweepSnapshotRequests } from "./carrier-census.mjs";
27
+ import { DEFAULT_RETRY_WINDOW_MS, RETRYABLE_OUTCOME_CODES, clientGuardMs, diagnoseControlReply, drainFrames, operationDeadlineEnv, operationOutcomeInOutput, overCapProblem, refusalInCapturedOutput, retryPauseMs, sleepSync } from "./broker-wire.mjs";
28
+ import { OWNERSHIP_ENV, OWNERSHIP_FILE_ENV, markCarriedSend, markCarriedUnsent } from "./ownership-mark.mjs";
18
29
  import {
19
- SnapshotRequestError, argvSha256, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
30
+ MANAGED_RUST_ENVIRONMENT_KEYS, SNAPSHOT_REQUEST_SCHEMA, SnapshotRequestError, argvSha256, isCallHandleOf, lockedArgv, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
20
31
  } from "./snapshot-request.mjs";
21
32
 
33
+ /**
34
+ * `env` without any managed Rust variable (MANAGED_RUST_ENVIRONMENT_KEYS, the list the client records as `observedEnv`; case-insensitive,
35
+ * as Windows environments are). The broker refuses a carried request whose observed value of one of them differs from the one it manages
36
+ * itself (service.rs validate_managed_rust_environment), and it supplies its own: so a carried call never forwards one, whatever the
37
+ * caller's shell held (CARGO_HOME, RUSTFLAGS, RUSTC_WRAPPER, ...).
38
+ */
39
+ export function withoutManagedRustEnv(env) {
40
+ const out = withoutSigningEnv(env); // GB-06: signing material (TAURI_SIGNING_*, TAURI_PRIVATE_KEY, TAURI_KEY_PASSWORD) never reaches the client of a carried call
41
+ for (const key of Object.keys(out)) if (MANAGED_RUST_ENVIRONMENT_KEYS.includes(key.toUpperCase())) delete out[key];
42
+ return out;
43
+ }
44
+
22
45
  /** The status a refused or unverified call returns to its caller: cargo's own error status. */
23
46
  export const BROKER_REFUSAL_STATUS = 101;
24
- export const CALLING_GATE_ENV = "RIGHTKIT_CALLING_GATE";
47
+ export const CALLING_GATE_ENV = "RUSTRIGHT_CALLING_GATE";
25
48
 
26
49
  /** The managed `ws` roots a broker pins its generation targets under (BROKER-CONTRACT output pinning). */
27
50
  export const brokerOutputRoots = ({ dataRoot }) => [path.join(dataRoot, "rightkit-build-control", "ws")];
28
51
 
29
52
  /** The broker's receipt of one request, read through the RustRight client (`rustright status <id>`), synchronously. */
30
- export function receiptSync({ client, requestId, env, spawn = spawnSync }) {
31
- const r = spawn(client, ["status", requestId], { encoding: "utf8", env, timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
32
- if (r.error || r.status !== 0) throw new SnapshotRequestError("receipt", `rustright status ${requestId} failed (${r.error?.message ?? `exit ${r.status}`})`);
53
+ export function receiptSync({ client, requestId, env, spawn = spawnSync, platform = process.platform }) {
54
+ const r = spawnBrokerClient(spawn, client, ["status", requestId], { encoding: "utf8", env, timeout: 60_000, maxBuffer: 64 * 1024 * 1024 }, { platform });
55
+ const failedRun = () => new SnapshotRequestError("receipt", `rustright status ${requestId} failed (${r.error?.message ?? `exit ${r.status}`})`);
56
+ if (r.error) throw failedRun();
33
57
  let doc;
34
- try { doc = JSON.parse(String(r.stdout)); } catch { throw new SnapshotRequestError("receipt", `rustright status ${requestId} printed no JSON`); }
58
+ // A refused or unavailable reply may arrive with a nonzero exit (the client exits 125 on a REJECTED result): its typed body is still diagnosed (GB-84).
59
+ try { doc = JSON.parse(String(r.stdout)); } catch { throw r.status !== 0 ? failedRun() : new SnapshotRequestError("receipt", `rustright status ${requestId} printed no JSON`); }
35
60
  // The status envelope is `{type:"STATUS", id, state, ..., receipt}`: `receipt` is null while the job is live and full once it is
36
61
  // terminal. Polling is not allowed, so a null receipt after the cargo process exited is a clear error, never a retry.
37
- if (!doc || typeof doc !== "object" || !Object.hasOwn(doc, "receipt")) throw new SnapshotRequestError("receipt", `rustright status ${requestId} returned no receipt field (not a STATUS envelope)`);
38
- if (doc.receipt === null) throw new SnapshotRequestError("receipt-not-ready", `rustright status ${requestId} reports the job ${doc.state ?? "state unknown"} with no receipt yet, although its cargo process exited; the gate does not poll, so this call is unverified`);
39
- if (typeof doc.receipt !== "object" || Array.isArray(doc.receipt)) throw new SnapshotRequestError("receipt", `rustright status ${requestId} returned a receipt that is not an object`);
40
- return doc.receipt;
62
+ // GB-53: an over-cap outcome is a typed truncation, never a missing-field mystery.
63
+ const over = overCapProblem(doc);
64
+ if (over) throw new SnapshotRequestError(over.code, `rustright status ${requestId}: ${over.message}`);
65
+ // GB-84: the typed envelope is judged BEFORE any receipt validation (diagnoseControlReply, RustRight's own rule, vector control-status-vectors.v1.json). Only a terminal
66
+ // reply with a receipt of this request is a build outcome; a live, unavailable, refused or malformed reply keeps the broker's state, code and identity, is never green, and
67
+ // is reconciled by the durable request id (the gate never resubmits because a diagnosis improved).
68
+ const diagnosis = diagnoseControlReply(doc, requestId);
69
+ const typed = (code, message) => Object.assign(new SnapshotRequestError(code, message), { diagnosis });
70
+ switch (diagnosis.kind) {
71
+ case "terminal": if (r.status !== 0) throw failedRun(); return diagnosis.receipt;
72
+ case "live": throw typed("receipt-not-ready", `rustright status ${requestId} reports the job ${diagnosis.state} with no receipt yet, although its cargo process exited; the gate does not poll or resubmit, so this call is unverified and the request id ${requestId} is the durable handle to reconcile it`);
73
+ case "unavailable": throw typed("broker-unavailable", `rustright status ${requestId}: the broker's front gate cannot serve now (${diagnosis.reason}); the request id ${requestId} is kept for reconciliation, nothing is resubmitted`);
74
+ case "refused": throw typed(`broker-refused-${diagnosis.refusal}`, `rustright status ${requestId}: the broker refused the reply (${diagnosis.code ?? "no code"}: ${diagnosis.message || diagnosis.refusal})${diagnosis.refusal === "suspended" ? "; the job runs again after the broker restarts: ask again then, by this id" : diagnosis.refusal === "paused" ? "; admission is paused, nothing was spawned" : ""}`);
75
+ default: throw typed("receipt", `rustright status ${requestId}: the reply is not a valid status for this request (${diagnosis.message})`);
76
+ }
41
77
  }
42
78
 
43
79
  /**
@@ -45,22 +81,32 @@ export function receiptSync({ client, requestId, env, spawn = spawnSync }) {
45
81
  * no build ownership, no RustRight client, no pins (an app repository without an enrollment), a malformed declared-inputs manifest.
46
82
  * There is no way to hand it pins: tests build their own object and inject it as `deps.snapshotBroker`.
47
83
  */
48
- export function createSnapshotBroker({ env = process.env, platform = process.platform, checkout, commit, snapshot, ownership, client, gateFile, dataRoot, spawn = spawnSync, loadPins = loadBrokerPins }) {
84
+ export function createSnapshotBroker({ env = process.env, platform = process.platform, checkout, commit, snapshot, ownership, client, gateFile, dataRoot, spawn = spawnSync, loadPins = loadBrokerPins, lease = null }) {
49
85
  const need = (ok, why) => { if (!ok) throw new SnapshotRequestError("broker", why); };
50
86
  need(checkout && path.isAbsolute(checkout), "the primary checkout must be an absolute path");
51
87
  need(commit?.sha && commit?.tree, "the commit and its tree are required");
52
88
  need(snapshot?.path && path.isAbsolute(snapshot.path) && snapshot.expectedTree === commit.tree, "the snapshot needs an absolute path and the commit's tree as expectedTree");
53
89
  need(ownership && typeof ownership.handle === "string" && ownership.handle && typeof ownership.markBrokerSubmit === "function", "a broker snapshot compile needs the gate's build ownership (handle and submit hook)");
54
- need(typeof client === "string" && path.isAbsolute(client), "a broker snapshot compile needs the RustRight client (`rustright`) by absolute path");
90
+ need(typeof client === "string" && (platform === "win32" ? path.win32 : path).isAbsolute(client), "a broker snapshot compile needs the RustRight client (`rustright`) by absolute path");
91
+ // The client the carried map hands every child is the executable itself: on win32 the `rustright.cmd` shim is resolved here, once,
92
+ // so no child re-resolves (or spawns) a .cmd (broker-client.mjs). A shim of any other shape refuses with RK_BROKER_CLIENT_UNRESOLVED.
93
+ try { client = resolveBrokerClient(client, { platform }).file; } catch (e) { throw new SnapshotRequestError(e.code ?? BROKER_CLIENT_UNRESOLVED, e.message); }
55
94
  const roots = resolveManagedRoots({ env, platform });
56
95
  const managedData = dataRoot ?? roots.dataRoot;
57
96
  const pins = loadPins({ dataRoot: managedData, platform, gateFile, checkout, spawn });
97
+ // GB-76: carriers a crashed gate left behind are retired on proof (creator dead, nothing live or unknown at the broker under their sub-handle), counted otherwise. Best-effort: a
98
+ // sweep that cannot run never stops a build, and nothing is deleted without proof.
99
+ let carrierSweep = null;
100
+ if (typeof lease === "function") { try { carrierSweep = sweepSnapshotRequests({ controlRoot: roots.controlRoot, snapshot: snapshot.path, lease: (q) => lease({ ...q, env, platform, spawn, client }) }); } catch { carrierSweep = null; } }
58
101
  return {
102
+ carrierSweep,
59
103
  controlRoot: roots.controlRoot, commit: { sha: commit.sha, tree: commit.tree }, snapshot: { path: snapshot.path, expectedTree: snapshot.expectedTree },
60
- declaredInputs: readDeclaredInputs(snapshot.path), handle: ownership.handle, markBrokerSubmit: () => ownership.markBrokerSubmit(),
104
+ declaredInputs: readDeclaredInputs(snapshot.path), handle: ownership.handle, markBrokerSubmit: (callHandle) => ownership.markBrokerSubmit(callHandle), markBrokerUnsent: typeof ownership.markBrokerUnsent === "function" ? (callHandle, reason) => ownership.markBrokerUnsent(callHandle, reason) : undefined,
61
105
  pins: { host: pins.host, keyId: pins.keyId, publicKey: pins.publicKey, brokerDigest: pins.brokerDigest }, pinsSource: pins.source,
62
106
  checkout: fs.realpathSync.native(checkout), outputRoots: brokerOutputRoots({ dataRoot: managedData }), platform, client,
63
- receiptOf: async (requestId) => receiptSync({ client, requestId, env, spawn }),
107
+ receiptOf: async (requestId) => receiptSync({ client, requestId, env, spawn, platform }),
108
+ // The broker's lease status of one sub-handle (SNAPSHOT_LEASE_STATUS, with the bounded wait): what a cancelled call is reconciled against.
109
+ ...(typeof lease === "function" ? { leaseStatus: ({ handle, waitMs = 0 }) => lease({ snapshot: snapshot.path, handle, env, platform, spawn, client, waitMs }) } : {}),
64
110
  };
65
111
  }
66
112
 
@@ -71,101 +117,526 @@ export function createSnapshotBroker({ env = process.env, platform = process.pla
71
117
  */
72
118
  export const PRODUCTION_SNAPSHOT_CARRIER = Object.freeze({ kind: "production", create: createSnapshotBroker });
73
119
 
120
+ /** What a typed refusal means for the caller, appended to the outcome's message: nothing was spawned, and whether waiting helps (BROKER-CONTRACT section 16). */
121
+ function refusalAdvice(refused) {
122
+ if (refused.refusal === "paused") return ": admission is paused and nothing was spawned";
123
+ if (refused.refusal === "recovery-pending") return ": restart recovery of the workspace is still running and nothing was admitted; the call is retried within its deadline";
124
+ if (refused.refusal === "storage-budget-unavailable") return ": the storage budget could not be settled (a maintenance pass is running) and nothing was admitted; the call is retried within its deadline";
125
+ if (refused.refusal === "adapter-pin-required") return ": nothing was spawned; the commit's gradle/wrapper/gradle-wrapper.properties must carry distributionSha256Sum=<64 lowercase hex> for its distribution (the pin is the commit's, so this is not retried)";
126
+ return "";
127
+ }
128
+
74
129
  /** The part of a snapshot broker the wrapper process needs, as the map file carries it (public values only). */
75
- export const brokerSection = (sb, { sentinel = null } = {}) => ({
76
- client: sb.client, controlRoot: sb.controlRoot, commit: sb.commit, snapshot: sb.snapshot, handle: sb.handle, checkout: sb.checkout,
77
- pins: sb.pins, outputRoots: sb.outputRoots, platform: sb.platform, sentinel,
78
- });
130
+ export const brokerSection = (sb, { sentinel = null } = {}) => {
131
+ const section = {
132
+ client: sb.client, controlRoot: sb.controlRoot, commit: sb.commit, snapshot: sb.snapshot, handle: sb.handle, checkout: sb.checkout,
133
+ pins: sb.pins, outputRoots: sb.outputRoots, platform: sb.platform, sentinel,
134
+ };
135
+ // The gate's own carried calls record their sub-handle through the ownership object it holds. Not enumerable: it never reaches the carrier
136
+ // map a child reads (JSON), whose carried calls record through the inherited ownership file instead (markCarriedRequest).
137
+ if (typeof sb.markBrokerSubmit === "function") Object.defineProperty(section, "markSend", { value: (callHandle) => sb.markBrokerSubmit(callHandle), enumerable: false });
138
+ if (typeof sb.markBrokerUnsent === "function") Object.defineProperty(section, "markUnsent", { value: (callHandle, reason) => sb.markBrokerUnsent(callHandle, reason), enumerable: false });
139
+ // GB-52: the broker's lease status of one sub-handle, asked after a cancelled call (not part of the map a child reads).
140
+ if (typeof sb.leaseStatus === "function") Object.defineProperty(section, "leaseStatus", { value: (query) => sb.leaseStatus(query), enumerable: false });
141
+ return section;
142
+ };
143
+
144
+ /**
145
+ * Records in the shared build-ownership record, immediately before the carried request `callHandle` is sent, that broker work may exist
146
+ * (build-ownership.mjs markBrokerSubmit: `submitted` and the sub-handle). This is the only place a carried call marks, whichever process
147
+ * it runs in: the gate's own, or a right-release child that inherited RIGHTKIT_BUILD_OWNERSHIP (+ _FILE) from it. `section.markSend` is a
148
+ * seam for a caller that holds the ownership object (and for fixtures). Throws when the mark cannot land; the request is then not sent.
149
+ */
150
+ function markCarriedRequest({ section, env, callHandle }) {
151
+ if (typeof section.markSend === "function") {
152
+ // GB-69: a direct hook's return value is never trusted unchecked: the same assertion the env-file path makes. A hook that did nothing, returned another handle's record, or
153
+ // returned no record of this call's sub-handle refuses the send (nothing is sent), whatever the hook is.
154
+ const marked = section.markSend(callHandle);
155
+ if (marked?.broker?.submitted !== true || !Array.isArray(marked.broker.handles) || !marked.broker.handles.includes(callHandle) || (marked.handle !== undefined && marked.handle !== section.handle)) throw new SnapshotRequestError("ownership", `the build ownership did not record the carried request ${callHandle} under ${section.handle} (the submit hook returned ${marked?.broker ? "another record" : "no record"}); the request is not sent`);
156
+ return marked;
157
+ }
158
+ const handle = env[OWNERSHIP_ENV] ?? process.env[OWNERSHIP_ENV];
159
+ if (handle !== section.handle) throw new SnapshotRequestError("ownership", `the build ownership in the environment (${handle ?? "none"}) is not this call's (${section.handle}); the carried request is not sent`);
160
+ const file = env[OWNERSHIP_FILE_ENV] ?? process.env[OWNERSHIP_FILE_ENV];
161
+ if (!file) throw new SnapshotRequestError("ownership", `${OWNERSHIP_FILE_ENV} is not set: the carried request cannot be recorded in the build ownership, so it is not sent`);
162
+ const marked = markCarriedSend({ file, handle, callHandle });
163
+ if (marked?.broker?.submitted !== true || !marked.broker.handles?.includes(callHandle)) throw new SnapshotRequestError("ownership", `the build ownership did not record the carried request ${callHandle}`);
164
+ return marked;
165
+ }
166
+
167
+ /** The spawn errors that prove the client process never started, so no byte reached the broker (GB-21). Anything else after a start is ambiguous and is never marked unsent. */
168
+ const NO_SPAWN_CODES = new Set(["ENOENT", "EACCES", "EPERM", "EINVAL", "ENOEXEC", "ENOTDIR", BROKER_CLIENT_UNRESOLVED]);
169
+ export const provenNoSpawn = (error) => Boolean(error) && NO_SPAWN_CODES.has(error.code);
170
+
171
+ /**
172
+ * GB-21: whether the RustRight client can be started at all, checked BEFORE the ownership is marked (a mark records that broker work may exist, so a call that cannot start
173
+ * must not leave one). An absolute client path must exist and (on POSIX) be executable; a bare name is resolved by the spawn itself and is left to it.
174
+ */
175
+ export function clientStartable(client, platform = process.platform) {
176
+ if (typeof client !== "string" || client === "") return "no RustRight client is named";
177
+ if (!(platform === "win32" ? path.win32 : path).isAbsolute(client)) return null;
178
+ if (platform !== process.platform) return null; // a simulated platform (a test) has no file system of its own to probe
179
+ try { const meta = fs.statSync(client); if (!meta.isFile()) return `${client} is not a file`; if (platform !== "win32") fs.accessSync(client, fs.constants.X_OK); return null; } catch (e) { return `${client} cannot be started (${e.code ?? e.message})`; }
180
+ }
181
+
182
+ /** Records, after a client that provably never started, that this sub-handle was never sent (ownership: broker.unsent). Best-effort: a mark that cannot land leaves the record held, never released. */
183
+ function markCarriedUnsentRequest({ section, env, callHandle, reason }) {
184
+ try {
185
+ if (typeof section.markUnsent === "function") { section.markUnsent(callHandle, reason); return true; }
186
+ const handle = env[OWNERSHIP_ENV] ?? process.env[OWNERSHIP_ENV]; const file = env[OWNERSHIP_FILE_ENV] ?? process.env[OWNERSHIP_FILE_ENV];
187
+ if (handle !== section.handle || !file) return false;
188
+ markCarriedUnsent({ file, handle, callHandle, reason });
189
+ return true;
190
+ } catch { return false; }
191
+ }
79
192
 
80
193
  const escapes = (rel) => rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel);
81
194
  const inside = (child, parent) => { const rel = path.relative(parent, child); return rel === "" || !escapes(rel); };
82
195
 
83
196
  /**
84
- * One cargo call of a test, carried through the broker. `workspace` is the real snapshot path of the cargo workspace the call
85
- * resolved; `checkoutWorkspace` its counterpart in the primary checkout, from the gate's own map. Returns cargo's exit status.
86
- * Every outcome is ledgered through `record` first or after, never skipped.
197
+ * The primary-checkout counterpart of a snapshot directory, spelled lexically (BROKER-CONTRACT section 16, GB-16): `base` (the logical checkout path) joined with the snapshot-relative `rel`,
198
+ * which `join` normalises, so the result is absolute and holds no `.` or `..`. Nothing about the checkout's present state decides it. `pathApi` is the platform's path module (a Windows
199
+ * spelling is judged with `path.win32`).
200
+ */
201
+ export const lexicalCounterpart = ({ base, rel, pathApi = path }) => pathApi.join(base, rel);
202
+
203
+ /**
204
+ * The nearest directory at or above `lexical` that exists (`isDirectory(p)`), or null when none does. A path that became a plain file is passed over: the nearest DIRECTORY is what a client
205
+ * can start in.
206
+ */
207
+ export function nearestExistingDirectory(lexical, { isDirectory = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } }, pathApi = path } = {}) {
208
+ for (let p = lexical; ; p = pathApi.dirname(p)) {
209
+ if (isDirectory(p)) return p;
210
+ if (pathApi.dirname(p) === p) return null;
211
+ }
212
+ }
213
+
214
+ /** `p` as the file system names it when it exists; when it is gone, the real path of its nearest existing directory plus what is left below it (a lexical path stays one). */
215
+ export function canonicalCounterpart(p) {
216
+ try { return fs.realpathSync.native(p); } catch (e) { if (e.code !== "ENOENT" && e.code !== "ENOTDIR") throw e; }
217
+ const ancestor = nearestExistingDirectory(p);
218
+ if (ancestor === null) throw Object.assign(new Error(`${p} has no existing ancestor`), { code: "ENOENT" });
219
+ return path.join(fs.realpathSync.native(ancestor), path.relative(ancestor, p));
220
+ }
221
+
222
+ /**
223
+ * The primary-checkout counterpart of a directory of the snapshot (GB-16). The carrier's `primaryCwd` is that counterpart spelled lexically; the RustRight client accepts it when its own working
224
+ * directory is it, or, when it is gone (an older commit built after the primary checkout removed the directory), the nearest directory of it that still exists, and then names the
225
+ * counterpart as the request's workspace (the attestation hashes that workspace). The broker maps by the snapshot-relative path. So a removed counterpart is no refusal: this returns
226
+ * `{ dir, spawnCwd, removed }`: `dir` the lexical counterpart (what the carrier names), `spawnCwd` the real directory the client is started in (the counterpart itself when it exists, else the
227
+ * real path of its nearest existing ancestor inside the checkout). An existing counterpart, and the ancestor of a removed one, are realpathed, so a link that leaves the checkout is still
228
+ * refused (`..._ESCAPE`); anything else the file system will not say (permission) is `..._UNREADABLE`. Returns `{ dir, spawnCwd, removed }` or `{ refusal: { code, message } }`.
229
+ */
230
+ export function checkoutCounterpart({ base, rel, checkout, codePrefix }) {
231
+ const escapeCode = `${codePrefix}_ESCAPE`; const unreadableCode = `${codePrefix}_UNREADABLE`;
232
+ // The logical base may be spelled through a link the checkout path is not (a map built from a realpath): resolve the base alone when it exists.
233
+ let root = base; try { root = fs.realpathSync.native(base); } catch { /* an absent base is judged by its spelling */ }
234
+ const lexical = lexicalCounterpart({ base: root, rel });
235
+ if (!inside(lexical, checkout)) return { refusal: { code: escapeCode, message: `${lexical} is outside the primary checkout ${checkout}` } };
236
+ let real;
237
+ try { real = fs.realpathSync.native(lexical); }
238
+ catch (e) {
239
+ if (e.code !== "ENOENT" && e.code !== "ENOTDIR") return { refusal: { code: unreadableCode, message: `${lexical} cannot be resolved (${e.code ?? e.message})` } };
240
+ const ancestor = nearestExistingDirectory(lexical);
241
+ let spawnCwd = null;
242
+ try { spawnCwd = ancestor === null ? null : fs.realpathSync.native(ancestor); } catch (error) { return { refusal: { code: unreadableCode, message: `${ancestor} cannot be resolved (${error.code ?? error.message})` } }; }
243
+ if (spawnCwd === null || !inside(spawnCwd, checkout)) return { refusal: { code: escapeCode, message: `${spawnCwd ?? ancestor ?? lexical} is outside the primary checkout ${checkout}` } };
244
+ return { dir: lexical, spawnCwd, removed: true };
245
+ }
246
+ try { if (!fs.statSync(real).isDirectory()) return { refusal: { code: unreadableCode, message: `${real} is not a directory` } }; } catch (e) { return { refusal: { code: unreadableCode, message: `${real} cannot be read (${e.code ?? e.message})` } }; }
247
+ if (!inside(real, checkout)) return { refusal: { code: escapeCode, message: `${real} is outside the primary checkout ${checkout}` } };
248
+ return { dir: real, spawnCwd: real, removed: false };
249
+ }
250
+
251
+ /** Per-call carrier variables, in both spellings (rustright-cli shims.rs LEGACY_ENV_SPELLINGS: the current RUSTRIGHT_* name wins over the legacy one). */
252
+ const CARRIER_CALL_VARIABLES = ["SNAPSHOT_REQUEST", "ATTEST_NONCE", "REQUEST_ID_FILE", "PARENT_REQUEST_ID", "PARENT_CAPABILITY_TOKEN", "SNAPSHOT_ADMISSION"];
253
+ /** The carrier variables of an environment, by name: a call's carrier is set by the gate for that one call and never inherited. */
254
+ export function inheritedCarrierEnv(env) {
255
+ const found = [];
256
+ for (const [key, value] of Object.entries(env ?? {})) {
257
+ const upper = key.toUpperCase();
258
+ const bare = upper.startsWith("RUSTRIGHT_") ? upper.slice("RUSTRIGHT_".length) : upper.startsWith("RIGHTKIT_") ? upper.slice("RIGHTKIT_".length) : null;
259
+ if (bare === null) continue;
260
+ if (CARRIER_CALL_VARIABLES.includes(bare) || (bare === "CHECK_ADMISSION" && String(value).trim() === "1")) found.push(key);
261
+ }
262
+ return found;
263
+ }
264
+
265
+ /**
266
+ * GB-52: a call that ran out its operation deadline is cancelled BY ID by the client, which then polls the terminal state. The gate does not take that for a release: it asks the broker once, with
267
+ * a bounded wait, what it holds under the call's sub-handle (SNAPSHOT_LEASE_STATUS members, GB-23/GB-85). `reaped` is true only for a well-formed REAPED answer (every submission under the
268
+ * handle durably complete); ACTIVE, UNKNOWN, a wait that timed out, a reader that is absent or throws all leave ownership held, and the outcome says which. Never resubmits.
269
+ */
270
+ function reconcileAfterCancel({ section, callHandle, childEnv }) {
271
+ const cancelWait = Number(childEnv?.RUSTRIGHT_CANCEL_WAIT_MS);
272
+ const waitMs = Math.min(30_000, Number.isFinite(cancelWait) && cancelWait >= 0 ? Math.trunc(cancelWait) : 30_000);
273
+ if (typeof section.leaseStatus !== "function") return { reaped: false, subHandle: { handle: callHandle, state: "UNKNOWN", why: "this gate has no lease reader" }, note: `the gate cannot ask the broker about sub-handle ${callHandle}, so ownership stays UNKNOWN until every sub-handle is REAPED` };
274
+ let status;
275
+ try { status = section.leaseStatus({ handle: callHandle, waitMs }); } catch (e) { status = { state: "UNKNOWN", why: e.message }; }
276
+ const state = status?.state ?? "UNKNOWN";
277
+ const subHandle = { handle: callHandle, state, why: status?.why ?? null, ...(status?.waitedMs !== undefined ? { waitedMs: status.waitedMs } : {}), ...(status?.waitTimedOut !== undefined ? { waitTimedOut: status.waitTimedOut } : {}), ...(Array.isArray(status?.members) ? { members: status.members } : {}) };
278
+ if (state === "REAPED") return { reaped: true, subHandle, note: `after the cancel the broker reports sub-handle ${callHandle} REAPED (every submission under it is settled)` };
279
+ return { reaped: false, subHandle, note: `after the cancel the broker reports sub-handle ${callHandle} still ${state}${status?.why ? ` (${status.why})` : ""}: ownership stays held until it is REAPED; the job is not resubmitted` };
280
+ }
281
+
282
+ /**
283
+ * GB-52, GB-78, GB-39 (BROKER-CONTRACT section 16): a carried call the broker answered with a refusal that clears on its own (RK_RECOVERY_PENDING, RK_STORAGE_BUDGET_UNAVAILABLE) or whose
284
+ * connection never opened (RK_BROKER_BUSY: every pipe instance busy, no byte sent) is a wait. The gate pauses (250 ms doubling to 5 s) and sends the call again within the call's own
285
+ * deadline (`deadlineMs`, else DEFAULT_RETRY_WINDOW_MS), each attempt with its own carrier, nonce and sub-handle (a handle is spent once; the refused attempt is a settled, spawned:false
286
+ * submission, a busy one is recorded unsent) and under ONE ledger id, so the final record is the call's. It is never success and never a generic failure: when the deadline passes the
287
+ * outcome keeps the broker's own code (`broker-refused-recovery-pending`, `broker-refused-storage-budget-unavailable`, `broker-busy`) and says how long it waited. Every other outcome, a
288
+ * verified one, RK_ADAPTER_PIN_REQUIRED and RK_OPERATION_DEADLINE included, is returned at once. `now` and `sleep` are seams for tests.
289
+ */
290
+ function carryWithRetries(once, call) {
291
+ const { deadlineMs = null, now = Date.now, sleep = sleepSync, err = () => {}, record = () => {}, ...rest } = call;
292
+ const window = deadlineMs !== null && Number.isFinite(deadlineMs) ? Math.max(1, Math.trunc(deadlineMs)) : DEFAULT_RETRY_WINDOW_MS;
293
+ const started = now();
294
+ const ledgerId = crypto.randomUUID();
295
+ let attempt = 0; let held = []; let result;
296
+ for (;;) {
297
+ attempt += 1;
298
+ held = [];
299
+ const remaining = Math.max(1, window - (now() - started));
300
+ result = once({ ...rest, record, err: (m) => held.push(m), deadlineMs: deadlineMs === null ? null : remaining, ledgerId });
301
+ const code = result.outcome?.status === "unverified" ? result.outcome.code : null;
302
+ if (code === null || !RETRYABLE_OUTCOME_CODES.has(code)) break;
303
+ const pause = retryPauseMs(attempt);
304
+ if (window - (now() - started) <= pause) {
305
+ const elapsed = now() - started;
306
+ const message = `${result.outcome.message}; retried ${attempt - 1} time(s) over ${elapsed} ms and the call's ${window} ms deadline passed before the broker would take it: it is a wait that did not clear, never a result`;
307
+ result = { ...result, outcome: { ...result.outcome, message, attempts: attempt, retried: attempt - 1 } };
308
+ try { record({ id: ledgerId, mode: "broker", status: "unverified", code: result.outcome.code, message }); } catch { /* the final record above stands */ }
309
+ held.push(`rightkit-gate: ${result.outcome.code}: ${message}`);
310
+ break;
311
+ }
312
+ sleep(pause);
313
+ // The next attempt's pending record replaces this attempt's outcome fields in the ledger (one id).
314
+ try { record({ id: ledgerId, mode: "broker", status: "pending", code: null, message: null }); } catch { /* the attempt records its own pending state */ }
315
+ }
316
+ for (const m of held) err(m);
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * One carried cargo call: its own fresh nonce and carrier, `primaryCwd` from the gate's snapshot-to-checkout map, `callingGate`
322
+ * inherited and never forged, the carrier removed after the call, the signed receipt verified, and a ledger record either way.
323
+ * `workspace` is the real snapshot path of the cargo workspace the call resolved; `checkoutWorkspace` its counterpart in the primary
324
+ * checkout, from the gate's own map. `stdio` is the client's (`inherit`, or `capture` for a caller that reads cargo's output).
325
+ * Returns `{ status, outcome, stdout, stderr }`; `outcome` is the ledgered record's outcome (`verified` with `outputDir`, or not).
326
+ * Refused before anything runs: a carrier or per-call carrier variable already in the environment, no calling gate, a working
327
+ * directory with no counterpart in the checkout. An output directory of the caller's is dropped (the broker pins it).
87
328
  */
88
- export function runBrokerCargo({ words, cwd, env, section, workspace, checkoutWorkspace, record, spawn = spawnSync, err, tmp = os.tmpdir() }) {
89
- const id = crypto.randomUUID();
329
+ export function carryBrokerCargo(call) { return carryWithRetries(carryBrokerCargoOnce, call); }
330
+
331
+ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWorkspace, record, spawn = spawnSync, err, tmp = os.tmpdir(), stdio = "inherit", deadlineMs = null, ledgerId = null }) {
332
+ words = lockedArgv(words); // a build or run is always locked, and a standard alias is expanded first (snapshot-request.mjs lockedArgv)
333
+ const id = ledgerId ?? crypto.randomUUID();
90
334
  const base = { id, mode: "broker", cwd, argv: [...words] };
91
- const refuse = (code, message) => { record({ ...base, status: "refused", code, message }); err(`rightkit-gate: ${code}: ${message}`); return BROKER_REFUSAL_STATUS; };
335
+ const refuse = (code, message) => { record({ ...base, status: "refused", code, message }); err(`rightkit-gate: ${code}: ${message}`); return { status: BROKER_REFUSAL_STATUS, outcome: { status: "refused", code, message }, stdout: "", stderr: `rightkit-gate: ${code}: ${message}\n` }; };
336
+ // GB-01: the one command-shape table (cargo-shapes.mjs, exercised against RustRight's cargo-shapes vector) decides BEFORE anything is marked or sent:
337
+ // a shape the broker refuses inside a snapshot (tree, pkgid, verify-project, a bare metadata, a write-form fmt, help, ...) launches zero processes.
338
+ const shape = carriedShapeDecision(words);
339
+ if (!shape.ok) return refuse(shape.code, shape.message);
92
340
  const gate = env[CALLING_GATE_ENV];
93
341
  if (typeof gate !== "string" || !gate.trim()) return refuse("RK_CARGO_BROKER_NO_GATE", `${CALLING_GATE_ENV} is not set: the call names no gate, and the wrapper never forges one`);
94
- let realCwd; let checkoutCwd;
342
+ const inherited = inheritedCarrierEnv(env);
343
+ if (inherited.length) return refuse("RK_CARGO_BROKER_INHERITED_ENV", `${inherited.join(", ")} ${inherited.length > 1 ? "are" : "is"} already in the environment; a carrier is set by the gate for one call only, so a nested or replayed call is refused`);
344
+ // The broker pins the output directory, so a carried call passes none: the caller's CARGO_TARGET_DIR (and the gate's unbound sentinel)
345
+ // are dropped here. The receipt's `outputDir` is verified to be the broker's generation target under the managed `ws` root, outside the
346
+ // snapshot and the checkout, whatever the environment held, so dropping it can only make the call safer.
347
+ const clientEnv = withoutManagedRustEnv(env);
348
+ // There is no stage grant any more (one model: a snapshot stage's compiles run nested under an adapter job); an inherited one is dropped, never forwarded.
349
+ for (const key of Object.keys(clientEnv)) if (/^(RUSTRIGHT|RIGHTKIT)_SNAPSHOT_GRANT$/i.test(key)) delete clientEnv[key];
350
+ // A release-bound stage's nonce (RIGHTKIT_ATTEST_STAGE_NONCE) is read here and never reaches the client: this call's nonce is derived from it.
351
+ const stageNonce = env[ATTEST_STAGE_NONCE_ENV];
352
+ for (const key of Object.keys(clientEnv)) if (key.toUpperCase() === ATTEST_STAGE_NONCE_ENV) delete clientEnv[key];
353
+ let realCwd; let checkoutCwd; let rel;
95
354
  try {
96
355
  realCwd = fs.realpathSync.native(cwd);
97
- const rel = path.relative(workspace, realCwd);
356
+ rel = path.relative(workspace, realCwd);
98
357
  if (escapes(rel)) throw new Error(`${realCwd} is not inside the workspace ${workspace}`);
99
- checkoutCwd = fs.realpathSync.native(path.join(checkoutWorkspace, rel));
100
- if (!fs.statSync(checkoutCwd).isDirectory()) throw new Error(`${checkoutCwd} is not a directory`);
101
- if (!inside(checkoutCwd, section.checkout)) throw new Error(`${checkoutCwd} is outside the primary checkout ${section.checkout}`);
102
- } catch (e) { return refuse("RK_CARGO_BROKER_CWD", `the call's working directory has no counterpart in the primary checkout (${e.message}); the broker runs it where the checkout has it`); }
358
+ } catch (e) { return refuse("RK_CARGO_BROKER_CWD", `the call's working directory is not inside its workspace (${e.message})`); }
359
+ const counterpart = checkoutCounterpart({ base: checkoutWorkspace, rel, checkout: section.checkout, codePrefix: "RK_CARGO_BROKER_COUNTERPART" });
360
+ if (counterpart.refusal) return refuse(counterpart.refusal.code, counterpart.refusal.message);
361
+ checkoutCwd = counterpart.dir; const spawnCwd = counterpart.spawnCwd;
103
362
  let inputs;
104
363
  try { inputs = readDeclaredInputs(section.snapshot.path); } catch (e) { return refuse("RK_CARGO_BROKER_DECLARED_INPUTS", e.message); }
105
- // The environment the client sees: the caller's, without the gate's unbound-sentinel output directory (the broker pins the output).
106
- const clientEnv = { ...env };
107
- for (const key of Object.keys(clientEnv)) {
108
- if (/^(CARGO_TARGET_DIR|CARGO_BUILD_TARGET_DIR)$/i.test(key) && section.sentinel && String(clientEnv[key]).startsWith(section.sentinel)) delete clientEnv[key];
109
- }
110
364
  const refDir = fs.mkdtempSync(path.join(tmp, "rk-snapreq-"));
111
365
  let made = null;
112
366
  try {
113
367
  try {
114
- made = writeSnapshotRequest({ controlRoot: section.controlRoot, commit: section.commit, snapshot: section.snapshot, declaredInputs: inputs, handle: section.handle, primaryCwd: checkoutCwd, argv: words, requestIdFile: path.join(refDir, "request.json") });
368
+ // Under a release-bound stage the nonce is derived from the stage's, this call's directory (relative to the primary checkout, posix) and
369
+ // its argv, so the hosted admission can recompute it from the stage's rows (provenance-verify.mjs carriedCallNonce); otherwise it is fresh.
370
+ const nonce = /^[0-9a-f]{64}$/.test(stageNonce ?? "") ? carriedCallNonce({ stageNonce, cwd: path.relative(section.checkout, checkoutCwd).split(path.sep).join("/") || ".", argv: words }) : undefined;
371
+ made = writeSnapshotRequest({ controlRoot: section.controlRoot, commit: section.commit, snapshot: section.snapshot, declaredInputs: inputs, handle: section.handle, primaryCwd: checkoutCwd, argv: words, nonce, requestIdFile: path.join(refDir, "request.json") });
115
372
  } catch (e) { return refuse("RK_CARGO_BROKER_CARRIER", e.message); }
116
- try { record({ ...base, status: "pending", nonce: made.carrier.nonce, checkoutCwd }); } catch (e) { fs.rmSync(made.file, { force: true }); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
117
- const childEnv = { ...clientEnv, ...made.env };
118
- const r = spawn(section.client, ["cargo", ...words], { cwd: checkoutCwd, env: childEnv, stdio: "inherit" });
119
- fs.rmSync(made.file, { force: true });
373
+ try { record({ ...base, status: "pending", nonce: made.carrier.nonce, ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd }); } catch (e) { removeCarrier(made.file); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
374
+ // GB-52: the call's operation deadline (the caller's own cap is never replaced) and an outer guard past the client's own cancel wait.
375
+ const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
376
+ const guard = clientGuardMs(childEnv);
377
+ const capture = stdio === "capture";
378
+ // Broker work may exist from the next statement on: the ownership records this request's sub-handle first (a mark that does not land sends nothing).
379
+ { const startable = clientStartable(section.client, section.platform); if (startable) { removeCarrier(made.file); return refuse("RK_CARGO_BROKER_CLIENT", `${startable}; nothing was marked or sent`); } }
380
+ try { markCarriedRequest({ section, env, callHandle: made.carrier.ownership.handle }); } catch (e) { removeCarrier(made.file); return refuse("RK_CARGO_BROKER_OWNERSHIP", e.message); }
381
+ const r = spawnBrokerClient(spawn, section.client, ["cargo", ...words], capture ? { cwd: spawnCwd, env: childEnv, encoding: "utf8", maxBuffer: 1 << 28, windowsHide: true, ...(guard ? { timeout: guard } : {}) } : { cwd: spawnCwd, env: childEnv, stdio: "inherit", ...(guard ? { timeout: guard } : {}) }, { platform: section.platform });
382
+ removeCarrier(made.file);
383
+ // GB-21: a client that provably never started sent nothing: that sub-handle is recorded unsent under the ownership lock, so it does not wedge a later reclaim. Any other
384
+ // failure after a start (a lost connection, a timeout, an exit before a reference) is ambiguous and stays held, reconciled by the request id.
385
+ if (r.error && provenNoSpawn(r.error)) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: `the RustRight client did not start (${r.error.code})` });
120
386
  let outcome;
121
387
  let ref = null;
122
388
  try {
123
- if (r.error) throw new SnapshotRequestError("spawn", `the RustRight client did not start (${r.error.message})`);
389
+ if (r.error) throw new SnapshotRequestError(r.error.code === BROKER_CLIENT_UNRESOLVED ? BROKER_CLIENT_UNRESOLVED : "spawn", `the RustRight client did not start (${r.error.message})`);
124
390
  ref = readRequestRef(path.join(refDir, "request.json"));
125
- const receipt = receiptSync({ client: section.client, requestId: ref.requestId, env: clientEnv, spawn });
391
+ const receipt = receiptSync({ client: section.client, requestId: ref.requestId, env: clientEnv, spawn, platform: section.platform });
126
392
  const observedEnv = observedEnvOf(clientEnv, section.platform);
127
- const { evidence, attribution, toolchain } = verifySnapshotReceipt(receipt, { pins: section.pins, jobId: ref.requestId, carrier: made.carrier, argv: words, workspace: ref.workspace, observedEnv, checkout: section.checkout, outputRoots: section.outputRoots, platform: section.platform });
393
+ const { evidence, attribution, toolchain, requestDigest } = verifySnapshotReceipt(receipt, { pins: section.pins, jobId: ref.requestId, carrier: made.carrier, argv: words, workspace: ref.workspace, observedEnv, checkout: section.checkout, outputRoots: section.outputRoots, platform: section.platform });
128
394
  // attribution and toolchain are recorded as the broker signed them (a consumer that needs per-package attribution treats available:false as FAILED).
129
- outcome = { status: "verified", requestId: ref.requestId, workspace: ref.workspace, observedEnv, outputDir: evidence.outputDir, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
395
+ outcome = { status: "verified", requestId: ref.requestId, requestDigest, workspace: ref.workspace, observedEnv, outputDir: evidence.outputDir, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
396
+ } catch (e) {
397
+ outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
398
+ }
399
+ const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
400
+ // GB-54: a request the broker REFUSED (admission paused, a suspended or unknown job, a cutover drain) never started: the client's typed REJECTED result is kept as the outcome's
401
+ // code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
402
+ if (outcome.status !== "verified" && capture) {
403
+ const refused = refusalInCapturedOutput(`${out.stdout}\n${out.stderr}`);
404
+ if (refused?.code === "RK_TOOLCHAIN_IDENTITY_UNAVAILABLE") outcome = { status: "unverified", code: "toolchain-unavailable", message: `the broker refused the compile before it started: it could not build the toolchain identity (RK_TOOLCHAIN_IDENTITY_UNAVAILABLE: ${refused.message}); nothing ran`, ...(outcome.requestId ? { requestId: outcome.requestId, workspace: outcome.workspace } : {}) };
405
+ else if (refused) outcome = { status: "unverified", code: `broker-refused-${refused.refusal}`, message: `the broker refused the request (${refused.code ?? "no code"}: ${refused.message || refused.refusal})${refusalAdvice(refused)}`, ...(outcome.requestId ? { requestId: outcome.requestId, workspace: outcome.workspace } : {}) };
406
+ }
407
+ // GB-52: a call that ran out its operation deadline, or whose client was stopped by the outer guard, is NOT a build outcome: the client cancelled by id and watched for the terminal
408
+ // state, and an unobserved state keeps ownership UNKNOWN (the sub-handle stays recorded; reconciled by the request id, never resubmitted). Busy and unavailable are named apart.
409
+ if (outcome.status !== "verified") {
410
+ const seen = operationOutcomeInOutput(`${out.stdout}\n${out.stderr}`) ?? (r.error?.code === "ETIMEDOUT" ? { code: "operation-deadline", deadlineMs: null, requestId: ref?.requestId ?? null, cancelConfirmed: null, terminalState: null, guard: true } : null);
411
+ if (seen) outcome = { status: "unverified", code: seen.code, message: seen.code === "operation-deadline" ? `the operation deadline${seen.deadlineMs ? ` of ${seen.deadlineMs} ms` : ""} passed${seen.guard ? " and the client was stopped by the gate's outer guard" : ""}; request ${seen.requestId ?? ref?.requestId ?? "unknown"}: cancelConfirmed: ${seen.cancelConfirmed ?? "unknown"}, terminal state: ${seen.terminalState ?? "not observed"}; the job is neither abandoned nor resubmitted and ownership stays UNKNOWN until every sub-handle is REAPED` : seen.code === "broker-busy" ? "every broker connection was busy (RK_BROKER_BUSY: a wait, not an outage); nothing was admitted" : "the broker was unavailable (RK_BROKER_UNAVAILABLE)", ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
412
+ }
413
+ if (outcome.status === "unverified" && outcome.code === "operation-deadline") {
414
+ const settled = reconcileAfterCancel({ section, callHandle: made.carrier.ownership.handle, childEnv });
415
+ outcome = { ...outcome, message: `${outcome.message}; ${settled.note}`, reaped: settled.reaped, subHandle: settled.subHandle };
416
+ }
417
+ // RK_BROKER_BUSY with no request reference: the connection never opened, so no byte reached the broker. That is the gate's own proof that this sub-handle was never sent (GB-21), recorded under
418
+ // the ownership lock so the retry's own sub-handle, not this one, is what the stage waits on; a busy answer after a reference exists (a reconnect) proves nothing of the kind.
419
+ if (outcome.status === "unverified" && outcome.code === "broker-busy" && ref === null) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: "every broker connection was busy (RK_BROKER_BUSY): the connection never opened and no byte was sent" });
420
+ // GB-53: captured output that lost a frame to FRAME_OVER_CAP (or a result the broker replaced with RK_RESPONSE_OVER_CAP) is truncated: refused, never read as complete.
421
+ if (outcome.status === "verified" && capture) {
422
+ const drained = drainFrames(`${out.stdout}\n${out.stderr}`);
423
+ const lost = drained.error ? { code: "frame-over-cap", message: drained.error } : drained.overCap.map(overCapProblem)[0];
424
+ if (lost) outcome = { status: "unverified", code: lost.code, message: lost.message, requestId: outcome.requestId, workspace: outcome.workspace };
425
+ }
426
+ record({ ...base, ...outcome, nonce: made.carrier.nonce, ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd, clientExit: r.status ?? null });
427
+ if (outcome.status !== "verified") {
428
+ const message = `rightkit-gate: RK_CARGO_BROKER_UNVERIFIED: \`cargo ${words.join(" ")}\` has no verified signed receipt (${outcome.code}: ${outcome.message}); the scan fails`;
429
+ err(message);
430
+ return { status: r.status ? r.status : BROKER_REFUSAL_STATUS, outcome, ...out, stderr: `${out.stderr}${message}\n` };
431
+ }
432
+ return { status: r.status ?? (r.signal ? 128 : BROKER_REFUSAL_STATUS), outcome, ...out };
433
+ } finally {
434
+ if (made) removeCarrier(made.file);
435
+ fs.rmSync(refDir, { recursive: true, force: true });
436
+ }
437
+ }
438
+
439
+ /**
440
+ * One carried adapter call: a pnpm, node or QA step of a snapshot stage as a broker adapter job (`rustright run <adapter> -- <words>`), with
441
+ * its own fresh (or, under a release-bound stage, derived) nonce and carrier. The step runs from `cwd`, a directory of the snapshot; the broker
442
+ * runs it from the primary checkout's counterpart of that directory (`checkout` + the path of `cwd` below the snapshot root, both from the gate's
443
+ * own `section`, never a Cargo workspace), and that directory is the carrier's `primaryCwd` and the client's own cwd (a per-call carrier, never a
444
+ * grant: a grant applies only to a call whose cwd is inside the grant's snapshot); the request's workspace is the primary toplevel. The carrier is removed after the call, the signed receipt is verified (its snapshotEvidence, its
445
+ * request digest over the adapter core, and its signed and mirrored `nestedChildren` list: every child exited 0 inside the snapshot, each with a managed
446
+ * output directory), and a ledger record is written either way. Returns `{ status, outcome, stdout, stderr }`; `outcome` is `verified` with the
447
+ * job's `outputDir` and the verified `nestedChildren`, or not.
448
+ * Refused before anything runs: a carrier or per-call carrier variable already in the environment (so a call from inside a broker job, which
449
+ * has a parent request, is refused), no calling gate, a working directory outside the snapshot or with no counterpart in the checkout.
450
+ */
451
+ export function carryBrokerAdapter(call) { return carryWithRetries(carryBrokerAdapterOnce, call); }
452
+
453
+ function carryBrokerAdapterOnce({ adapter, words, cwd, env, section, record, spawn = spawnSync, err, tmp = os.tmpdir(), stdio = "inherit", deadlineMs = null, ledgerId = null }) {
454
+ const id = ledgerId ?? crypto.randomUUID();
455
+ words = [...words].map(String);
456
+ const base = { id, mode: "broker", shim: "adapter", adapter, cwd, argv: [...words] };
457
+ const refuse = (code, message) => { record({ ...base, status: "refused", code, message }); err(`rightkit-gate: ${code}: ${message}`); return { status: BROKER_REFUSAL_STATUS, outcome: { status: "refused", code, message }, stdout: "", stderr: `rightkit-gate: ${code}: ${message}\n` }; };
458
+ const describe = `${adapter} ${words.join(" ")}`.trim();
459
+ // GB-67: the broker owns SwiftPM's scratch and build path; a caller-named one is refused before the carrier, the mark and the send.
460
+ if (adapter === "rightkit-swift") { const owned = swiftScratchRefusal(words); if (owned) return refuse(owned.code, owned.message); }
461
+ if (typeof adapter !== "string" || !/^[a-z][a-z0-9-]*$/.test(adapter)) return refuse("RK_ADAPTER_BROKER_ADAPTER", `${JSON.stringify(adapter)} is not a registered adapter name`);
462
+ const gate = env[CALLING_GATE_ENV];
463
+ if (typeof gate !== "string" || !gate.trim()) return refuse("RK_ADAPTER_BROKER_NO_GATE", `${CALLING_GATE_ENV} is not set: the call names no gate, and the carrier never forges one`);
464
+ const inherited = inheritedCarrierEnv(env);
465
+ if (inherited.length) return refuse("RK_ADAPTER_BROKER_INHERITED_ENV", `${inherited.join(", ")} ${inherited.length > 1 ? "are" : "is"} already in the environment; a carrier is set by the gate for one call only, so a nested or replayed call (or one from inside a broker job) is refused`);
466
+ // The client's environment: the stage nonce is read here and never forwarded (this call's nonce is derived from it); no output directory
467
+ // and no stage grant is forwarded either (the broker pins the output, and no grant exists).
468
+ const clientEnv = withoutManagedRustEnv(env);
469
+ for (const key of Object.keys(clientEnv)) if (/^(RUSTRIGHT|RIGHTKIT)_SNAPSHOT_GRANT$/i.test(key)) delete clientEnv[key];
470
+ const stageNonce = env[ATTEST_STAGE_NONCE_ENV];
471
+ for (const key of Object.keys(clientEnv)) if (key.toUpperCase() === ATTEST_STAGE_NONCE_ENV) delete clientEnv[key];
472
+ // The request environment the client will forward (RUSTRIGHT_ADAPTER_ENV and the variables it names): bound into the request core below and
473
+ // recorded in the ledger so the gate's own re-verification recomputes the same digest. A reserved or unset name is refused here, as the client would.
474
+ let adapterEnv;
475
+ try { adapterEnv = forwardedAdapterEnv(env); } catch (e) { return refuse(e.code ?? "RK_ADAPTER_ENV_INVALID", e.message); }
476
+ base.adapterEnv = adapterEnv;
477
+ let realCwd; let checkoutCwd; let rel;
478
+ try {
479
+ const snapshotRoot = fs.realpathSync.native(section.snapshot.path);
480
+ realCwd = fs.realpathSync.native(cwd);
481
+ rel = path.relative(snapshotRoot, realCwd);
482
+ if (escapes(rel)) throw new Error(`${realCwd} is not inside the snapshot ${snapshotRoot}`);
483
+ } catch (e) { return refuse("RK_ADAPTER_BROKER_CWD", `the step's working directory is not inside the snapshot (${e.message})`); }
484
+ const counterpart = checkoutCounterpart({ base: section.checkout, rel, checkout: section.checkout, codePrefix: "RK_ADAPTER_BROKER_COUNTERPART" });
485
+ if (counterpart.refusal) return refuse(counterpart.refusal.code, counterpart.refusal.message);
486
+ checkoutCwd = counterpart.dir; const spawnCwd = counterpart.spawnCwd;
487
+ let inputs;
488
+ try { inputs = readDeclaredInputs(section.snapshot.path); } catch (e) { return refuse("RK_ADAPTER_BROKER_DECLARED_INPUTS", e.message); }
489
+ const refDir = fs.mkdtempSync(path.join(tmp, "rk-snapreq-"));
490
+ let made = null;
491
+ try {
492
+ try {
493
+ const nonce = /^[0-9a-f]{64}$/.test(stageNonce ?? "") ? carriedCallNonce({ stageNonce, cwd: path.relative(section.checkout, checkoutCwd).split(path.sep).join("/") || ".", argv: words }) : undefined;
494
+ made = writeSnapshotRequest({ controlRoot: section.controlRoot, commit: section.commit, snapshot: section.snapshot, declaredInputs: inputs, handle: section.handle, primaryCwd: checkoutCwd, argv: words, nonce, requestIdFile: path.join(refDir, "request.json") });
495
+ } catch (e) { return refuse("RK_ADAPTER_BROKER_CARRIER", e.message); }
496
+ try { record({ ...base, status: "pending", nonce: made.carrier.nonce, ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd }); } catch (e) { removeCarrier(made.file); return refuse("RK_CARGO_LEDGER_ERROR", `the call cannot be recorded (${e.message}); it does not run`); }
497
+ // GB-52: the call's operation deadline (the caller's own cap is never replaced) and an outer guard past the client's own cancel wait.
498
+ const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
499
+ const guard = clientGuardMs(childEnv);
500
+ const capture = stdio === "capture";
501
+ // Broker work may exist from the next statement on: the ownership records this request's sub-handle first (a mark that does not land sends nothing).
502
+ { const startable = clientStartable(section.client, section.platform); if (startable) { removeCarrier(made.file); return refuse("RK_ADAPTER_BROKER_CLIENT", `${startable}; nothing was marked or sent`); } }
503
+ try { markCarriedRequest({ section, env, callHandle: made.carrier.ownership.handle }); } catch (e) { removeCarrier(made.file); return refuse("RK_ADAPTER_BROKER_OWNERSHIP", e.message); }
504
+ const r = spawnBrokerClient(spawn, section.client, ["run", adapter, "--", ...words], capture ? { cwd: spawnCwd, env: childEnv, encoding: "utf8", maxBuffer: 1 << 28, windowsHide: true, ...(guard ? { timeout: guard } : {}) } : { cwd: spawnCwd, env: childEnv, stdio: "inherit", ...(guard ? { timeout: guard } : {}) }, { platform: section.platform });
505
+ removeCarrier(made.file);
506
+ // GB-21: a client that provably never started sent nothing: that sub-handle is recorded unsent under the ownership lock, so it does not wedge a later reclaim. Any other
507
+ // failure after a start (a lost connection, a timeout, an exit before a reference) is ambiguous and stays held, reconciled by the request id.
508
+ if (r.error && provenNoSpawn(r.error)) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: `the RustRight client did not start (${r.error.code})` });
509
+ let outcome;
510
+ let ref = null;
511
+ let nativeManaged = null;
512
+ try {
513
+ if (r.error) throw new SnapshotRequestError(r.error.code === BROKER_CLIENT_UNRESOLVED ? BROKER_CLIENT_UNRESOLVED : "spawn", `the RustRight client did not start (${r.error.message})`);
514
+ ref = readRequestRef(path.join(refDir, "request.json"));
515
+ // The request's workspace of a snapshot-bound adapter job is the directory the client ran in: the primary checkout's counterpart of the step's
516
+ // snapshot directory (the broker, START: "the job runs where it was asked to (request.workspace, already the primary's counterpart of a
517
+ // directory in the snapshot), which must be inside that root"). It is the carrier's primaryCwd, never the toplevel for a step run from a subfolder
518
+ // (an app whose root is `app/`), and never a directory outside the primary checkout or one that is not this call's counterpart.
519
+ if (canonicalCounterpart(ref.workspace) !== canonicalCounterpart(checkoutCwd) || !inside(canonicalCounterpart(ref.workspace), section.checkout)) throw new SnapshotRequestError("workspace", `the request's workspace ${ref.workspace} is not the primary checkout counterpart ${checkoutCwd} of the step's directory ${realCwd} (primary toplevel ${section.checkout})`);
520
+ const receipt = receiptSync({ client: section.client, requestId: ref.requestId, env: clientEnv, spawn, platform: section.platform });
521
+ const { payload, evidence, attribution, toolchain, nestedChildren } = verifySnapshotReceipt(receipt, { pins: section.pins, jobId: ref.requestId, carrier: made.carrier, argv: words, workspace: ref.workspace, adapter, adapterEnv, checkout: section.checkout, outputRoots: section.outputRoots, platform: section.platform });
522
+ nativeManaged = signedNativeOutputs(receipt, payload);
523
+ outcome = { status: "verified", requestId: ref.requestId, workspace: ref.workspace, outputDir: evidence.outputDir, nestedChildren, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
130
524
  } catch (e) {
131
525
  outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
132
526
  }
133
- record({ ...base, ...outcome, nonce: made.carrier.nonce, checkoutCwd, clientExit: r.status ?? null });
527
+ const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
528
+ // GB-54: a request the broker REFUSED (admission paused, a suspended or unknown job, a cutover drain) never started: the client's typed REJECTED result is kept as the outcome's
529
+ // code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
530
+ if (outcome.status !== "verified" && capture) {
531
+ const refused = refusalInCapturedOutput(`${out.stdout}\n${out.stderr}`);
532
+ if (refused?.code === "RK_TOOLCHAIN_IDENTITY_UNAVAILABLE") outcome = { status: "unverified", code: "toolchain-unavailable", message: `the broker refused the compile before it started: it could not build the toolchain identity (RK_TOOLCHAIN_IDENTITY_UNAVAILABLE: ${refused.message}); nothing ran`, ...(outcome.requestId ? { requestId: outcome.requestId, workspace: outcome.workspace } : {}) };
533
+ else if (refused) outcome = { status: "unverified", code: `broker-refused-${refused.refusal}`, message: `the broker refused the request (${refused.code ?? "no code"}: ${refused.message || refused.refusal})${refusalAdvice(refused)}`, ...(outcome.requestId ? { requestId: outcome.requestId, workspace: outcome.workspace } : {}) };
534
+ }
535
+ // GB-52: a call that ran out its operation deadline, or whose client was stopped by the outer guard, is NOT a build outcome: the client cancelled by id and watched for the terminal
536
+ // state, and an unobserved state keeps ownership UNKNOWN (the sub-handle stays recorded; reconciled by the request id, never resubmitted). Busy and unavailable are named apart.
537
+ if (outcome.status !== "verified") {
538
+ const seen = operationOutcomeInOutput(`${out.stdout}\n${out.stderr}`) ?? (r.error?.code === "ETIMEDOUT" ? { code: "operation-deadline", deadlineMs: null, requestId: ref?.requestId ?? null, cancelConfirmed: null, terminalState: null, guard: true } : null);
539
+ if (seen) outcome = { status: "unverified", code: seen.code, message: seen.code === "operation-deadline" ? `the operation deadline${seen.deadlineMs ? ` of ${seen.deadlineMs} ms` : ""} passed${seen.guard ? " and the client was stopped by the gate's outer guard" : ""}; request ${seen.requestId ?? ref?.requestId ?? "unknown"}: cancelConfirmed: ${seen.cancelConfirmed ?? "unknown"}, terminal state: ${seen.terminalState ?? "not observed"}; the job is neither abandoned nor resubmitted and ownership stays UNKNOWN until every sub-handle is REAPED` : seen.code === "broker-busy" ? "every broker connection was busy (RK_BROKER_BUSY: a wait, not an outage); nothing was admitted" : "the broker was unavailable (RK_BROKER_UNAVAILABLE)", ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
540
+ }
541
+ if (outcome.status === "unverified" && outcome.code === "operation-deadline") {
542
+ const settled = reconcileAfterCancel({ section, callHandle: made.carrier.ownership.handle, childEnv });
543
+ outcome = { ...outcome, message: `${outcome.message}; ${settled.note}`, reaped: settled.reaped, subHandle: settled.subHandle };
544
+ }
545
+ // RK_BROKER_BUSY with no request reference: the connection never opened, so no byte reached the broker. That is the gate's own proof that this sub-handle was never sent (GB-21), recorded under
546
+ // the ownership lock so the retry's own sub-handle, not this one, is what the stage waits on; a busy answer after a reference exists (a reconnect) proves nothing of the kind.
547
+ if (outcome.status === "unverified" && outcome.code === "broker-busy" && ref === null) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: "every broker connection was busy (RK_BROKER_BUSY): the connection never opened and no byte was sent" });
548
+ // GB-53: captured output that lost a frame to FRAME_OVER_CAP (or a result the broker replaced with RK_RESPONSE_OVER_CAP) is truncated: refused, never read as complete.
549
+ if (outcome.status === "verified" && capture) {
550
+ const drained = drainFrames(`${out.stdout}\n${out.stderr}`);
551
+ const lost = drained.error ? { code: "frame-over-cap", message: drained.error } : drained.overCap.map(overCapProblem)[0];
552
+ if (lost) outcome = { status: "unverified", code: lost.code, message: lost.message, requestId: outcome.requestId, workspace: outcome.workspace };
553
+ }
554
+ record({ ...base, ...outcome, nonce: made.carrier.nonce, ownershipHandle: made.carrier.ownership.handle, checkoutCwd, spawnCwd, clientExit: r.status ?? null });
134
555
  if (outcome.status !== "verified") {
135
- err(`rightkit-gate: RK_CARGO_BROKER_UNVERIFIED: \`cargo ${words.join(" ")}\` has no verified signed receipt (${outcome.code}: ${outcome.message}); the scan fails`);
136
- return r.status ? r.status : BROKER_REFUSAL_STATUS;
556
+ const message = `rightkit-gate: RK_ADAPTER_BROKER_UNVERIFIED: \`${describe}\` has no verified signed receipt (${outcome.code}: ${outcome.message}); the stage fails`;
557
+ err(message);
558
+ return { status: r.status ? r.status : BROKER_REFUSAL_STATUS, outcome, ...out, stderr: `${out.stderr}${message}\n` };
137
559
  }
138
- return r.status ?? (r.signal ? 128 : BROKER_REFUSAL_STATUS);
560
+ // RA-07/RA-09: the native call's output mapping, as the broker SIGNED it (`executable.managedOutputs`); kept out of the ledger record, which is unchanged.
561
+ return { status: r.status ?? (r.signal ? 128 : BROKER_REFUSAL_STATUS), outcome, ...out, ...(nativeManaged ? { executable: { managedOutputs: nativeManaged } } : {}) };
139
562
  } finally {
140
- if (made) fs.rmSync(made.file, { force: true });
563
+ if (made) removeCarrier(made.file);
141
564
  fs.rmSync(refDir, { recursive: true, force: true });
142
565
  }
143
566
  }
144
567
 
568
+ /**
569
+ * The native build tool's output mapping of a verified adapter receipt (RustRight service.rs: a native call's `executable.managedOutputs` is `{ tool, state, outputs: [{ role, path }] }`, signed in
570
+ * `payload.executable` with `argv` removed, and mirrored at the receipt's `nativeOutputs`), or null when the call has none (xcodegen without `--use-cache`, every non-native adapter, an older broker).
571
+ * Only the SIGNED copy is read; a receipt mirror that is present must equal it, and a mirror with nothing signed is refused: the receipt's own say-so is never the mapping.
572
+ */
573
+ export function signedNativeOutputs(receipt, payload) {
574
+ const signed = payload?.executable?.managedOutputs;
575
+ const mirror = receipt?.nativeOutputs;
576
+ if (signed === undefined || signed === null) {
577
+ if (mirror !== undefined && mirror !== null) throw new SnapshotRequestError("evidence", "the receipt carries nativeOutputs that the signed payload does not (executable.managedOutputs): the mapping is not covered by the signature");
578
+ return null;
579
+ }
580
+ if (mirror !== undefined && mirror !== null && JSON.stringify(canonicalValue(mirror)) !== JSON.stringify(canonicalValue(signed))) throw new SnapshotRequestError("evidence", "receipt nativeOutputs differs from the signed executable.managedOutputs");
581
+ const shaped = signed !== null && typeof signed === "object" && typeof signed.tool === "string" && typeof signed.state === "string" && Array.isArray(signed.outputs)
582
+ && signed.outputs.every((o) => o !== null && typeof o === "object" && typeof o.role === "string" && typeof o.path === "string");
583
+ if (!shaped) throw new SnapshotRequestError("evidence", "the signed executable.managedOutputs is not { tool, state, outputs: [{ role, path }] }");
584
+ return signed;
585
+ }
586
+ const canonicalValue = (v) => (Array.isArray(v) ? v.map(canonicalValue) : v !== null && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, canonicalValue(v[k])])) : v);
587
+
588
+ /** One cargo call of a test, carried through the broker (`carryBrokerCargo`). Returns cargo's exit status. */
589
+ export const runBrokerCargo = (call) => carryBrokerCargo(call).status;
590
+
145
591
  /**
146
592
  * The gate's own check of every ledgered broker call of one scan. A call counts only when the broker's signed receipt, fetched
147
593
  * again now, verifies against THIS gate's commit, snapshot, ownership handle and pins, and its carrier was used once: a nonce or a
148
594
  * request id that appears twice in the run (`seen`) is a replay. Returns the problems; none means every call verified.
149
595
  */
150
- export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set() } } = {}) {
596
+ export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set(), handles: new Set() } } = {}) {
597
+ seen.handles ??= new Set();
151
598
  const problems = [];
152
599
  for (const e of entries) {
153
600
  if (e.mode !== "broker" || e.status !== "verified") continue;
154
- const call = `cargo ${(e.argv ?? []).join(" ")}`.trim();
601
+ const adapter = typeof e.adapter === "string" && e.adapter ? e.adapter : null;
602
+ const call = `${adapter ?? "cargo"} ${(e.argv ?? []).join(" ")}`.trim();
155
603
  try {
156
604
  if (typeof e.nonce !== "string" || typeof e.requestId !== "string" || !Array.isArray(e.argv) || !e.argv.every((a) => typeof a === "string") || typeof e.workspace !== "string" || typeof e.checkoutCwd !== "string") throw new SnapshotRequestError("ledger", "the record is incomplete");
157
605
  if (seen.nonces.has(e.nonce) || seen.ids.has(e.requestId)) throw new SnapshotRequestError("replay", "a carrier or request was used twice; each call carries its own fresh nonce and is used once");
158
- seen.nonces.add(e.nonce); seen.ids.add(e.requestId);
159
- const workspace = fs.realpathSync.native(e.workspace);
606
+ if (e.adapterEnv !== undefined && !(e.adapterEnv && typeof e.adapterEnv === "object" && !Array.isArray(e.adapterEnv) && Object.values(e.adapterEnv).every((v) => typeof v === "string"))) throw new SnapshotRequestError("ledger", "the record's adapter environment is not a map of strings");
607
+ if (adapter === null && e.adapterEnv !== undefined && Object.keys(e.adapterEnv).length) throw new SnapshotRequestError("ledger", "a cargo call carries no request environment");
608
+ // One lease handle per call: the sub-handle the gate sent (recorded with the call) is exactly one of this ownership's, used once in the run;
609
+ // the receipt must sign that very handle (verifySnapshotReceipt binds equality, never a prefix).
610
+ if (!isCallHandleOf(e.ownershipHandle, sb.handle)) throw new SnapshotRequestError("ledger", `the record names no sub-handle of the build ownership ${sb.handle}`);
611
+ if (seen.handles.has(e.ownershipHandle)) throw new SnapshotRequestError("replay", `lease handle ${e.ownershipHandle} was used by two calls; each carried request has its own`);
612
+ seen.nonces.add(e.nonce); seen.ids.add(e.requestId); seen.handles.add(e.ownershipHandle);
613
+ // GB-16: a counterpart the primary checkout no longer has is the lexical path the carrier named (the client names it as the workspace); it is judged by its nearest existing ancestor.
614
+ const workspace = canonicalCounterpart(e.workspace);
160
615
  if (!inside(workspace, sb.checkout)) throw new SnapshotRequestError("ledger", `the request ran in ${workspace}, outside the primary checkout`);
161
- if (fs.realpathSync.native(e.checkoutCwd) !== workspace) throw new SnapshotRequestError("ledger", "the request's working directory is not the one the call recorded");
616
+ if (adapter === null && canonicalCounterpart(e.checkoutCwd) !== workspace) throw new SnapshotRequestError("ledger", "the request's working directory is not the one the call recorded");
617
+ if (adapter !== null) {
618
+ // The workspace is the directory the client ran in, the carrier's primaryCwd; the step's snapshot directory and that checkout directory are the
619
+ // same path below their two roots.
620
+ if (workspace !== canonicalCounterpart(e.checkoutCwd)) throw new SnapshotRequestError("ledger", `the adapter request's workspace ${workspace} is not the primary checkout counterpart ${e.checkoutCwd} of the step's directory`);
621
+ const below = path.relative(fs.realpathSync.native(sb.snapshot.path), fs.realpathSync.native(e.cwd));
622
+ if (escapes(below)) throw new SnapshotRequestError("ledger", `the step ran from ${e.cwd}, outside the snapshot`);
623
+ if (path.relative(sb.checkout, canonicalCounterpart(e.checkoutCwd)) !== below) throw new SnapshotRequestError("ledger", `the carrier named ${e.checkoutCwd}, not the checkout counterpart of ${e.cwd}`);
624
+ }
162
625
  // The gate's own truth: its commit, snapshot, handle and declared inputs; only the call's own argv, nonce and cwd come from the ledger.
163
626
  const carrier = {
164
- schema: "rightkit.snapshot-request/1", commit: sb.commit, snapshot: sb.snapshot, declaredInputs: { inputs: readDeclaredInputs(sb.snapshot.path) },
165
- ownership: { handle: sb.handle }, primaryCwd: workspace, argvSha256: argvSha256(e.argv), nonce: e.nonce,
627
+ schema: SNAPSHOT_REQUEST_SCHEMA, commit: sb.commit, snapshot: sb.snapshot, declaredInputs: { inputs: readDeclaredInputs(sb.snapshot.path) },
628
+ ownership: { handle: e.ownershipHandle }, primaryCwd: e.checkoutCwd, argvSha256: argvSha256(e.argv), nonce: e.nonce,
166
629
  };
167
630
  const receipt = await sb.receiptOf(e.requestId);
168
- verifySnapshotReceipt(receipt, { pins: sb.pins, jobId: e.requestId, carrier, argv: e.argv, workspace: e.workspace, observedEnv: e.observedEnv ?? {}, checkout: sb.checkout, outputRoots: sb.outputRoots, platform: sb.platform });
631
+ const verified = verifySnapshotReceipt(receipt, { pins: sb.pins, jobId: e.requestId, carrier, argv: e.argv, workspace: e.workspace, observedEnv: e.observedEnv ?? {}, adapter, adapterEnv: e.adapterEnv ?? {}, checkout: sb.checkout, outputRoots: sb.outputRoots, platform: sb.platform });
632
+ if (adapter !== null) {
633
+ // The children the ledger records (the bundle root and the stage's output directories are read from them) are the signed ones, and no more.
634
+ if (canonicalJson(e.nestedChildren ?? null) !== canonicalJson(verified.nestedChildren)) throw new SnapshotRequestError("nested", "the nested children the call recorded are not the ones the broker's receipt signs");
635
+ for (const child of verified.nestedChildren) {
636
+ if (seen.ids.has(child.requestId)) throw new SnapshotRequestError("replay", `nested child ${child.requestId} was reported twice in this stage`);
637
+ seen.ids.add(child.requestId);
638
+ }
639
+ }
169
640
  } catch (error) { problems.push(`${call} (from ${e.cwd}): the gate's own verification of the broker's receipt failed (${error.code ?? "error"}: ${error.message})`); }
170
641
  }
171
642
  return problems;