@rightkit/release 0.6.24 → 0.6.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -35
- package/android-dev.mjs +2 -1
- package/app-check.mjs +1 -2
- package/app-closure.mjs +3 -2
- package/assembled-app.mjs +11 -3
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +83 -26
- package/build-helper-trace.mjs +264 -0
- package/build-helpers-command.mjs +184 -0
- package/build-launch.mjs +11 -75
- package/build-release.mjs +10 -11
- package/cargo-profile-contract.mjs +19 -29
- package/cargo-target-shim.mjs +31 -0
- package/cli/right-release.mjs +5 -7
- package/create-mac-updater.mjs +4 -0
- package/cross-targets.mjs +94 -0
- package/deterministic-scans.mjs +310 -20
- package/development.mjs +8 -8
- package/freshness-census.mjs +1 -1
- package/gate-stages.mjs +1 -1
- package/install-assembled-app.mjs +1 -1
- package/install-tauri-authenticode.mjs +3 -3
- package/local-publish.mjs +27 -5
- package/mac-package-recipe.mjs +55 -2
- package/managed-roots.mjs +12 -4
- package/package.json +1 -5
- package/pipeline-admission.mjs +1 -1
- package/pipeline-receipts.mjs +302 -302
- package/provenance-verify.mjs +3 -1
- package/qa-build-features.mjs +11 -2
- package/release-qa-audit.mjs +9 -27
- package/release-qa-command.mjs +23 -64
- package/release-qa-fixtures.mjs +9 -5
- package/release-qa-floor.json +0 -1
- package/release-qa-policy.mjs +9 -10
- package/release-qa-receipt.mjs +6 -44
- package/release-source-identity.mjs +1 -12
- package/release-switch-registry.json +4 -1
- package/rightkit-registry-resolver.mjs +35 -3
- package/rightkit-versions.json +2 -2
- package/sign-windows.mjs +1 -1
- package/snapshot-broker.mjs +33 -8
- package/snapshot-metadata.mjs +32 -0
- package/snapshot-request.mjs +6 -3
- package/stage-sidecar.mjs +28 -11
- package/suite-signing-policy.mjs +8 -58
- package/templates/cargo-profiles.toml +6 -25
- package/test-command.mjs +40 -140
- package/updater-shipped-key.mjs +26 -0
- package/web-build.mjs +29 -4
- package/qa-journeys-prebuilt-run.mjs +0 -18
- package/qa-journeys-prebuilt.mjs +0 -148
- package/qa-layout.mjs +0 -229
- package/qa-snapshot-run.mjs +0 -53
- package/qa-snapshot.mjs +0 -164
- package/sign-qa-bundle.mjs +0 -210
- package/suite-signing.mjs +0 -259
|
@@ -320,6 +320,9 @@ export async function resolveRightKitPins({
|
|
|
320
320
|
const observedAt = new Date(now()).toISOString();
|
|
321
321
|
const packuments = new Map();
|
|
322
322
|
const packument = (name) => { if (!packuments.has(name)) packuments.set(name, client.getJson(npmUrl(name), { accept: "application/vnd.npm.install-v1+json" })); return packuments.get(name); };
|
|
323
|
+
// The freshness bar needs publish dates: the abbreviated packument has no `time`, so the full one is read for the settle.
|
|
324
|
+
const fullPackuments = new Map();
|
|
325
|
+
const fullPackument = (name) => { if (!fullPackuments.has(name)) fullPackuments.set(name, client.getJson(npmUrl(name), { accept: "application/json" })); return fullPackuments.get(name); };
|
|
323
326
|
const indexes = new Map();
|
|
324
327
|
const index = (name) => { if (!indexes.has(name)) indexes.set(name, client.getJson(cargoIndexUrl(name), { text: true })); return indexes.get(name); };
|
|
325
328
|
|
|
@@ -379,17 +382,24 @@ export async function resolveRightKitPins({
|
|
|
379
382
|
};
|
|
380
383
|
await pool(names, async (entry) => {
|
|
381
384
|
try {
|
|
382
|
-
let value;
|
|
385
|
+
let value; let body;
|
|
383
386
|
if (entry.ecosystem === "npm") {
|
|
384
|
-
|
|
387
|
+
body = (await fullPackument(entry.name)).body;
|
|
388
|
+
value = body?.["dist-tags"]?.latest;
|
|
385
389
|
} else {
|
|
386
390
|
const response = await client.getJson(`${REGISTRIES.cargoApi}${entry.name}`);
|
|
391
|
+
body = response.body;
|
|
387
392
|
value = response.body?.crate?.max_stable_version;
|
|
388
393
|
}
|
|
389
394
|
if (typeof value !== "string" || !EXACT_SEMVER.test(value)) throw new RightKitRegistryError("registry-outage", `${entry.name} registry answer carries no usable latest version`, { recoverable: true });
|
|
390
395
|
const previous = cache.read("latest", entry.ecosystem, entry.name);
|
|
391
396
|
const mark = previous?.highWater?.version ?? previous?.latest;
|
|
392
|
-
|
|
397
|
+
// The freshness bar is the settled latest (published at least SETTLE_MS ago), never above the live latest.
|
|
398
|
+
const settled = settledLatest(entry.ecosystem, body);
|
|
399
|
+
// Fail closed: a registry answer without publish dates cannot prove a version is settled (never fall back to the live latest).
|
|
400
|
+
if (settled === null) throw new RightKitRegistryError("registry-outage", `${entry.name} registry answer carries no publish dates, so the 24-hour settle cannot be judged`, { recoverable: true });
|
|
401
|
+
const bar = settled && EXACT_SEMVER.test(settled) && compareVersions(settled, value) < 0 ? settled : value;
|
|
402
|
+
const row = { ecosystem: entry.ecosystem, name: entry.name, latest: bar, ...(bar !== value ? { liveLatest: value } : {}), observedAt, via: "live" };
|
|
393
403
|
let highWater = { version: value, observedAt };
|
|
394
404
|
if (typeof mark === "string" && EXACT_SEMVER.test(mark) && compareVersions(mark, value) > 0) {
|
|
395
405
|
if (entry.ecosystem === "npm" && await stillPublished(entry, mark)) {
|
|
@@ -586,6 +596,28 @@ export function verifiedVersionMaps(snapshot) {
|
|
|
586
596
|
return maps;
|
|
587
597
|
}
|
|
588
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Adrian, 2026-11-02: a pin is held to the newest version that has been published for at least 24 hours (`SETTLE_MS`), so a RightKit publish
|
|
601
|
+
* during an app's run never turns its pins stale. The registry's live latest is still recorded (`liveLatest`). Versions are the registry's own:
|
|
602
|
+
* crates.io `versions[]` ({num, created_at, yanked}), npm `time` + `versions` (deprecated skipped). When no stable version is that old, the
|
|
603
|
+
* oldest stable version is the floor (nothing newer is required yet). Returns null when the registry answer carries no dates.
|
|
604
|
+
*/
|
|
605
|
+
export const SETTLE_MS = 24 * 60 * 60 * 1000;
|
|
606
|
+
export function settledLatest(ecosystem, body, { now = Date.now(), settleMs = SETTLE_MS } = {}) {
|
|
607
|
+
let rows;
|
|
608
|
+
if (ecosystem === "npm") {
|
|
609
|
+
const times = body?.time ?? {}; const versions = body?.versions ?? {};
|
|
610
|
+
rows = Object.keys(versions).filter((v) => !versions[v]?.deprecated).map((v) => ({ v, at: Date.parse(times[v]) }));
|
|
611
|
+
} else {
|
|
612
|
+
rows = (body?.versions ?? []).filter((r) => r && !r.yanked).map((r) => ({ v: r.num, at: Date.parse(r.created_at) }));
|
|
613
|
+
}
|
|
614
|
+
rows = rows.filter((r) => typeof r.v === "string" && EXACT_SEMVER.test(r.v) && !r.v.includes("-") && Number.isFinite(r.at));
|
|
615
|
+
if (!rows.length) return null;
|
|
616
|
+
const byVersion = (a, b) => compareVersions(a.v, b.v);
|
|
617
|
+
const settled = rows.filter((r) => now - r.at >= settleMs).sort(byVersion);
|
|
618
|
+
return settled.length ? settled.at(-1).v : rows.sort(byVersion)[0].v;
|
|
619
|
+
}
|
|
620
|
+
|
|
589
621
|
// ---------------------------------------------------------------------------------------------- freshness + holds
|
|
590
622
|
|
|
591
623
|
function matchesPattern(name, pattern) {
|
package/rightkit-versions.json
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@rightkit/qa": "0.6.2",
|
|
50
50
|
"@rightkit/qa-chrome": "0.1.12",
|
|
51
51
|
"@rightkit/react-shell": "0.7.2",
|
|
52
|
-
"@rightkit/release": "0.6.
|
|
52
|
+
"@rightkit/release": "0.6.25",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|
|
@@ -607,7 +607,7 @@
|
|
|
607
607
|
"rightkit-playback": "0.3.0",
|
|
608
608
|
"rightkit-policy-rules": "0.1.0",
|
|
609
609
|
"rightkit-privileged-helper": "0.1.0",
|
|
610
|
-
"rightkit-qa": "0.10.
|
|
610
|
+
"rightkit-qa": "0.10.2",
|
|
611
611
|
"rightkit-qa-driver": "0.7.0",
|
|
612
612
|
"rightkit-record": "0.3.3",
|
|
613
613
|
"rightkit-reframe": "0.1.0",
|
package/sign-windows.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Windows Authenticode signing through Azure Artifact Signing. Two entries: the CLI (`node sign-windows.mjs [--dry-run|--verify-only] [--receipt <file>] <file>...`,
|
|
3
|
-
// unchanged) and the functions `signWindowsFiles` / `verifyWindowsFiles` that the gate's
|
|
3
|
+
// unchanged) and the functions `signWindowsFiles` / `verifyWindowsFiles` that the gate's signing steps reuse, so there is
|
|
4
4
|
// one signer and one verifier. The identity is the signer SUBJECT (WINDOWS_SIGNER_SUBJECT), never a thumbprint.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
package/snapshot-broker.mjs
CHANGED
|
@@ -49,9 +49,25 @@ export const CALLING_GATE_ENV = "RUSTRIGHT_CALLING_GATE";
|
|
|
49
49
|
/** The managed `ws` roots a broker pins its generation targets under (BROKER-CONTRACT output pinning). */
|
|
50
50
|
export const brokerOutputRoots = ({ dataRoot }) => [path.join(dataRoot, "rightkit-build-control", "ws")];
|
|
51
51
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The longest a read waits for a job that is still live although its client already exited (A7). The client can return before the broker
|
|
54
|
+
* has started the job (a race seen at 16 lanes), so one STATUS read of a live job is not a verdict: the read then follows the job with
|
|
55
|
+
* `rustright attach <id>` (the client's WATCH: it blocks until the job ends and rides out a broker restart; never a poll) and reads STATUS again.
|
|
56
|
+
*/
|
|
57
|
+
export const LIVE_WAIT_MS = 300_000;
|
|
58
|
+
const liveWaitOptions = (env, ms) => ({ ...statusOptions(env), timeout: ms });
|
|
59
|
+
const isLiveRead = (e) => e?.diagnosis?.kind === "live";
|
|
60
|
+
const stillLive = (e, waitedMs) => Object.assign(new SnapshotRequestError(e.code, `${e.message}; the read also waited up to ${Math.round(waitedMs / 1000)} s with \`rustright attach\` (WATCH) for the terminal state and the job was still live`), { diagnosis: e.diagnosis });
|
|
61
|
+
|
|
62
|
+
/** The broker's receipt of one request, read through the RustRight client (`rustright status <id>`), synchronously; a live job is waited for once, bounded (LIVE_WAIT_MS). */
|
|
63
|
+
export function receiptSync({ client, requestId, env, spawn = spawnSync, platform = process.platform, liveWaitMs = LIVE_WAIT_MS }) {
|
|
64
|
+
const read = () => receiptFromReply(spawnBrokerClient(spawn, client, ["status", requestId], statusOptions(env), { platform }), requestId);
|
|
65
|
+
try { return read(); } catch (e) {
|
|
66
|
+
if (!isLiveRead(e) || !(liveWaitMs > 0)) throw e;
|
|
67
|
+
// The attach result is not the verdict (its stream is not a receipt): whatever it ends with, the terminal state is read back by STATUS.
|
|
68
|
+
spawnBrokerClient(spawn, client, ["attach", requestId], liveWaitOptions(env, liveWaitMs), { platform });
|
|
69
|
+
try { return read(); } catch (again) { throw isLiveRead(again) ? stillLive(again, liveWaitMs) : again; }
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
const statusOptions = (env) => ({ encoding: "utf8", env, timeout: 60_000, maxBuffer: 64 * 1024 * 1024 });
|
|
57
73
|
|
|
@@ -63,7 +79,8 @@ function receiptFromReply(r, requestId) {
|
|
|
63
79
|
// 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).
|
|
64
80
|
try { doc = JSON.parse(String(r.stdout)); } catch { throw r.status !== 0 ? failedRun() : new SnapshotRequestError("receipt", `rustright status ${requestId} printed no JSON`); }
|
|
65
81
|
// The status envelope is `{type:"STATUS", id, state, ..., receipt}`: `receipt` is null while the job is live and full once it is
|
|
66
|
-
// terminal.
|
|
82
|
+
// terminal. A null receipt after the cargo process exited is not a verdict at once: the caller waits for the terminal state once (receiptSync:
|
|
83
|
+
// `rustright attach`, bounded, never a poll), and a job still live after that is a clear error, never a retry.
|
|
67
84
|
// GB-53: an over-cap outcome is a typed truncation, never a missing-field mystery.
|
|
68
85
|
const over = overCapProblem(doc);
|
|
69
86
|
if (over) throw new SnapshotRequestError(over.code, `rustright status ${requestId}: ${over.message}`);
|
|
@@ -386,7 +403,7 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
|
|
|
386
403
|
} catch (e) { return refuse("RK_CARGO_BROKER_CARRIER", e.message); }
|
|
387
404
|
try { record({ ...base, status: "pending", nonce: made.carrier.nonce, requestRef: path.join(refDir, "request.json"), 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`); }
|
|
388
405
|
// 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.
|
|
389
|
-
const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
|
|
406
|
+
const childEnv = { RUST_BACKTRACE: clientEnv.RUST_BACKTRACE ?? "1", ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
|
|
390
407
|
const guard = clientGuardMs(childEnv);
|
|
391
408
|
const capture = stdio === "capture";
|
|
392
409
|
// 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).
|
|
@@ -410,6 +427,7 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
|
|
|
410
427
|
} catch (e) {
|
|
411
428
|
outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
|
|
412
429
|
}
|
|
430
|
+
outcome = { ...outcome, clientStatus: r.status ?? null, clientSignal: r.signal ?? null }; // A7: the client's own exit, kept with every outcome
|
|
413
431
|
const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
|
|
414
432
|
// 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
|
|
415
433
|
// code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
|
|
@@ -595,7 +613,7 @@ function* carryBrokerAdapterSteps({ adapter, words, cwd, env, section, record, e
|
|
|
595
613
|
} catch (e) { return refuse("RK_ADAPTER_BROKER_CARRIER", e.message); }
|
|
596
614
|
try { record({ ...base, status: "pending", nonce: made.carrier.nonce, requestRef: path.join(refDir, "request.json"), 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`); }
|
|
597
615
|
// 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.
|
|
598
|
-
const childEnv = { ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
|
|
616
|
+
const childEnv = { RUST_BACKTRACE: clientEnv.RUST_BACKTRACE ?? "1", ...clientEnv, ...made.env, ...operationDeadlineEnv({ env: clientEnv, deadlineMs }) };
|
|
599
617
|
const guard = clientGuardMs(childEnv);
|
|
600
618
|
const capture = stdio === "capture";
|
|
601
619
|
// 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).
|
|
@@ -625,14 +643,21 @@ function* carryBrokerAdapterSteps({ adapter, words, cwd, env, section, record, e
|
|
|
625
643
|
// 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
|
|
626
644
|
// (an app whose root is `app/`), and never a directory outside the primary checkout or one that is not this call's counterpart.
|
|
627
645
|
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})`);
|
|
628
|
-
|
|
629
|
-
|
|
646
|
+
let statusReply = yield { args: ["status", ref.requestId], options: statusOptions(clientEnv) };
|
|
647
|
+
// A job still live although its client exited (A7): follow it once with `rustright attach` (WATCH, bounded), then read STATUS again; never a poll.
|
|
648
|
+
let liveWaited = false;
|
|
649
|
+
try { receiptFromReply(statusReply, ref.requestId); } catch (probe) {
|
|
650
|
+
if (isLiveRead(probe)) { yield { args: ["attach", ref.requestId], options: liveWaitOptions(clientEnv, LIVE_WAIT_MS) }; statusReply = yield { args: ["status", ref.requestId], options: statusOptions(clientEnv) }; liveWaited = true; }
|
|
651
|
+
}
|
|
652
|
+
let receipt;
|
|
653
|
+
try { receipt = receiptFromReply(statusReply, ref.requestId); } catch (again) { throw liveWaited && isLiveRead(again) ? stillLive(again, LIVE_WAIT_MS) : again; }
|
|
630
654
|
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 });
|
|
631
655
|
nativeManaged = signedNativeOutputs(receipt, payload);
|
|
632
656
|
outcome = { status: "verified", requestId: ref.requestId, workspace: ref.workspace, outputDir: evidence.outputDir, nestedChildren, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit, attribution, toolchain };
|
|
633
657
|
} catch (e) {
|
|
634
658
|
outcome = { status: "unverified", code: e.code ?? "error", message: e.message, ...(ref ? { requestId: ref.requestId, workspace: ref.workspace } : {}) };
|
|
635
659
|
}
|
|
660
|
+
outcome = { ...outcome, clientStatus: r.status ?? null, clientSignal: r.signal ?? null }; // A7: the client's own exit, kept with every outcome
|
|
636
661
|
const out = capture ? { stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") } : { stdout: "", stderr: "" };
|
|
637
662
|
// 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
|
|
638
663
|
// code ("admission is paused; nothing was spawned"), not folded into a missing request reference. It is never a build outcome.
|
package/snapshot-metadata.mjs
CHANGED
|
@@ -121,3 +121,35 @@ export function snapshotCargoSpawn({ spawn, carrySpawn = spawn, cargoCommand, sn
|
|
|
121
121
|
} catch (e) { return refused(e.code ?? "RK_SNAPSHOT_BROKER", e.message); }
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* GATE-CONTRACT GC-A11 (Adrian, 2026-11-02: "Class a never in the broker. Any cargo things that are cheap and non compiling are in class a,
|
|
127
|
+
* can run outside the broker."): the same read-only shapes as above, run DIRECTLY with the host's real toolchain cargo, never carried to the
|
|
128
|
+
* broker. `directPath` is the PATH with RustRight shims and outer gate wrappers taken off (managed-cargo.mjs directCargoPath), so `cargo`
|
|
129
|
+
* resolves to the pinned toolchain, not the broker's shim. Each call writes only to its own scratch target under `tmpRoot` (cargo would
|
|
130
|
+
* otherwise write target/.rustc_info.json beside the manifest and the snapshot would no longer be the commit's tree); the scratch is removed
|
|
131
|
+
* when the call returns. Every other cargo call inside the snapshot is refused (a compile belongs to the broker); calls outside the snapshot
|
|
132
|
+
* and every non-cargo command pass through to `spawn` unchanged. `spawn` may be synchronous or return a promise.
|
|
133
|
+
*/
|
|
134
|
+
export function directReadSpawn({ spawn, cargoCommand, snapshotRoot, directPath, pathKey = "PATH", tmpRoot, env, err = () => {} }) {
|
|
135
|
+
const prefix = cargoCommand.slice(1);
|
|
136
|
+
const refused = (code, message) => { err(`rightkit-gate: ${code}: ${message}`); return { status: BROKER_REFUSAL_STATUS, signal: null, error: null, stdout: "", stderr: `rightkit-gate: ${code}: ${message}\n` }; };
|
|
137
|
+
return (cmd, args, opts = {}) => {
|
|
138
|
+
const cargo = cmd === cargoCommand[0] && prefix.every((w, i) => args[i] === w);
|
|
139
|
+
if (!cargo || typeof opts.cwd !== "string" || !within(real(opts.cwd), real(snapshotRoot))) return spawn(cmd, args, opts);
|
|
140
|
+
const words = args.slice(prefix.length);
|
|
141
|
+
const shape = carriedScanRefusal(words);
|
|
142
|
+
if (shape !== null) return refused(CARRIED_METADATA_REFUSED, `\`cargo ${words.join(" ")}\` would run inside the managed snapshot ${snapshotRoot} and is not one of the read-only shapes Class A runs directly (GC-A11): ${shape}`);
|
|
143
|
+
let scratch;
|
|
144
|
+
try { fs.mkdirSync(tmpRoot, { recursive: true }); scratch = fs.mkdtempSync(path.join(tmpRoot, "rk-class-a-cargo-")); }
|
|
145
|
+
catch (e) { return refused("RK_CLASS_A_SCRATCH", `cannot create a scratch target under ${tmpRoot}: ${e.message}`); }
|
|
146
|
+
const childEnv = { ...env, ...(opts.env ?? {}), [pathKey]: directPath, CARGO_TARGET_DIR: scratch };
|
|
147
|
+
const cleanup = () => { try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best effort: a scratch dir under the gate's tmp root */ } };
|
|
148
|
+
let r;
|
|
149
|
+
try { r = spawn("cargo", words, { ...opts, env: childEnv }); }
|
|
150
|
+
catch (e) { cleanup(); throw e; }
|
|
151
|
+
if (r && typeof r.then === "function") return r.finally(cleanup);
|
|
152
|
+
cleanup();
|
|
153
|
+
return r;
|
|
154
|
+
};
|
|
155
|
+
}
|
package/snapshot-request.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import { REQUEST_OVER_CAP_CODE, requestOverCap } from "./broker-wire.mjs";
|
|
24
24
|
import { ownerFileOf, writeCarrierOwner } from "./carrier-census.mjs";
|
|
25
25
|
import { BROKER_COMPILE_COMMANDS, CARGO_COMMAND_ALIASES, cargoCommandIndex, cargoSubcommandOf, expandCargoAliases, isCompileCommand, isPackageObservation } from "./cargo-shapes.mjs";
|
|
26
|
-
import { canonicalJson, CAUSE_EVIDENCE_SCHEMA, CAUSE_UNEXPLAINED, CAUSE_UNKNOWN, ProvenanceError,
|
|
26
|
+
import { canonicalJson, CAUSE_EVIDENCE_SCHEMA, CAUSE_UNEXPLAINED, CAUSE_UNKNOWN, ProvenanceError, verifyBr19Evidence, verifyCauseEvidence, verifyProvenance, verifyReceiptCoverage, verifyResultEvidence } from "./provenance-verify.mjs";
|
|
27
27
|
|
|
28
28
|
export const SNAPSHOT_REQUEST_SCHEMA = "rustright.snapshot-request/1";
|
|
29
29
|
// One spelling per variable, the current one (rustright-cli shims.rs reads RUSTRIGHT_* first, then the legacy RIGHTKIT_* spelling, which task
|
|
@@ -646,8 +646,11 @@ export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, wor
|
|
|
646
646
|
// legitimately carries no census; when one carries cause evidence anyway it must still be well formed, but nothing is judged for it (nothing was rebuilt).
|
|
647
647
|
const signedFreshness = stage.freshness ?? null;
|
|
648
648
|
let causeEvidence = null;
|
|
649
|
-
|
|
650
|
-
|
|
649
|
+
// Adrian, 2026-11-02 (the broker owner's chat, checked with pulse `said`): "Refusal, remove but I want a daily job checking the warmth etc and
|
|
650
|
+
// reporting issues." An unexplained or unknown rebuild no longer fails a stage: the cause evidence is still read, checked well formed and
|
|
651
|
+
// recorded (the daily warmth check reports it), never judged here. The broker stopped refusing it too (rustright b00220f).
|
|
652
|
+
// A compile must still carry well-formed signed cause evidence (verifyCauseEvidence refuses a missing or incoherent one); its verdict is not judged.
|
|
653
|
+
if (compile || (isObject(signedFreshness) && signedFreshness.causeEvidence !== undefined)) causeEvidence = verifyCauseEvidence(signedFreshness);
|
|
651
654
|
// Fields the signature does not cover must still be present with their types (the consumer reads them). Every carried command:
|
|
652
655
|
// exit (an integer, or null with a signal) and signal, timingsMs{admission, queue, bind, cargo, afterExitBind, diagnostics} (each a
|
|
653
656
|
// number or null) and diagnosticsPath (a path, or null). Success needs exit 0 and no signal, as signed. `verdict` and `refreshed`
|
package/stage-sidecar.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
bundledFileName, planMediaSidecar, sourceOfferText, stagedFileName, validateMediaSidecarConfig, verifyStagedSidecars, writeMediaSidecarNotices,
|
|
47
47
|
} from "./media-sidecar.mjs";
|
|
48
48
|
import { ContentDigestError, contentIdentity, digestFor } from "./content-digest.mjs";
|
|
49
|
+
import { oneOutput } from "./cross-targets.mjs";
|
|
49
50
|
|
|
50
51
|
export const STAGE_SIDECAR_CONTRACT = "rightkit-stage-sidecar-v1";
|
|
51
52
|
export const STAGE_SIDECAR_RECEIPT_NAME = "stage-sidecar.json";
|
|
@@ -107,7 +108,7 @@ const NATIVE_OTHER_ROUTE = Object.freeze({ ffmpeg: "has its own kind: use { kind
|
|
|
107
108
|
/** A catalog version token (1.24.4, chromium/8021): only compared with the rows, never used as a path. */
|
|
108
109
|
const NATIVE_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+/-]{0,63}$/;
|
|
109
110
|
const KEYS = Object.freeze({
|
|
110
|
-
"cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "manifestPath", "sha256", "dest", "clean", "platforms"],
|
|
111
|
+
"cargo-bin": ["kind", "package", "bin", "name", "profile", "features", "devFeatures", "manifestPath", "sha256", "dest", "clean", "platforms"],
|
|
111
112
|
ffmpeg: ["kind", "variant", "variants", "tools", "notices", "catalog", "stage", "bundleDir", "resourceDir", "sources", "probe", "platforms"],
|
|
112
113
|
onnxruntime: ["kind", "version", "catalog", "resourceDir", "as", "licenseDir", "cacheDir", "companions", "platforms"],
|
|
113
114
|
"native-library": ["kind", "library", "version", "catalog", "resourceDir", "licenseDir", "cacheDir", "as", "files", "companions", "source", "platforms"],
|
|
@@ -190,6 +191,8 @@ export function resolveBuildMode(env = process.env) {
|
|
|
190
191
|
|
|
191
192
|
/** A profile is a Cargo profile name or exactly { dev, release } of names. Returns the pair or the single name. */
|
|
192
193
|
function parseProfile(value, label, errors) {
|
|
194
|
+
// One build output (GATE-CONTRACT GC-A15): a sidecar builds only the release profile; any other profile, or a { dev, release } pair, is refused.
|
|
195
|
+
if (value !== "release") { errors.push(`${label} must be "release" (no other Cargo profile is allowed, GC-A15); omit it, release is the default`); return "release"; }
|
|
193
196
|
if (typeof value === "string") {
|
|
194
197
|
if (!PROFILE.test(value)) errors.push(`${label} must be a Cargo profile name`);
|
|
195
198
|
return value;
|
|
@@ -329,8 +332,9 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
329
332
|
if (!isObject(wrapper)) fail("STAGE_SIDECAR_CONFIG_INVALID", "sidecars must be an array (or { sidecars, stageDir?, receiptPath?, profiles? })");
|
|
330
333
|
for (const key of Object.keys(wrapper)) if (!["sidecars", "stageDir", "receiptPath", "profiles", "wipeStageDir"].includes(key)) errors.push(`unknown key ${JSON.stringify(key)}`);
|
|
331
334
|
if (wrapper.wipeStageDir !== undefined && typeof wrapper.wipeStageDir !== "boolean") errors.push("wipeStageDir must be a boolean");
|
|
332
|
-
|
|
333
|
-
if (
|
|
335
|
+
// The { dev, release } profile pair is retired (GC-A15): every sidecar builds release; a `profiles` key is refused.
|
|
336
|
+
if (wrapper.profiles !== undefined) errors.push("profiles is not allowed: every sidecar builds the release profile (GC-A15); remove the key");
|
|
337
|
+
const defaultProfile = undefined;
|
|
334
338
|
if (!Array.isArray(wrapper.sidecars) || !wrapper.sidecars.length) errors.push("sidecars must be a non-empty array");
|
|
335
339
|
const stageDir = relativePath(wrapper.stageDir ?? DEFAULT_STAGE_DIR, "stageDir", errors);
|
|
336
340
|
const receiptPath = wrapper.receiptPath === undefined
|
|
@@ -374,8 +378,13 @@ export function validateSidecarManifest(raw, { target, mode = "release" } = {})
|
|
|
374
378
|
if (typeof name !== "string" || !NAME.test(name) || /\.exe$/i.test(name) || name !== path.basename(name)) errors.push(`${where}: name must be a bare sidecar name (no path, no .exe, no target triple)`);
|
|
375
379
|
const declared = item.profile !== undefined ? parseProfile(item.profile, `${where}: profile`, errors) : (defaultProfile ?? "release");
|
|
376
380
|
const profile = isObject(declared) ? declared[mode] : declared;
|
|
377
|
-
|
|
378
|
-
|
|
381
|
+
// `devFeatures` are added only for the dev build (RIGHTKIT_BUILD_MODE=dev, set by the dev lane): the app's QA feature, e.g. qa-native.
|
|
382
|
+
// The Cargo profile is release either way (GC-A15); the release tool refuses those features in a release (release-switch-registry.json).
|
|
383
|
+
const devFeatures = item.devFeatures ?? [];
|
|
384
|
+
const badList = (list) => !Array.isArray(list) || list.some((f) => typeof f !== "string" || !/^[A-Za-z0-9_/-]+$/.test(f));
|
|
385
|
+
if (badList(item.features ?? [])) errors.push(`${where}: features must be a list of feature names (package/feature allowed)`);
|
|
386
|
+
if (badList(devFeatures)) errors.push(`${where}: devFeatures must be a list of feature names (package/feature allowed)`);
|
|
387
|
+
const features = [...(item.features ?? []), ...(mode === "dev" && Array.isArray(devFeatures) ? devFeatures : [])];
|
|
379
388
|
const manifestPath = item.manifestPath === undefined ? undefined : relativePath(item.manifestPath, `${where}: manifestPath`, errors);
|
|
380
389
|
sha(item.sha256, "sha256");
|
|
381
390
|
const dest = destOf(item.dest);
|
|
@@ -961,7 +970,14 @@ function stageCargoBin(entry, ctx) {
|
|
|
961
970
|
fs.chmodSync(temporary, 0o755);
|
|
962
971
|
fs.renameSync(temporary, dest);
|
|
963
972
|
carryMtime(ctx, dest, slices.map((slice) => slice.source));
|
|
964
|
-
|
|
973
|
+
// rightkit#1443.2: Tauri's universal build compiles each arch and resolves an externalBin per THIN triple, so each slice is also staged as
|
|
974
|
+
// `<name>-<thin triple>` beside the universal file (the pre-gate R2 build staged all three). Only for an externalBin (no `dest`).
|
|
975
|
+
const thin = entry.dest ? [] : slices.map((slice) => {
|
|
976
|
+
const thinDest = path.resolve(ctx.appRoot, ctx.stageDir, stagedName(entry.name, parseTargetTriple(slice.triple)));
|
|
977
|
+
const copied = copyInto(ctx, slice.source, thinDest, { executable: true });
|
|
978
|
+
return { kind: "cargo-bin", name: entry.name, path: rel(ctx, thinDest), ...copied, sourceSha256: slice.sourceSha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: false, sliceOf: rel(ctx, dest), triple: slice.triple };
|
|
979
|
+
});
|
|
980
|
+
return [{ kind: "cargo-bin", name: entry.name, path: rel(ctx, dest), sha256, sizeBytes: fs.statSync(dest).size, changed, sourceSha256: sha256, package: entry.package, bin: entry.bin, profile: entry.profile, features: entry.features, externalBin: !entry.dest, slices: slices.map((slice) => ({ triple: slice.triple, sha256: slice.sourceSha256 })) }, ...thin];
|
|
965
981
|
}
|
|
966
982
|
|
|
967
983
|
function stageCargoSlice(entry, ctx, triple, { dest }) {
|
|
@@ -974,9 +990,10 @@ function stageCargoSlice(entry, ctx, triple, { dest }) {
|
|
|
974
990
|
/** Build one slice through `rustright cargo` and locate the executable. Returns { source, sourceSha256 }. */
|
|
975
991
|
function stageCargoBuild(entry, ctx, triple) {
|
|
976
992
|
const { run, appRoot, env, fs } = ctx;
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
993
|
+
// One output directory (oneOutput, cross-targets.mjs): a sidecar always builds with the release profile, whatever profile its entry names.
|
|
994
|
+
const args = oneOutput(["cargo", "build", "--locked", ...(entry.manifestPath ? ["--manifest-path", path.resolve(appRoot, entry.manifestPath)] : []),
|
|
995
|
+
"-p", entry.package, "--bin", entry.bin, "--target", triple,
|
|
996
|
+
...(entry.features.length ? ["--features", entry.features.join(",")] : []), "--message-format=json-render-diagnostics"]);
|
|
980
997
|
ctx.log(`stage-sidecar: rightkit ${args.join(" ")}`);
|
|
981
998
|
// From a snapshot, Cargo would resolve `target/` (or a relative build.target-dir) against the snapshot: its output directory is
|
|
982
999
|
// resolved in the checkout instead and passed as an absolute CARGO_TARGET_DIR, so the warm outputs stay the checkout's and the
|
|
@@ -996,7 +1013,7 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
996
1013
|
if (carried && (!source || !fs.existsSync(source))) {
|
|
997
1014
|
const targetDir = typeof built.outputDir === "string" && path.isAbsolute(built.outputDir) ? built.outputDir : undefined;
|
|
998
1015
|
if (!targetDir) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and the receipt names no output directory`, { bin: entry.bin });
|
|
999
|
-
const candidates = builtBinaryCandidates({ targetDir, targetDirs: [], triple, profile:
|
|
1016
|
+
const candidates = builtBinaryCandidates({ targetDir, targetDirs: [], triple, profile: "release", bin: entry.bin, windows: ctx.windows });
|
|
1000
1017
|
source = candidates.find((file) => fs.existsSync(file));
|
|
1001
1018
|
if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build did not produce ${entry.bin} (looked in ${candidates.join(", ")})`, { bin: entry.bin, candidates });
|
|
1002
1019
|
} else if (!source || !fs.existsSync(source)) {
|
|
@@ -1008,7 +1025,7 @@ function stageCargoBuild(entry, ctx, triple) {
|
|
|
1008
1025
|
// authoritative; an explicit CARGO_TARGET_DIR in this environment is tried too, resolved against the app root.
|
|
1009
1026
|
const envDir = typeof buildEnv.CARGO_TARGET_DIR === "string" && buildEnv.CARGO_TARGET_DIR.trim() ? path.resolve(appRoot, buildEnv.CARGO_TARGET_DIR) : undefined;
|
|
1010
1027
|
if (!targetDir && !envDir) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `cargo reported no ${entry.bin} executable and its target directory cannot be read`, { bin: entry.bin });
|
|
1011
|
-
const candidates = builtBinaryCandidates({ targetDir, targetDirs: envDir ? [envDir] : [], triple, profile:
|
|
1028
|
+
const candidates = builtBinaryCandidates({ targetDir, targetDirs: envDir ? [envDir] : [], triple, profile: "release", bin: entry.bin, windows: ctx.windows });
|
|
1012
1029
|
source = candidates.find((file) => fs.existsSync(file));
|
|
1013
1030
|
if (!source) fail("STAGE_SIDECAR_BINARY_NOT_FOUND", `the build did not produce ${entry.bin} (looked in ${candidates.join(", ")})`, { bin: entry.bin, candidates });
|
|
1014
1031
|
}
|
package/suite-signing-policy.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// The signing policy of a gate-run test or dev build (rightkit#1303, GATE-CONTRACT "One signing identity per product, every build"). Pure: it reads
|
|
2
|
-
// nothing but what its caller hands it, so the gate's route decision (rightkit-gate.mjs routePlan, from the commit's own bytes)
|
|
3
|
-
// (suite-signing.mjs, from the snapshot) and the tests all judge the same text.
|
|
2
|
+
// nothing but what its caller hands it, so the gate's route decision (rightkit-gate.mjs routePlan, from the commit's own bytes) and the tests all judge the same text.
|
|
4
3
|
//
|
|
5
4
|
// - The identity is derived, never declared: the app's tauri.conf.json `identifier` is the one id. Mac install.bundleId must equal it, and a
|
|
6
5
|
// dev or test Tauri `--config` that overrides it is refused. The team is DEFAULT_TEAM, the signer subject WINDOWS_SIGNER_SUBJECT.
|
|
7
6
|
// - A suite that builds a product (tauri build/bundle, or a cargo build of the tauri package or a sidecar package) must be named by a later `sign`
|
|
8
|
-
// step; a Windows dev `install.kind: "tauri"` is refused (`tauri-authenticode` is the signed route);
|
|
9
|
-
// (ad hoc)
|
|
7
|
+
// step; a Windows dev `install.kind: "tauri"` is refused (`tauri-authenticode` is the signed route); RIGHTKIT_DEV_SIGNING
|
|
8
|
+
// (ad hoc) is refused.
|
|
10
9
|
// - These refusals apply on Mac and Dell only (process.platform darwin or win32). Linux builds sign nothing and hosted CI is exempt.
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import { createRequire } from "node:module";
|
|
@@ -14,14 +13,6 @@ import { DEFAULT_TEAM, WINDOWS_SIGNER_SUBJECT } from "./signing-identity.mjs";
|
|
|
14
13
|
|
|
15
14
|
export const SIGN_TEAM = DEFAULT_TEAM;
|
|
16
15
|
export const SIGN_SUBJECT = WINDOWS_SIGNER_SUBJECT;
|
|
17
|
-
/** The environment variable a successful `sign` step sets for the suites after it: the signed copy of the first file. */
|
|
18
|
-
export const QA_APP_BINARY_ENV = "RIGHTKIT_QA_APP_BINARY";
|
|
19
|
-
/** The environment variable a successful `sign` step sets beside it: the path of the signing evidence file (signed files, sha256 before and after, identity). */
|
|
20
|
-
export const QA_SIGNING_EVIDENCE_ENV = "RIGHTKIT_QA_SIGNING_EVIDENCE";
|
|
21
|
-
/** GB-42: set beside the two above: a JSON object mapping each file a `sign` step signed (its `path`, relative to the output layout) to its signed copy, so a
|
|
22
|
-
* consumer is handed every declared sidecar's signed bytes, not only the main binary's. */
|
|
23
|
-
export const QA_SIGNED_FILES_ENV = "RIGHTKIT_QA_SIGNED_FILES";
|
|
24
|
-
export const SIGN_STEP_KEYS = Object.freeze(["name", "sign"]);
|
|
25
16
|
export const WINDOWS_TAURI_KIND = "tauri";
|
|
26
17
|
export const WINDOWS_SIGNED_KIND = "tauri-authenticode";
|
|
27
18
|
|
|
@@ -44,45 +35,8 @@ export class SuiteSigningError extends Error {
|
|
|
44
35
|
}
|
|
45
36
|
const refuse = (code, message, details) => { throw new SuiteSigningError(code, message, details); };
|
|
46
37
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
47
|
-
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
48
38
|
const SIGNING_ID = /^[A-Za-z0-9][A-Za-z0-9.-]{0,254}$/;
|
|
49
39
|
|
|
50
|
-
/** A relative path with no `..`, no drive, no root: the form a sign file path takes inside a receipt's output directory. Returns it with `/`. */
|
|
51
|
-
export function containedRelative(value, label) {
|
|
52
|
-
if (typeof value !== "string" || !value.trim()) refuse("SIGN_STEP_PATH", `${label} must be a non-empty relative path`);
|
|
53
|
-
const normalized = value.replaceAll("\\", "/");
|
|
54
|
-
if (path.isAbsolute(value) || path.win32.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.startsWith("/") || normalized.split("/").includes("..") || normalized.split("/").includes("")) {
|
|
55
|
-
refuse("SIGN_STEP_PATH", `${label} must be a relative path inside the earlier suite's output directory (no absolute path, no .., no empty segment): ${JSON.stringify(value)}`);
|
|
56
|
-
}
|
|
57
|
-
return normalized;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Validate one `sign` spec: `{ files: [{ from, path }] }`. `from` names an EARLIER suite of the same list (`earlier` is the set of names declared
|
|
62
|
-
* before this step), `path` is relative to that suite's carried-receipt output directory. Returns the spec with normalized paths.
|
|
63
|
-
*/
|
|
64
|
-
export function validateSignSpec(sign, label, earlier) {
|
|
65
|
-
if (!isObject(sign)) refuse("SIGN_STEP_SHAPE", `${label} must be an object { files: [{ from, path }] }`);
|
|
66
|
-
const unknown = Object.keys(sign).filter((key) => key !== "files");
|
|
67
|
-
if (unknown.length) refuse("SIGN_STEP_SHAPE", `${label} has unknown key(s): ${unknown.join(", ")} (allowed: files). The identity is derived from the product, never declared here`);
|
|
68
|
-
if (!Array.isArray(sign.files) || sign.files.length === 0) refuse("SIGN_STEP_SHAPE", `${label}.files must be a non-empty list of { from, path }`);
|
|
69
|
-
const seen = new Set();
|
|
70
|
-
const files = sign.files.map((item, index) => {
|
|
71
|
-
const at = `${label}.files[${index}]`;
|
|
72
|
-
if (!isObject(item)) refuse("SIGN_STEP_SHAPE", `${at} must be { from, path }`);
|
|
73
|
-
const extra = Object.keys(item).filter((key) => key !== "from" && key !== "path");
|
|
74
|
-
if (extra.length) refuse("SIGN_STEP_SHAPE", `${at} has unknown key(s): ${extra.join(", ")} (allowed: from, path)`);
|
|
75
|
-
if (typeof item.from !== "string" || !NAME.test(item.from)) refuse("SIGN_STEP_FROM", `${at}.from must name an earlier suite`);
|
|
76
|
-
if (!earlier.has(item.from)) refuse("SIGN_STEP_FROM", `${at}.from ${JSON.stringify(item.from)} is not a suite declared before this step (${[...earlier].join(", ") || "none"}); a sign step signs what an earlier suite built`);
|
|
77
|
-
const rel = containedRelative(item.path, `${at}.path`);
|
|
78
|
-
const key = `${item.from}\0${rel}`;
|
|
79
|
-
if (seen.has(key)) refuse("SIGN_STEP_SHAPE", `${at} repeats ${item.from}:${rel}`);
|
|
80
|
-
seen.add(key);
|
|
81
|
-
return { from: item.from, path: rel };
|
|
82
|
-
});
|
|
83
|
-
return { files };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
40
|
// ---- what a step is -------------------------------------------------------------------------------------------------------------
|
|
87
41
|
|
|
88
42
|
const base = (program) => path.basename(String(program ?? "").replaceAll("\\", "/")).replace(/\.(exe|cmd|bat)$/i, "").toLowerCase();
|
|
@@ -391,13 +345,11 @@ function signPathNames(file, layout, bin) {
|
|
|
391
345
|
}
|
|
392
346
|
|
|
393
347
|
/**
|
|
394
|
-
* GB-42 (GATE 42, E19; stable identity before launch): why
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* declaration names the right paths. The supported route is: compile only (`cargo test --no-run`, a build), sign, then a consumer that is handed the
|
|
398
|
-
* signed path (the QA runner, which reads RIGHTKIT_QA_APP_BINARY and RIGHTKIT_QA_SIGNED_FILES) and runs prebuilt bytes.
|
|
348
|
+
* GB-42 (GATE 42, E19; stable identity before launch): why `cargo run` or an integration test that uses CARGO_BIN_EXE_ is never safe in a build
|
|
349
|
+
* stage. Cargo resolves the product bin inside the build's output directory, so such a step starts the raw, unsigned bytes in the target directory.
|
|
350
|
+
* Product bins run only from the installed, signed build (GATE-CONTRACT GC-A07: QA journeys run after the build against the installed dev copy).
|
|
399
351
|
*/
|
|
400
|
-
export const RAW_TARGET_LAUNCH = "launches a product bin Cargo resolves inside the build's target directory (cargo run, or an integration test using CARGO_BIN_EXE_): the signer signs a copy elsewhere and nothing redirects Cargo or a compile-time literal to it, so
|
|
352
|
+
export const RAW_TARGET_LAUNCH = "launches a product bin Cargo resolves inside the build's target directory (cargo run, or an integration test using CARGO_BIN_EXE_): the signer signs a copy elsewhere and nothing redirects Cargo or a compile-time literal to it, so the bytes that run are never the signed ones. Product bins run only from the installed, signed build (GATE-CONTRACT GC-A07)";
|
|
401
353
|
|
|
402
354
|
/**
|
|
403
355
|
* Launch binding (rightkit#1303): the earlier sign steps that satisfy a launching suite must name, in `sign.files`, each executable it launches. `launch` is
|
|
@@ -435,11 +387,9 @@ export function isProductBuildStep(step, targets = { packages: new Set(), manife
|
|
|
435
387
|
return found.product || found.launches || found.unknown.length > 0;
|
|
436
388
|
}
|
|
437
389
|
|
|
438
|
-
/** Why a step asks for ad hoc signing (the
|
|
390
|
+
/** Why a step asks for ad hoc signing (the ad hoc opt-in), or null. */
|
|
439
391
|
export function adHocReason(step) {
|
|
440
392
|
const words = stepWords(step);
|
|
441
|
-
const bad = words.find((word) => /sign-qa-bundle/.test(word));
|
|
442
|
-
if (bad !== undefined) return `${JSON.stringify(bad)} runs the ad hoc QA bundle signer (sign-qa-bundle)`;
|
|
443
393
|
const named = words.find((word) => /RIGHTKIT_DEV_SIGNING/.test(word));
|
|
444
394
|
if (named !== undefined) return `${JSON.stringify(named)} names RIGHTKIT_DEV_SIGNING`;
|
|
445
395
|
if (isObject(step?.env) && Object.keys(step.env).some((key) => key === "RIGHTKIT_DEV_SIGNING")) return "its env sets RIGHTKIT_DEV_SIGNING (ad hoc signing)";
|
|
@@ -1,28 +1,9 @@
|
|
|
1
|
-
# RightKit standard Cargo
|
|
2
|
-
# One
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
[profile.dev]
|
|
8
|
-
opt-level = 1
|
|
9
|
-
debug = "line-tables-only"
|
|
10
|
-
strip = "none"
|
|
11
|
-
debug-assertions = true
|
|
12
|
-
overflow-checks = true
|
|
13
|
-
lto = "off"
|
|
14
|
-
panic = "unwind"
|
|
15
|
-
incremental = true
|
|
16
|
-
codegen-units = 256
|
|
17
|
-
rpath = false
|
|
18
|
-
|
|
19
|
-
# Dependencies: non-incremental so sccache can cache them across apps.
|
|
20
|
-
[profile.dev.package."*"]
|
|
21
|
-
incremental = false
|
|
22
|
-
codegen-units = 16
|
|
23
|
-
|
|
24
|
-
# No test-profile overrides: test inherits dev.
|
|
25
|
-
# The retired release-iterate profile must not appear: delete it and its package overrides.
|
|
1
|
+
# RightKit standard Cargo profile for every app workspace (Adrian, 2026-10-16; GATE-CONTRACT GC-A01, GC-A07).
|
|
2
|
+
# One build output and one warm cache per app per host: every gate compile (check, clippy, rustdoc, the test compile and
|
|
3
|
+
# both installers, dev and release) uses [profile.release] for the host's target (cross-targets.mjs oneOutput). The dev
|
|
4
|
+
# installer is the release profile plus `qa-native`. An app carries no [profile.dev], [profile.test] or other custom
|
|
5
|
+
# profile: the RightKit ownership scanner refuses them. `release-iterate` is retired. Panic behaviour is the same in every
|
|
6
|
+
# app: unwind. Keep the app's existing [profile.release] optimization keys, but set panic = "unwind".
|
|
26
7
|
|
|
27
8
|
[profile.release]
|
|
28
9
|
panic = "unwind"
|