@rightkit/release 0.6.21 → 0.6.23
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/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +52 -14
- package/broker-wire.mjs +4 -0
- package/carried-cargo.mjs +2 -2
- package/deterministic-scans.mjs +10 -2
- package/disk-command.mjs +4 -1
- package/gate-stages.mjs +10 -5
- package/local-publish.mjs +5 -1
- package/package-recipe-targets.mjs +12 -2
- package/package.json +1 -1
- package/rightkit-versions.json +1 -1
- package/snapshot-broker.mjs +28 -3
- package/snapshot-request.mjs +32 -16
- package/stage-ledger.mjs +5 -2
- package/stage-sidecar-command.mjs +14 -5
- package/stage-sidecar.mjs +43 -2
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -1642,7 +1642,13 @@ async function runStages(o, args, root, git, io) {
|
|
|
1642
1642
|
ok = true;
|
|
1643
1643
|
if (ds) ok = await stageWebBuildOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1644
1644
|
if (ok && ds) ok = await stageWindowsIdentityOnce({ o, ds, root, git, appDir, cli, commonDir, commit, tree, stage, executor });
|
|
1645
|
-
if (ok && ds)
|
|
1645
|
+
if (ok && ds) {
|
|
1646
|
+
// The package triple: a Mac recipe with `stageSidecars` builds for its own triple (universal-apple-darwin by default), so the gate stages that triple here, under its own
|
|
1647
|
+
// producer carrier, and the recipe's prepare step (`stage-sidecar --target <triple>`) then finds it current and changes nothing.
|
|
1648
|
+
const packageConfigs = sidecarConfigsOf();
|
|
1649
|
+
const packageTarget = await packageSidecarTarget({ o, ds, configs: packageConfigs });
|
|
1650
|
+
ok = await stageSidecarsOnce({ o, ds, root, git, configs: packageConfigs, appDir, cli, commonDir, commit, tree, stage, executor, target: packageTarget });
|
|
1651
|
+
}
|
|
1646
1652
|
// Source QA (right-release qa --stage source) exists for mac and win only; a mobile package is the signed build alone.
|
|
1647
1653
|
// Row 7.04c (R-T1, 7.09): the snapshot holds no Cargo target. On a direct host the checkout's output directory is resolved here, in the primary
|
|
1648
1654
|
// checkout, and handed to right-release absolute: it binds every manifest's target for this stage, and the sealed manifest must record it back
|
|
@@ -1944,6 +1950,16 @@ async function restagedSidecars({ o, ds, prior, now }) {
|
|
|
1944
1950
|
return out;
|
|
1945
1951
|
}
|
|
1946
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* The --target the package stage stages sidecars for: `package` on a Mac host, which right-release stage-sidecar resolves itself from the
|
|
1955
|
+
* snapshot's config (the Mac package recipe's own triple, e.g. universal-apple-darwin), so this gate never imports app code; null elsewhere (host).
|
|
1956
|
+
*/
|
|
1957
|
+
export async function packageSidecarTarget({ o, ds, configs = [] }) {
|
|
1958
|
+
if (o.platform !== "darwin" || !ds) return null;
|
|
1959
|
+
// Read as text, never imported (as committedSidecarConfigs does): only a config whose Mac package recipe stages sidecars gets a package-triple staging.
|
|
1960
|
+
return configs.some((config) => { try { return /\bstageSidecars\b/.test(o.readFile(path.join(ds.path, config))); } catch { return false; } }) ? "package" : null;
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1947
1963
|
/** The release configs of `commit` (under `appDir`) that declare `sidecars`, read from `base` (the snapshot on the snapshot route): the commit's config decides, never the checkout's. */
|
|
1948
1964
|
export function committedSidecarConfigs({ o, git, commit, appDir = ".", base }) {
|
|
1949
1965
|
const listedConfigs = git("ls-tree", "-r", "--name-only", "-z", commit);
|
|
@@ -1957,9 +1973,14 @@ export function committedSidecarConfigs({ o, git, commit, appDir = ".", base })
|
|
|
1957
1973
|
* step's additions must equal the files it reported plus its receipt (anything else is an unattributed write, RK_CHECK_ERROR). The
|
|
1958
1974
|
* staged files become immutable `staged` ledger entries. Returns true when nothing is owed or staging passed.
|
|
1959
1975
|
*/
|
|
1960
|
-
export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:" }) {
|
|
1961
|
-
|
|
1962
|
-
|
|
1976
|
+
export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appDir, cli, commonDir, commit, tree, stage, executor = null, tag = "rightkit-gate:", target = null }) {
|
|
1977
|
+
// Once per door run PER TARGET TRIPLE: `target` null is the host (the compile stage: stage-sidecar's own default, no `--target` passed), anything else is the triple the
|
|
1978
|
+
// package recipe stages for (universal-apple-darwin on a Mac recipe with `stageSidecars`), passed to stage-sidecar as `--target`. stage-sidecar is a no-op for a target whose
|
|
1979
|
+
// receipt and files are already current, so the recipe's own prepare step afterwards changes nothing.
|
|
1980
|
+
const staged = ds.sidecarsStaged instanceof Set ? ds.sidecarsStaged : (ds.sidecarsStaged = new Set());
|
|
1981
|
+
const key = target ?? "host";
|
|
1982
|
+
if (staged.has(key)) return true;
|
|
1983
|
+
staged.add(key);
|
|
1963
1984
|
if (!configs.length) return true;
|
|
1964
1985
|
const { attributionProblems, reportedPaths } = await import("../stage-ledger.mjs");
|
|
1965
1986
|
const source = sourceSplitOf({ root, ds, appDir });
|
|
@@ -1967,13 +1988,13 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1967
1988
|
for (const config of configs) {
|
|
1968
1989
|
const before = ds.ledger?.entries ?? [];
|
|
1969
1990
|
// The stage-sidecar child's Cargo calls (its builds and metadata queries) are carried on a broker host (carried-cargo.mjs).
|
|
1970
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
1991
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
1971
1992
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; nothing was staged`); return false; }
|
|
1972
1993
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
1973
1994
|
let r;
|
|
1974
1995
|
try {
|
|
1975
|
-
o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)} (from the snapshot ${ds.path})`);
|
|
1976
|
-
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
1996
|
+
o.out(`${tag} [${stage}] right-release stage-sidecar --config ${path.join(ds.path, config)}${target ? ` --target ${target}` : ""} (from the snapshot ${ds.path})`);
|
|
1997
|
+
r = (o.spawnCapture ?? ((cmd, a, opts) => spawnInherit(cmd, a, { ...opts, encoding: "utf8", maxBuffer: 1 << 26 })))(process.execPath, [cli, "stage-sidecar", "--config", path.join(ds.path, config), ...(target ? ["--target", target] : []), "--json"], { cwd: snapshotAppRootOf(ds.path, appDir), env: carriedEnv(carrier, { ...cleanEnv, ...run.env }) });
|
|
1977
1998
|
} finally { endRun(run); }
|
|
1978
1999
|
const carriedOk = await carrier.finish({ stage });
|
|
1979
2000
|
if (r?.stderr) o.err(String(r.stderr).trimEnd());
|
|
@@ -1989,7 +2010,7 @@ export async function stageSidecarsOnce({ o, ds, root, git = null, configs, appD
|
|
|
1989
2010
|
const configDir = path.posix.dirname(String(config).replaceAll("\\", "/"));
|
|
1990
2011
|
const underApp = (rel) => (configDir === "." ? rel : `${configDir}/${rel}`).replaceAll("\\", "/");
|
|
1991
2012
|
const receiptAbs = typeof result.receiptPath === "string" ? path.relative(ds.path, result.receiptPath).split(path.sep).join("/") : null;
|
|
1992
|
-
const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs })];
|
|
2013
|
+
const problems = [...walked.problems, ...attributionProblems({ before, after: walked.entries, reported: [...reportedPaths(result)].map(underApp), receipt: receiptAbs, removable: before.filter((e) => e.kind === "staged" && e.producer?.stage === "sidecar" && e.producer?.step === config).map((e) => e.path) /* only THIS config's earlier staged files (Oracle: another config's build must never delete them unreported) */ })];
|
|
1993
2014
|
if (problems.length) { for (const p of problems.slice(0, 10)) o.err(`${tag} RK_CHECK_ERROR: [${stage}] stage-sidecar ${config}: ${p}`); return false; }
|
|
1994
2015
|
const priorPaths = new Set(before.map((e) => e.path));
|
|
1995
2016
|
const priorByPath = new Map(before.map((e) => [e.path, e]));
|
|
@@ -2048,7 +2069,7 @@ export async function stageWebBuildOnce({ o, ds, root, git = null, appDir, cli,
|
|
|
2048
2069
|
for (const config of configs) {
|
|
2049
2070
|
if (!absent().length) break;
|
|
2050
2071
|
const before = ds.ledger?.entries ?? [];
|
|
2051
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2072
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
2052
2073
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the web build did not run`); return false; }
|
|
2053
2074
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2054
2075
|
let r;
|
|
@@ -2100,7 +2121,7 @@ export async function stageWindowsIdentityOnce({ o, ds, root, git = null, appDir
|
|
|
2100
2121
|
const producer = path.join(path.dirname(cli), "..", "windows-identity.mjs");
|
|
2101
2122
|
for (const config of configs) {
|
|
2102
2123
|
const before = ds.ledger?.entries ?? [];
|
|
2103
|
-
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag });
|
|
2124
|
+
const carrier = await carrierForChildren({ o, ds, root, git, executor, stage, tag, producer: true });
|
|
2104
2125
|
if (carrier.refusal) { o.err(`${tag} BLOCKED: ${carrier.refusal}; the Windows identity package did not build`); return false; }
|
|
2105
2126
|
const run = startRun({ commonDir, commit, tree, stage, source });
|
|
2106
2127
|
let r;
|
|
@@ -2731,7 +2752,7 @@ async function snapshotCargoWorkspaces({ git, ds, root }) {
|
|
|
2731
2752
|
* child's environment; `finish` runs after the child ended, fails on any call that was refused, unverified or unfinished and on any
|
|
2732
2753
|
* receipt the gate cannot verify again itself, and removes the carrier. A direct executor, or no snapshot, has nothing to carry.
|
|
2733
2754
|
*/
|
|
2734
|
-
async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:" }) {
|
|
2755
|
+
async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "rightkit-gate:", producer = false }) {
|
|
2735
2756
|
const none = { env: {}, carried: false, refusal: null, async finish() { return true; }, cleanup() {} };
|
|
2736
2757
|
if (!ds || executor?.kind !== "broker") return none;
|
|
2737
2758
|
let base = null;
|
|
@@ -2740,6 +2761,15 @@ async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "ri
|
|
|
2740
2761
|
const carried = await import("../carried-cargo.mjs");
|
|
2741
2762
|
const { brokerSection } = await import("../snapshot-broker.mjs");
|
|
2742
2763
|
const sb = await carrierBroker({ o, root, ds, executor });
|
|
2764
|
+
// Only a PRODUCER carrier (the gate's own web-build, windows-identity and stage-sidecar steps, which write declared inputs by design) takes the gate's digest of the
|
|
2765
|
+
// declared inputs at stage open (absent included); the end-of-stage ledger re-verify then accepts any receipt state for an input that digest says the step changed.
|
|
2766
|
+
// Every other carrier passes none: each receipt, adapter receipts included, must equal the stage-end digest of every declared input (strict), so a child that swaps
|
|
2767
|
+
// an input around an admission and restores it is refused. A malformed declaration or an unreadable input is the refusal below, as before.
|
|
2768
|
+
let openInputDigests = null;
|
|
2769
|
+
if (producer) {
|
|
2770
|
+
const { readDeclaredInputs: readInputsOf, declaredInputDigestsOf: digestInputsOf } = await import("../snapshot-request.mjs");
|
|
2771
|
+
openInputDigests = digestInputsOf(ds.path, readInputsOf(ds.path), { allowAbsent: true });
|
|
2772
|
+
}
|
|
2743
2773
|
// Installing the carrier map sends nothing, so it records nothing: the child's own uncarried probes (`rustc -vV`, `--version`) never
|
|
2744
2774
|
// reach the broker under a snapshot, and each carried request records its sub-handle just before it is sent (snapshot-broker.mjs).
|
|
2745
2775
|
const checkoutDirs = await snapshotCargoWorkspaces({ git, ds, root });
|
|
@@ -2758,7 +2788,7 @@ async function carrierForChildren({ o, ds, root, git, executor, stage, tag = "ri
|
|
|
2758
2788
|
finished = true;
|
|
2759
2789
|
let problems;
|
|
2760
2790
|
try {
|
|
2761
|
-
problems = await installed.problems(sb);
|
|
2791
|
+
problems = await installed.problems(sb, openInputDigests ? { openInputDigests } : {});
|
|
2762
2792
|
// The output directories the stage's verified receipts name: the package stage's sealed manifest must record only these.
|
|
2763
2793
|
for (const dir of installed.outputDirs?.() ?? []) ((ds.carriedByStage ??= {})[at] ??= new Set()).add(dir);
|
|
2764
2794
|
// And by workspace, for the next stage's children: a receipt already names the output directory, so none asks Cargo for it.
|
|
@@ -4055,12 +4085,20 @@ export async function main(argv, io = {}) {
|
|
|
4055
4085
|
const unitByName = new Map(rust.units.map((u) => [u.name, u]));
|
|
4056
4086
|
const known = new Set(unitByName.keys());
|
|
4057
4087
|
const builtBins = {};
|
|
4058
|
-
|
|
4088
|
+
// Where Cargo put them: `<dir>/debug` on a direct host, `<dir>/<triple>/debug` on a broker host, whose jobs build with --target <triple> (the receipt
|
|
4089
|
+
// names it, `targetTriple`). A broker row with no triple records nothing for its directory: a guessed path could vouch for a stale binary.
|
|
4090
|
+
const addBins = (dir, unit, profile) => {
|
|
4091
|
+
if (!dir || !profile || !unit?.testLinksBins || !unit.bins?.length) return;
|
|
4092
|
+
const prior = builtBins[dir];
|
|
4093
|
+
if (prior && prior.profile !== profile) { builtBins[dir] = { profile: null, bins: [] }; return; }
|
|
4094
|
+
builtBins[dir] = { profile, bins: [...new Set([...(prior?.bins ?? []), ...unit.bins])].sort() };
|
|
4095
|
+
};
|
|
4059
4096
|
for (const r of scanResult.scans) {
|
|
4060
4097
|
// A row answered from the scan cache compiled nothing in this run: the bytes in the target are not vouched for by it.
|
|
4061
4098
|
if (r.status !== "pass" || r.cached === true || !/^test:rust(?:-serial)?:/.test(String(r.id))) continue;
|
|
4062
4099
|
const unit = unitByName.get(rustUnitOfScanId(r.id, known));
|
|
4063
|
-
|
|
4100
|
+
if (snapshotBroker) addBins(r.snapshotEvidence?.outputDir, unit, r.snapshotEvidence?.targetTriple ? `${r.snapshotEvidence.targetTriple}/debug` : null);
|
|
4101
|
+
else addBins(targetDirs?.[unit?.cargoDir ?? "."], unit, "debug");
|
|
4064
4102
|
}
|
|
4065
4103
|
const outputWorkspaces = (snapshotBroker ? scanResult.scans.map((r) => r.snapshotEvidence?.outputDir) : Object.values(targetDirs ?? {})).filter(Boolean).map((dir) => [dir]);
|
|
4066
4104
|
o.onCompileOutputs?.(outputWorkspaces.map(([dir]) => dir), builtBins);
|
package/broker-wire.mjs
CHANGED
|
@@ -217,6 +217,10 @@ export function refusalInCapturedOutput(text) {
|
|
|
217
217
|
const diagnosis = diagnoseControlReply(event);
|
|
218
218
|
if (diagnosis.kind === "refused") return { code: diagnosis.code, refusal: diagnosis.refusal, retryable: diagnosis.retryable === true, message: diagnosis.message };
|
|
219
219
|
}
|
|
220
|
+
// The client's plain-text form of the same refusal (its stderr without --json): `rustright: admission is paused; resume with rustright resume-admission`.
|
|
221
|
+
// Without it a paused broker read as "no request reference" (HeardRight Mac, 2026-11-02).
|
|
222
|
+
const paused = /^rustright: (admission is paused\b[^\r\n]*)/m.exec(String(text ?? ""));
|
|
223
|
+
if (paused) return { code: "RK_ADMISSION_PAUSED", refusal: "paused", retryable: false, message: paused[1].trim() };
|
|
220
224
|
return null;
|
|
221
225
|
}
|
|
222
226
|
|
package/carried-cargo.mjs
CHANGED
|
@@ -136,10 +136,10 @@ function finish(ledgerDir, dir, workspaces = {}) {
|
|
|
136
136
|
const checkoutRoots = Object.values(workspaces);
|
|
137
137
|
return {
|
|
138
138
|
/** Every call of the stage that is not a completed, verified success, and every verified call the gate cannot verify again itself. */
|
|
139
|
-
async problems(sb) {
|
|
139
|
+
async problems(sb, opts = {}) {
|
|
140
140
|
const { entries, problems } = readLedger(ledgerDir, UNSCOPED);
|
|
141
141
|
const out = [...problems];
|
|
142
|
-
if (sb) out.push(...await verifyBrokerLedger(entries, sb));
|
|
142
|
+
if (sb) out.push(...await verifyBrokerLedger(entries, sb, opts));
|
|
143
143
|
return out;
|
|
144
144
|
},
|
|
145
145
|
/** The verified calls' output directories, in call order (the receipts'; an adapter job's nested children's included). */
|
package/deterministic-scans.mjs
CHANGED
|
@@ -37,7 +37,7 @@ import { observedEnvOf, readRequestRef, verifySnapshotReceipt, writeSnapshotRequ
|
|
|
37
37
|
import { SCAN_TOKEN_ENV, appendLedger, readLedger, scanTokenOf, unclaimedLedgers } from "./cargo-ledger.mjs";
|
|
38
38
|
import { brokerSection, carryBrokerAdapterAsync, verifyBrokerLedger } from "./snapshot-broker.mjs";
|
|
39
39
|
import { ADMISSION_POLL_MS, CARRIED_CANCEL_GRACE_MS, QUEUED_CEILING_FACTOR, createBrokerLive, createRunClock, createStatusPoller, watchBrokerAdmission } from "./broker-admission.mjs";
|
|
40
|
-
import { CANCEL_WAIT_ENV, operationDeadlineEnv } from "./broker-wire.mjs";
|
|
40
|
+
import { CANCEL_WAIT_ENV, operationDeadlineEnv, refusalInCapturedOutput } from "./broker-wire.mjs";
|
|
41
41
|
import { removeCarrier } from "./carrier-census.mjs";
|
|
42
42
|
|
|
43
43
|
export const SCAN_RESULT_SCHEMA = "rightkit.deterministic-scans/1";
|
|
@@ -2755,11 +2755,19 @@ export async function runScans(plan, options = {}) {
|
|
|
2755
2755
|
// The broker's signed account of that request: the receipt verifies against this host's pins and its snapshotEvidence names
|
|
2756
2756
|
// this commit, this snapshot unchanged from START to exit, this ownership and the output directory. Anything less fails the row.
|
|
2757
2757
|
if (carrier) {
|
|
2758
|
+
// A typed broker refusal with no request reference (admission paused, a cutover drain): nothing was admitted under this scan's sub-handle, so it is
|
|
2759
|
+
// recorded unsent at once, as a busy connection is; otherwise a dead owner's record could never be reclaimed (HeardRight Mac, 2026-11-02).
|
|
2760
|
+
if (!fs.existsSync(path.join(carrier.refDir, "request.json"))) {
|
|
2761
|
+
const refused = refusalInCapturedOutput(`${r?.stdout ?? ""}\n${r?.stderr ?? ""}`);
|
|
2762
|
+
if (refused && typeof deps.snapshotBroker?.markBrokerUnsent === "function") {
|
|
2763
|
+
try { deps.snapshotBroker.markBrokerUnsent(carrier.made.carrier.ownership.handle, `the broker refused the submit before admitting it (${refused.code}): nothing was spawned`); } catch { /* the reclaim then judges the sub-handle as before */ }
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2758
2766
|
try {
|
|
2759
2767
|
const ref = readRequestRef(path.join(carrier.refDir, "request.json"));
|
|
2760
2768
|
const receipt = await deps.snapshotBroker.receiptOf(ref.requestId);
|
|
2761
2769
|
const { evidence } = verifySnapshotReceipt(receipt, { pins: deps.snapshotBroker.pins, jobId: ref.requestId, carrier: carrier.made.carrier, argv: carrier.cargoArgv, workspace: ref.workspace, observedEnv: observedEnvOf({ ...deps.env, ...s.env, ...(extraEnv ?? {}) }, deps.platform ?? process.platform), checkout: deps.snapshotBroker.checkout, outputRoots: deps.snapshotBroker.outputRoots, platform: deps.snapshotBroker.platform ?? deps.platform ?? process.platform });
|
|
2762
|
-
row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
|
|
2770
|
+
row.snapshotEvidence = { requestId: ref.requestId, outputDir: evidence.outputDir, cargoDir: s.cwd ?? ".", targetTriple: typeof receipt?.targetTriple === "string" && /^[A-Za-z0-9_.]+(?:-[A-Za-z0-9_.]+){1,4}$/.test(receipt.targetTriple) ? receipt.targetTriple : null, treeAtStart: evidence.snapshot.treeAtStart, treeAtExit: evidence.snapshot.treeAtExit };
|
|
2763
2771
|
} catch (e) {
|
|
2764
2772
|
status = "fail"; code = "SNAPSHOT_EVIDENCE"; detail = `${s.id}: the broker's snapshot evidence did not verify (${e.code ?? "error"}: ${e.message})`;
|
|
2765
2773
|
}
|
package/disk-command.mjs
CHANGED
|
@@ -210,7 +210,10 @@ function defaultGlobalRoots() {
|
|
|
210
210
|
return roots;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
// The node a scheduled job runs is the real file, never a version manager's link to it: nvm-windows' C:\\nvm4w\\nodejs is a symlink into the user profile that a
|
|
214
|
+
// scheduled task context cannot always follow (the Dell task failed with 0x80070003, path not found, 2026-11-02).
|
|
215
|
+
const realNode = () => { try { return realpathSync.native(process.execPath); } catch { return process.execPath; } };
|
|
216
|
+
export function scheduleSpec({ platform = process.platform, nodePath = realNode(), cli = path.join(path.dirname(fileURLToPath(import.meta.url)), "cli", "right-release.mjs"), home = os.homedir() } = {}) {
|
|
214
217
|
if (platform === "darwin") {
|
|
215
218
|
// launchd cannot open log files on an external volume for a background job (spawn fails with EX_CONFIG);
|
|
216
219
|
// `disk run` writes its own structured log to disk-hygiene.jsonl on the heavy volume.
|
package/gate-stages.mjs
CHANGED
|
@@ -234,10 +234,11 @@ export function compileKeyOf(env = {}) {
|
|
|
234
234
|
export const COMPILE_PROFILE_DIR = "debug";
|
|
235
235
|
|
|
236
236
|
/** How `binaries` was chosen: only the bin targets the compile built in that directory (a consumer requires this marker). */
|
|
237
|
-
export const BUILT_BINARIES_SCHEMA = "built-bin-targets/
|
|
237
|
+
export const BUILT_BINARIES_SCHEMA = "built-bin-targets/2";
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* The sha256 of each binary this compile built (rustright#209 item 5): for every output directory, the files `<dir
|
|
240
|
+
* The sha256 of each binary this compile built (rustright#209 item 5): for every output directory, the files `<dir>/<profile>/<bin>[.exe]` (`debug`, or
|
|
241
|
+
* `<triple>/debug` for a broker build with --target; each row's `path` names it, schema built-bin-targets/2) of the
|
|
241
242
|
* bin targets the compile built there (`builtBins`, { [dir]: [bin target name] }: the units whose scans ran `cargo build -p <crate>` in that
|
|
242
243
|
* directory's workspace, so each binary was rebuilt or proven fresh by Cargo for this commit). Any other file in `debug/`, a stale binary of a
|
|
243
244
|
* removed or renamed target or of a package this run did not build, is never recorded. A census proves nothing once truncated (a broker
|
|
@@ -247,12 +248,16 @@ export const BUILT_BINARIES_SCHEMA = "built-bin-targets/1";
|
|
|
247
248
|
export function compiledBinaries(builtBins = {}, { platform = process.platform, stat = (f) => fs.statSync(f), readFile = (f) => fs.readFileSync(f) } = {}) {
|
|
248
249
|
const out = [];
|
|
249
250
|
for (const dir of Object.keys(builtBins ?? {}).filter((d) => typeof d === "string" && d).sort()) {
|
|
250
|
-
|
|
251
|
+
// { profile, bins }: profile is the directory under `dir` Cargo wrote them to (`debug`, or `<triple>/debug` for a --target build); a bare array is `debug`.
|
|
252
|
+
const entry = Array.isArray(builtBins[dir]) ? { profile: COMPILE_PROFILE_DIR, bins: builtBins[dir] } : builtBins[dir] ?? {};
|
|
253
|
+
if (typeof entry.profile !== "string" || !/^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)?$/.test(entry.profile) || entry.profile.split("/").some((c) => c === "." || c === "..")) continue;
|
|
254
|
+
for (const bin of [...new Set(entry.bins ?? [])].sort()) {
|
|
251
255
|
const name = `${bin}${platform === "win32" ? ".exe" : ""}`;
|
|
252
|
-
const
|
|
256
|
+
const rel = `${entry.profile}/${name}`;
|
|
257
|
+
const file = path.join(dir, ...rel.split("/"));
|
|
253
258
|
let info = null;
|
|
254
259
|
try { info = stat(file); } catch { info = null; }
|
|
255
|
-
out.push(info?.isFile?.() ? { dir, name, size: info.size, sha256: sha256(readFile(file)) } : { dir, name, size: null, sha256: null });
|
|
260
|
+
out.push(info?.isFile?.() ? { dir, path: rel, name, size: info.size, sha256: sha256(readFile(file)) } : { dir, path: rel, name, size: null, sha256: null });
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
return out;
|
package/local-publish.mjs
CHANGED
|
@@ -368,6 +368,10 @@ export function localTestPlan({ kind, unit, dir, config, manifest, batch = false
|
|
|
368
368
|
// Every tested feature set is linted (-D warnings), not only the first.
|
|
369
369
|
if (!batch) for (const features of featureSets) steps.push({ command: "cargo", args: ["clippy", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", "--all-targets", ...features, "--", "-D", "warnings"], test: false });
|
|
370
370
|
for (const features of allFeatureCheckSets(entry)) steps.push({ command: "cargo", args: ["check", "-p", unit, "--message-format=json-render-diagnostics", "--all-targets", ...features], test: false });
|
|
371
|
+
// rustdoc with -D warnings, for the tested and the checked feature sets: the same rows the CI shards (scripts/ci/gate.mjs) and
|
|
372
|
+
// publish.yml's native verify run, so a broken or private intra-doc link fails this unit stage instead of a later verify.
|
|
373
|
+
const docSets = [...new Map([...featureSets, ...allFeatureCheckSets(entry)].map((features) => [JSON.stringify(features), features])).values()];
|
|
374
|
+
for (const features of docSets) steps.push({ command: "cargo", args: ["doc", "-p", unit, "--message-format=json-render-diagnostics", "--no-deps", ...features], test: false, env: { RUSTDOCFLAGS: "-D warnings" } });
|
|
371
375
|
// Consumer rows (separate Cargo workspaces) gate only this crate: they come from this crate's own entry.
|
|
372
376
|
const consumers = consumerRows(unit, entry);
|
|
373
377
|
for (const row of consumers) steps.push(...consumerRowInvocations(row));
|
|
@@ -666,7 +670,7 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
666
670
|
// A crate's own steps run in its Cargo workspace (its own Cargo.lock and target directory, #1285); a consumer row names its
|
|
667
671
|
// separate workspace by --manifest-path relative to the repository root.
|
|
668
672
|
const stepCwd = kind === "cargo" && step.command === "cargo" && !step.args.includes("--manifest-path") ? unitPath : workspaceRoot;
|
|
669
|
-
const result = await run(step.command, step.command === "cargo" ? withCargoJobs(step.args, parsed.jobs) : step.args, { cwd: stepCwd, env });
|
|
673
|
+
const result = await run(step.command, step.command === "cargo" ? withCargoJobs(step.args, parsed.jobs) : step.args, { cwd: stepCwd, env: step.env ? { ...env, ...step.env } : env });
|
|
670
674
|
const row = { command: commandText, ok: false, isTest: step.test && TEST_COMMAND.test(commandText) };
|
|
671
675
|
if (result.error || result.status !== 0) {
|
|
672
676
|
steps.push(row);
|
|
@@ -272,6 +272,16 @@ function stageSidecarsSpec(value) {
|
|
|
272
272
|
};
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/** The triple a `targets.mac.macPackage` spec builds for: `triple`, else this host's arch when `universal: false`, else universal-apple-darwin. */
|
|
276
|
+
export function macPackageTriple(spec) {
|
|
277
|
+
return spec.triple ?? (spec.universal === false ? (process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin") : "universal-apple-darwin");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** True when the macPackage spec stages its declared sidecars itself (`stageSidecars: true | { config?, receipt? }`, the recipe's first prepare step). */
|
|
281
|
+
export function macPackageStagesSidecars(spec) {
|
|
282
|
+
return spec.stageSidecars !== undefined && spec.stageSidecars !== false;
|
|
283
|
+
}
|
|
284
|
+
|
|
275
285
|
export function expandMacPackageTarget(target, { appRoot, config = {}, env = process.env, resolveTargetRoot = cargoResolveTargetRoot, managedRoots }) {
|
|
276
286
|
if (!target?.macPackage) return withoutRecipe(target);
|
|
277
287
|
const spec = target.macPackage;
|
|
@@ -291,7 +301,7 @@ export function expandMacPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
291
301
|
if (spec.updater !== undefined && spec.updater !== true && spec.updater !== false && typeof spec.updater !== "string") fail("targets.mac.macPackage.updater must be true or an app-relative .tar.gz path");
|
|
292
302
|
if (typeof spec.updater === "string" && !/\.tar\.gz$/i.test(relativeInside(spec.updater, "targets.mac.macPackage.updater"))) fail("targets.mac.macPackage.updater must end in .tar.gz");
|
|
293
303
|
// Tauri's universal-apple-darwin target builds both slices and lipos them itself.
|
|
294
|
-
const triple =
|
|
304
|
+
const triple = macPackageTriple(spec);
|
|
295
305
|
if (spec.triple !== undefined) {
|
|
296
306
|
const wrong = [...MAC_TRIPLES, "universal-apple-darwin"].find((other) => other !== triple && target.notarize.app.replaceAll("\\", "/").split("/").includes(other));
|
|
297
307
|
if (wrong) fail(`targets.mac.macPackage.triple is ${triple} but notarize.app is under ${wrong}; the built .app lives in the ${triple} directory`);
|
|
@@ -301,7 +311,7 @@ export function expandMacPackageTarget(target, { appRoot, config = {}, env = pro
|
|
|
301
311
|
const sidecars = stageSidecarsSpec(spec.stageSidecars);
|
|
302
312
|
if (sidecars) {
|
|
303
313
|
// First prepare step: stage the declared sidecars for this triple (universal: both slices, lipo, <name>-universal-apple-darwin).
|
|
304
|
-
extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
|
|
314
|
+
extras.prepare = [{ cmd: "right-release", args: ["stage-sidecar", "--target", triple, "--if-stale", ...(sidecars.config ? ["--config", sidecars.config] : [])] }, ...extras.prepare];
|
|
305
315
|
}
|
|
306
316
|
let helpers;
|
|
307
317
|
if (spec.nativeHelpers !== undefined) {
|
package/package.json
CHANGED
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.23",
|
|
53
53
|
"@rightkit/settings-ui": "0.7.0",
|
|
54
54
|
"@rightkit/social-ui": "0.1.0",
|
|
55
55
|
"@rightkit/system-ui": "0.1.0",
|
package/snapshot-broker.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { removeCarrier, sweepSnapshotRequests } from "./carrier-census.mjs";
|
|
|
27
27
|
import { DEFAULT_RETRY_WINDOW_MS, RETRYABLE_OUTCOME_CODES, clientGuardMs, diagnoseControlReply, drainFrames, operationDeadlineEnv, operationOutcomeInOutput, overCapProblem, refusalInCapturedOutput, retryPauseMs, sleepSync } from "./broker-wire.mjs";
|
|
28
28
|
import { OWNERSHIP_ENV, OWNERSHIP_FILE_ENV, markCarriedSend, markCarriedUnsent } from "./ownership-mark.mjs";
|
|
29
29
|
import {
|
|
30
|
-
MANAGED_RUST_ENVIRONMENT_KEYS, SNAPSHOT_REQUEST_SCHEMA, SnapshotRequestError, argvSha256, isCallHandleOf, lockedArgv, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
|
|
30
|
+
MANAGED_RUST_ENVIRONMENT_KEYS, SNAPSHOT_REQUEST_SCHEMA, SnapshotRequestError, argvSha256, declaredInputDigestsOf, isCallHandleOf, lockedArgv, observedEnvOf, readDeclaredInputs, readRequestRef, verifySnapshotReceipt, writeSnapshotRequest,
|
|
31
31
|
} from "./snapshot-request.mjs";
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -438,6 +438,10 @@ function carryBrokerCargoOnce({ words, cwd, env, section, workspace, checkoutWor
|
|
|
438
438
|
// 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
|
|
439
439
|
// 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.
|
|
440
440
|
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" });
|
|
441
|
+
// A typed REJECTED result (admission paused, a cutover drain, ...) with no request reference: the broker refused the submit before it admitted anything, so nothing
|
|
442
|
+
// runs under this sub-handle and the broker will never list it as a member. Recorded unsent now, or a dead owner's record could never be reclaimed (HeardRight Mac,
|
|
443
|
+
// 2026-11-02: 42 paused refusals left a member-less sub-handle the broker answers UNKNOWN for).
|
|
444
|
+
else if (outcome.status === "unverified" && /^broker-refused-/.test(String(outcome.code)) && ref === null) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: `the broker refused the submit before admitting it (${outcome.code}): nothing was spawned` });
|
|
441
445
|
// 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.
|
|
442
446
|
if (outcome.status === "verified" && capture) {
|
|
443
447
|
const drained = drainFrames(`${out.stdout}\n${out.stderr}`);
|
|
@@ -650,6 +654,10 @@ function* carryBrokerAdapterSteps({ adapter, words, cwd, env, section, record, e
|
|
|
650
654
|
// 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
|
|
651
655
|
// 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.
|
|
652
656
|
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" });
|
|
657
|
+
// A typed REJECTED result (admission paused, a cutover drain, ...) with no request reference: the broker refused the submit before it admitted anything, so nothing
|
|
658
|
+
// runs under this sub-handle and the broker will never list it as a member. Recorded unsent now, or a dead owner's record could never be reclaimed (HeardRight Mac,
|
|
659
|
+
// 2026-11-02: 42 paused refusals left a member-less sub-handle the broker answers UNKNOWN for).
|
|
660
|
+
else if (outcome.status === "unverified" && /^broker-refused-/.test(String(outcome.code)) && ref === null) markCarriedUnsentRequest({ section, env, callHandle: made.carrier.ownership.handle, reason: `the broker refused the submit before admitting it (${outcome.code}): nothing was spawned` });
|
|
653
661
|
// 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.
|
|
654
662
|
if (outcome.status === "verified" && capture) {
|
|
655
663
|
const drained = drainFrames(`${out.stdout}\n${out.stderr}`);
|
|
@@ -698,9 +706,26 @@ export const runBrokerCargo = (call) => carryBrokerCargo(call).status;
|
|
|
698
706
|
* again now, verifies against THIS gate's commit, snapshot, ownership handle and pins, and its carrier was used once: a nonce or a
|
|
699
707
|
* request id that appears twice in the run (`seen`) is a replay. Returns the problems; none means every call verified.
|
|
700
708
|
*/
|
|
701
|
-
export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set(), handles: new Set() } } = {}) {
|
|
709
|
+
export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set(), ids: new Set(), handles: new Set() }, openInputDigests = null } = {}) {
|
|
702
710
|
seen.handles ??= new Set();
|
|
703
711
|
const problems = [];
|
|
712
|
+
// `openInputDigests` is supplied ONLY by a producer carrier (the gate's own web-build, windows-identity and stage-sidecar steps, which write declared inputs by design):
|
|
713
|
+
// the inputs whose digest at stage open (the gate's own, allowAbsent) differs from the digest at stage end, computed once, are then "produced" and accept any well-formed
|
|
714
|
+
// receipt entry. Without it (every other carrier) nothing is produced: every receipt, cargo AND adapter, must equal the stage-end digest input by input, so an input a child
|
|
715
|
+
// swapped around an admission and restored is refused. closeSnapshotStage's attribution check is what makes "produced" safe. A failure to digest ends the verification with that problem.
|
|
716
|
+
let inputPolicy = null;
|
|
717
|
+
const ledgered = entries.some((e) => e.mode === "broker" && e.status === "verified");
|
|
718
|
+
if (ledgered) {
|
|
719
|
+
try {
|
|
720
|
+
const end = declaredInputDigestsOf(sb.snapshot.path, readDeclaredInputs(sb.snapshot.path), { platform: sb.platform, allowAbsent: true });
|
|
721
|
+
const produced = new Set();
|
|
722
|
+
if (Array.isArray(openInputDigests)) {
|
|
723
|
+
if (openInputDigests.length !== end.length) throw new SnapshotRequestError("ledger", "the stage-open digest of the declared inputs does not match the declaration");
|
|
724
|
+
end.forEach((d, i) => { if (openInputDigests[i]?.path !== d.path) throw new SnapshotRequestError("ledger", "the stage-open digest of the declared inputs is not in declaration order"); if (canonicalJson(openInputDigests[i]) !== canonicalJson(d)) produced.add(d.path); });
|
|
725
|
+
}
|
|
726
|
+
inputPolicy = { produced, current: end };
|
|
727
|
+
} catch (error) { return [`the gate cannot digest the snapshot's declared inputs at the end of the stage (${error.code ?? "error"}: ${error.message})`]; }
|
|
728
|
+
}
|
|
704
729
|
for (const e of entries) {
|
|
705
730
|
if (e.mode !== "broker" || e.status !== "verified") continue;
|
|
706
731
|
const adapter = typeof e.adapter === "string" && e.adapter ? e.adapter : null;
|
|
@@ -733,7 +758,7 @@ export async function verifyBrokerLedger(entries, sb, { seen = { nonces: new Set
|
|
|
733
758
|
ownership: { handle: e.ownershipHandle }, primaryCwd: e.checkoutCwd, argvSha256: argvSha256(e.argv), nonce: e.nonce,
|
|
734
759
|
};
|
|
735
760
|
const receipt = await sb.receiptOf(e.requestId);
|
|
736
|
-
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 });
|
|
761
|
+
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, inputPolicy });
|
|
737
762
|
if (adapter !== null) {
|
|
738
763
|
// 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.
|
|
739
764
|
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");
|
package/snapshot-request.mjs
CHANGED
|
@@ -356,9 +356,9 @@ export function assertSuppliedInputs(snapshotRoot, supplied) {
|
|
|
356
356
|
* anywhere, a special file, or a limit breach is a refusal. The name is the input's path components joined by `/` on EVERY platform (GB-49: RustRight hashes
|
|
357
357
|
* `/` on Windows too, so a `\` in a declaration or a platform separator never reaches the hash), UTF-8, no Unicode normalisation.
|
|
358
358
|
*
|
|
359
|
-
* `allowAbsent`
|
|
360
|
-
*
|
|
361
|
-
*
|
|
359
|
+
* `allowAbsent` makes a declared input that does not exist in the snapshot (ENOENT/ENOTDIR on a component; a link is still refused) the entry
|
|
360
|
+
* `{ path, absent: true }` (no `sha256`, 0 files, 0 bytes), the wire form RustRight signs. The broker admits every request with an absent input
|
|
361
|
+
* (a compile is refused at exit only if an input CHANGED while it ran, rustright d378d32), so the receipt check digests with it for every class.
|
|
362
362
|
*/
|
|
363
363
|
export function declaredInputDigestsOf(snapshotRoot, inputs, { allowAbsent = false } = {}) {
|
|
364
364
|
let files = 0; let bytes = 0;
|
|
@@ -528,7 +528,8 @@ export function verifyNestedChildren(evidence, receipt, { snapshotPath, checkout
|
|
|
528
528
|
* Beyond the signature, the job, the nonce and the request digest, the receipt must carry (item 7 row 7.22):
|
|
529
529
|
* - signed SUCCEEDED with exit 0, and signed snapshotEvidence equal to the receipt's copies (commit, ownership handle, request id,
|
|
530
530
|
* snapshot path, `treeAtStart` = `treeAtExit` = the expected tree);
|
|
531
|
-
* - `declaredInputDigests`
|
|
531
|
+
* - `declaredInputDigests` well formed (the declaration's paths in order, each `{path, sha256}` or `{path, absent:true}`), and for a cargo request equal to the gate's own
|
|
532
|
+
* digest of the snapshot's declared inputs read now (absent included; an adapter request skips the comparison, it may produce an input); `inputPolicy` replaces that digest (see below);
|
|
532
533
|
* - `outputDir` absolute, outside the snapshot and the checkout, and a managed generation target under one of `outputRoots`;
|
|
533
534
|
* - signed stageEvidence (freshness, attribution, toolchain) equal to the receipt's;
|
|
534
535
|
* - for a compile, freshness.causeEvidence (rustright.cause-evidence.v1) coherent with its census and with a verdict of `explained`: refused as `evidence` when missing,
|
|
@@ -536,7 +537,7 @@ export function verifyNestedChildren(evidence, receipt, { snapshotPath, checkout
|
|
|
536
537
|
* - timingsMs, exit, diagnosticsPath, brokerVersion and executor present with their types; the retired `verdict` and `refreshed` are refused (GB-30: Class A and its verdict reuse are the gate's).
|
|
537
538
|
* `checkout` and `outputRoots` are required: without them nothing can be said about where the outputs went, so it refuses.
|
|
538
539
|
*/
|
|
539
|
-
export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, workspace, observedEnv = {}, adapter = null, adapterEnv = {}, checkout, outputRoots, platform = process.platform, canonicalPath = (p) => fs.realpathSync.native(p), digestInputs = declaredInputDigestsOf, readInputs = readDeclaredInputs }) {
|
|
540
|
+
export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, workspace, observedEnv = {}, adapter = null, adapterEnv = {}, checkout, outputRoots, platform = process.platform, canonicalPath = (p) => fs.realpathSync.native(p), digestInputs = declaredInputDigestsOf, readInputs = readDeclaredInputs, inputPolicy = null }) {
|
|
540
541
|
const requestDigest = requestDigestOf(snapshotRequestCore({ argv, workspace, observedEnv, carrier, adapter, adapterEnv }));
|
|
541
542
|
const payload = verifyProvenance(receipt, pins, { jobId, nonce: carrier.nonce, requestDigest });
|
|
542
543
|
// The signed outcome, not the client's exit code: a passing row needs the broker to have signed SUCCEEDED with exit 0 and no
|
|
@@ -563,21 +564,36 @@ export function verifySnapshotReceipt(receipt, { pins, jobId, carrier, argv, wor
|
|
|
563
564
|
commit: carrier.commit, tree: carrier.snapshot.expectedTree, jobId, handle: carrier.ownership.handle, snapshotPath: carrier.snapshot.path,
|
|
564
565
|
samePath: (a, b) => { try { return canonicalPath(a) === canonicalPath(b); } catch { return false; } },
|
|
565
566
|
});
|
|
566
|
-
// Declared inputs
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
567
|
+
// Declared inputs. The broker admits every snapshot request with a declared input absent and records it as `{ path, absent: true }` in the signed
|
|
568
|
+
// snapshotEvidence.declaredInputDigests (declaration order); a compile is refused at exit if any declared input changed while it ran, an absent one
|
|
569
|
+
// appearing included (rustright d378d32). So an absent entry is evidence on any request, and the receipt is judged per class:
|
|
570
|
+
// - a cargo request (adapter === null): the receipt's digests equal the gate's current digest of the snapshot's inputs (the child copies the next
|
|
571
|
+
// file only after the call returns, and the broker refused a compile whose inputs changed during it);
|
|
572
|
+
// - an adapter request: the digest comparison is skipped (the job may be the producer of an input), but the receipt's entries must be well formed,
|
|
573
|
+
// the declaration's paths in declaration order, each exactly `{ path, sha256 }` or exactly `{ path, absent: true }`;
|
|
574
|
+
// - with `inputPolicy` ({ produced: Set<path>, current: digest array }) the gate's own digest is replaced: an input whose path is in `produced` accepts any
|
|
575
|
+
// well-formed entry (absent or any sha256); every other input must equal `current`'s entry for that path (the swap-around-admission refusal).
|
|
576
|
+
let gateInputs; let gateDigests = null;
|
|
575
577
|
try {
|
|
576
578
|
gateInputs = readInputs(carrier.snapshot.path);
|
|
577
|
-
gateDigests = digestInputs(carrier.snapshot.path, gateInputs, { platform, allowAbsent:
|
|
579
|
+
if (!inputPolicy && adapter === null) gateDigests = digestInputs(carrier.snapshot.path, gateInputs, { platform, allowAbsent: true });
|
|
578
580
|
} catch (e) { fail("evidence", `the gate cannot digest the snapshot's declared inputs (${e.message})`); }
|
|
579
581
|
if (!same(gateInputs, carrier.declaredInputs?.inputs)) fail("evidence", "the snapshot's declared inputs are not the ones this request carried (declaredInputs differs from the snapshot declaration)");
|
|
580
|
-
|
|
582
|
+
const signedInputs = evidence.declaredInputDigests;
|
|
583
|
+
const wellFormed = Array.isArray(signedInputs) && signedInputs.length === gateInputs.length && signedInputs.every((d, i) => {
|
|
584
|
+
if (!isObject(d) || d.path !== gateInputs[i]) return false;
|
|
585
|
+
const keys = Object.keys(d).sort().join();
|
|
586
|
+
return (keys === "path,sha256" && typeof d.sha256 === "string" && /^[0-9a-f]{64}$/.test(d.sha256)) || (keys === "absent,path" && d.absent === true);
|
|
587
|
+
});
|
|
588
|
+
if (!wellFormed) fail("evidence", "the signed declaredInputDigests are not the snapshot's declared inputs in declaration order, each exactly { path, sha256 } or { path, absent: true }");
|
|
589
|
+
if (inputPolicy) {
|
|
590
|
+
const { produced, current } = inputPolicy;
|
|
591
|
+
if (!(produced instanceof Set) || !Array.isArray(current) || current.length !== gateInputs.length) fail("evidence", "the gate's input policy is not { produced: Set, current: one digest per declared input }");
|
|
592
|
+
signedInputs.forEach((d, i) => {
|
|
593
|
+
if (produced.has(gateInputs[i])) return;
|
|
594
|
+
if (!same(d, current[i])) fail("evidence", `the signed declaredInputDigests entry for ${gateInputs[i]} is not the gate's own digest of that input at the end of the stage, and the stage did not change it (an input the receipt records absent must be absent, and the reverse)`);
|
|
595
|
+
});
|
|
596
|
+
} else if (adapter === null && !same(signedInputs, gateDigests)) fail("evidence", "the signed declaredInputDigests are not the gate's own digest of the snapshot's declared inputs (an input the receipt records absent must be absent in the snapshot, and the reverse)");
|
|
581
597
|
// Output directory: where the outputs went must be the broker's managed generation target, never under the snapshot or the checkout.
|
|
582
598
|
const out = evidence.outputDir;
|
|
583
599
|
if (typeof out !== "string" || !path.isAbsolute(out) && !path.win32.isAbsolute(out)) fail("evidence", "the signed evidence names no absolute output directory");
|
package/stage-ledger.mjs
CHANGED
|
@@ -137,12 +137,15 @@ export const sameEntries = (x, y) => { const d = diffEntries(x, y); return !d.ad
|
|
|
137
137
|
* The attribution diff of a staging step: every path the step added must be one it reported (or its receipt), and every path it
|
|
138
138
|
* reported must exist. Anything else is an unattributed write. `reported` and `receipt` are snapshot-relative posix paths.
|
|
139
139
|
*/
|
|
140
|
-
export function attributionProblems({ before, after, reported, receipt }) {
|
|
140
|
+
export function attributionProblems({ before, after, reported, receipt, removable = [] }) {
|
|
141
141
|
const d = diffEntries(before, after);
|
|
142
142
|
const allowed = new Set([...reported, ...(receipt ? [receipt] : [])]);
|
|
143
|
+
// A step re-staging another target may replace a file an earlier staging step staged (a CHANGED path it reports is allowed above) and may remove one (a path in `removable`,
|
|
144
|
+
// the staged sidecar outputs of the step's own earlier staging, which a `wipeStageDir` re-stage deletes); nothing else may be removed.
|
|
145
|
+
const removableSet = new Set(removable);
|
|
143
146
|
const problems = [];
|
|
144
147
|
for (const p of [...d.added, ...d.changed]) if (!allowed.has(p)) problems.push(`${p} was written by the staging step but it did not report it`);
|
|
145
|
-
for (const p of d.removed) problems.push(`${p} was removed by the staging step`);
|
|
148
|
+
for (const p of d.removed) if (!removableSet.has(p)) problems.push(`${p} was removed by the staging step`);
|
|
146
149
|
const present = new Set(after.map((e) => e.path));
|
|
147
150
|
for (const p of allowed) if (!present.has(p)) problems.push(`${p} was reported by the staging step but is not in the snapshot as a ledger entry`);
|
|
148
151
|
return problems;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `right-release stage-sidecar [--config right-release.config.json] [--target <triple
|
|
2
|
+
// `right-release stage-sidecar [--config right-release.config.json] [--target <triple>|package] [--if-stale] [--check] [--json]` (bogusyogi/rightkit#345):
|
|
3
3
|
// stages the config's declarative `sidecars` into src-tauri/binaries with Tauri externalBin naming. `--check` validates
|
|
4
4
|
// the manifest and prints the plan without building, downloading, copying or deleting anything (for template gates).
|
|
5
5
|
import path from "node:path";
|
|
@@ -9,7 +9,7 @@ import { planSidecarStaging, resolveTargetTriple, stageSidecars } from "./stage-
|
|
|
9
9
|
import { readSourceSplit } from "./source-split.mjs";
|
|
10
10
|
|
|
11
11
|
export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
12
|
-
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false };
|
|
12
|
+
const opts = { config: defaultReleaseConfigFile(cwd), target: undefined, json: false, check: false, ifStale: false };
|
|
13
13
|
for (let i = 0; i < argv.length; i += 1) {
|
|
14
14
|
const arg = argv[i];
|
|
15
15
|
if (arg === "--config" || arg === "--target") {
|
|
@@ -19,6 +19,7 @@ export function parseStageSidecarArgs(argv, cwd = process.cwd()) {
|
|
|
19
19
|
i += 1;
|
|
20
20
|
} else if (arg === "--json") opts.json = true;
|
|
21
21
|
else if (arg === "--check") opts.check = true;
|
|
22
|
+
else if (arg === "--if-stale") opts.ifStale = true;
|
|
22
23
|
else throw new Error(`unknown stage-sidecar argument: ${arg}`);
|
|
23
24
|
}
|
|
24
25
|
return opts;
|
|
@@ -57,15 +58,23 @@ async function main() {
|
|
|
57
58
|
const config = await importReleaseConfig(configPath, { lenient: true });
|
|
58
59
|
if (config?.sidecars === undefined) throw new Error(`${opts.config} declares no sidecars`);
|
|
59
60
|
if (opts.check) {
|
|
60
|
-
const target = opts.target ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
61
|
+
const target = (opts.target === "package" ? undefined : opts.target) ?? process.env.TAURI_ENV_TARGET_TRIPLE ?? nodeHostTriple();
|
|
61
62
|
const plan = planSidecarStaging({ manifest: config.sidecars, target });
|
|
62
63
|
if (opts.json) console.log(JSON.stringify(plan, null, 2));
|
|
63
64
|
else for (const line of formatSidecarPlan(plan)) console.log(line);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
+
// `--target package` (the gate's package stage): the triple the config's Mac package recipe stages for (its first prepare step's own
|
|
68
|
+
// --target), decided here in the child that already loaded the config, so the gate never imports app code; otherwise the host's.
|
|
69
|
+
let wanted = opts.target;
|
|
70
|
+
if (wanted === "package") {
|
|
71
|
+
const { macPackageStagesSidecars, macPackageTriple } = await import("./package-recipe-targets.mjs");
|
|
72
|
+
const mac = process.platform === "darwin" ? config?.targets?.mac?.macPackage : undefined;
|
|
73
|
+
wanted = mac && macPackageStagesSidecars(mac) ? macPackageTriple(mac) : undefined;
|
|
74
|
+
}
|
|
75
|
+
const target = resolveTargetTriple({ target: wanted, cwd: appRoot });
|
|
67
76
|
// Warnings (a never-run row, a catalog row that is not staged) go to stderr; stdout stays the staged-file list or the --json document.
|
|
68
|
-
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, ...(split ? { cacheRoot: split.checkoutAppRoot } : {}), deps: { warn: (line) => console.error(line) } });
|
|
77
|
+
const result = await stageSidecars({ appRoot, manifest: config.sidecars, target, ifStale: opts.ifStale, ...(split ? { cacheRoot: split.checkoutAppRoot } : {}), deps: { warn: (line) => console.error(line) } });
|
|
69
78
|
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
70
79
|
else {
|
|
71
80
|
for (const file of result.files ?? []) console.log(`right-release stage-sidecar: staged ${typeof file === "string" ? file : file.path ?? JSON.stringify(file)}`);
|
package/stage-sidecar.mjs
CHANGED
|
@@ -1936,12 +1936,11 @@ export function planSidecarStaging({ manifest, target, env = process.env } = {})
|
|
|
1936
1936
|
* fs (existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, renameSync, chmodSync, statSync, readdirSync, rmSync), run, fetch (https downloads of `remote`), fetchBytes, extractMember, loadMediaCatalog, loadNativeCatalog, makeTempDir, removeDir, now, pid, log (progress), warn (warnings; defaults to log), env.
|
|
1937
1937
|
* The API is quiet by default; the `right-release stage-sidecar` command passes `warn` so the CLI prints warnings on stderr.
|
|
1938
1938
|
*/
|
|
1939
|
-
export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps = {} } = {}) {
|
|
1939
|
+
export async function stageSidecars({ appRoot, manifest, target, cacheRoot, ifStale = false, deps = {} } = {}) {
|
|
1940
1940
|
if (typeof appRoot !== "string" || !path.isAbsolute(appRoot)) fail("STAGE_SIDECAR_CONFIG_INVALID", "appRoot must be an absolute path");
|
|
1941
1941
|
if (cacheRoot !== undefined && (typeof cacheRoot !== "string" || !path.isAbsolute(cacheRoot))) fail("STAGE_SIDECAR_CONFIG_INVALID", "cacheRoot must be an absolute path");
|
|
1942
1942
|
const env = deps.env ?? process.env;
|
|
1943
1943
|
const plan = validateSidecarManifest(manifest, { target, mode: resolveBuildMode(env) });
|
|
1944
|
-
assertNotNested(plan, env);
|
|
1945
1944
|
// A source split (source-split.mjs; rightkit-gate's snapshot route): `appRoot` is the snapshot of the commit, where the declared
|
|
1946
1945
|
// sources are read and where the staged files and the receipt go (the Tauri build script reads them there). `cacheRoot` is the
|
|
1947
1946
|
// checkout's app directory, which keeps every cache, archive, slice and build tree right-release owns (`.right-release/...`) and
|
|
@@ -1969,6 +1968,14 @@ export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps
|
|
|
1969
1968
|
makeTempDir: deps.makeTempDir ?? (() => nodeFs.mkdtempSync(path.join(os.tmpdir(), "rightkit-stage-"))),
|
|
1970
1969
|
removeDir: deps.removeDir ?? ((dir) => nodeFs.rmSync(dir, { recursive: true, force: true })),
|
|
1971
1970
|
};
|
|
1971
|
+
// Already staged: when the receipt at the plan's receipt path is for this manifest and target and every file it names is on disk with its recorded sha256, nothing is built,
|
|
1972
|
+
// downloaded, copied or written (the gate staged the package triple first, rightkit-gate stageSidecarsOnce, so a recipe's own prepare step changes nothing). Judged before the
|
|
1973
|
+
// nested-broker refusal, which guards only a step that would build.
|
|
1974
|
+
// Opt-in (`ifStale`, `stage-sidecar --if-stale`): only a Mac package recipe's own prepare step asks for it. The check is structural (no source hash), so every
|
|
1975
|
+
// other caller, the gate's staging step included, builds as before.
|
|
1976
|
+
const current = ifStale ? currentStageReceipt(plan, ctx) : null;
|
|
1977
|
+
if (current) return { receipt: current, receiptPath: path.resolve(appRoot, plan.receiptPath), files: current.files, externalBinNames: current.files.filter((file) => file.externalBin).map((file) => file.name), unchanged: true };
|
|
1978
|
+
assertNotNested(plan, env);
|
|
1972
1979
|
await claimNativeOutputs(plan, ctx);
|
|
1973
1980
|
const files = [];
|
|
1974
1981
|
const skipped = [];
|
|
@@ -2009,6 +2016,40 @@ export async function stageSidecars({ appRoot, manifest, target, cacheRoot, deps
|
|
|
2009
2016
|
return { receipt, receiptPath: receiptFile, files, externalBinNames: files.filter((file) => file.externalBin).map((file) => file.name) };
|
|
2010
2017
|
}
|
|
2011
2018
|
|
|
2019
|
+
/**
|
|
2020
|
+
* The receipt at the plan's receipt path when staging is already current, else null: a valid receipt of this target whose skipped rows are the plan's, that has a file of the
|
|
2021
|
+
* same kind and name for every named row the plan stages, and whose every file is on disk, a regular file, with its recorded sha256 (and, for a plan that wipes its stage
|
|
2022
|
+
* directory, nothing else in that directory). A receipt that cannot be read or does not match is not current; nothing is ever written.
|
|
2023
|
+
*/
|
|
2024
|
+
function currentStageReceipt(plan, ctx) {
|
|
2025
|
+
const { fs, appRoot } = ctx;
|
|
2026
|
+
try {
|
|
2027
|
+
const receiptFile = path.resolve(appRoot, plan.receiptPath);
|
|
2028
|
+
const receipt = validateStageSidecarReceipt(JSON.parse(fs.readFileSync(receiptFile, "utf8")), { target: plan.target.triple });
|
|
2029
|
+
const planned = plan.entries.filter((entry) => !entry.skipped);
|
|
2030
|
+
const skipped = plan.entries.filter((entry) => entry.skipped).map((entry) => ({ kind: entry.kind, index: entry.index, platforms: entry.platforms }));
|
|
2031
|
+
if (JSON.stringify(skipped) !== JSON.stringify(receipt.skipped ?? [])) return null;
|
|
2032
|
+
if (receipt.files.length < planned.length) return null;
|
|
2033
|
+
for (const entry of planned) if (typeof entry.name === "string" && ["cargo-bin", "command", "cmake", "prebuilt", "remote"].includes(entry.kind) && !receipt.files.some((file) => file.kind === entry.kind && file.name === entry.name)) return null;
|
|
2034
|
+
const named = new Set();
|
|
2035
|
+
for (const file of receipt.files) {
|
|
2036
|
+
const full = path.resolve(appRoot, file.path);
|
|
2037
|
+
const meta = fs.statSync(full);
|
|
2038
|
+
if (!meta.isFile() || sha256Of(fs.readFileSync(full)) !== file.sha256) return null;
|
|
2039
|
+
named.add(full.toLowerCase());
|
|
2040
|
+
}
|
|
2041
|
+
if (plan.wipeStageDir) {
|
|
2042
|
+
named.add(receiptFile.toLowerCase());
|
|
2043
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).every((item) => {
|
|
2044
|
+
const full = path.join(dir, item.name);
|
|
2045
|
+
return item.isDirectory() ? walk(full) : named.has(path.resolve(full).toLowerCase());
|
|
2046
|
+
});
|
|
2047
|
+
if (!walk(path.resolve(appRoot, plan.stageDir))) return null;
|
|
2048
|
+
}
|
|
2049
|
+
return receipt;
|
|
2050
|
+
} catch { return null; }
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2012
2053
|
/** Structural check of a staging receipt; `expected` may pin target and names. */
|
|
2013
2054
|
export function validateStageSidecarReceipt(receipt, expected = {}) {
|
|
2014
2055
|
const bad = (message) => fail("STAGE_SIDECAR_RECEIPT_INVALID", `stage sidecar receipt: ${message}`);
|