Files
gstack/lib/remote-gate-stand-in.ts
T
Garry Tanandcapy-ai[bot] e4b1e34552 fix(ship): a cleared remote gate for HEAD's exact tree satisfies a lane whose inventory refuses that tree (GST-1688) (#4098)
* fix(ship): a cleared remote gate for HEAD's exact tree satisfies a lane whose inventory refuses that tree (GST-1688)

When a ship_lanes_from inventory refuses HEAD's tree (inventory_failed) and
.gstack/tmp/pregate.json verifies for that exact tree with the remote gate's own
report for it, record-ci --remote-gate-stands-in records the lane as
source=remote_gate and accept prints a CI_CARRY: remote_gate line naming the
receipt. A gate for any other tree still stops, with a CARRY_DIAGNOSIS line.
A run whose tree.json lists no files now says to dispatch a fresh full run.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: keep ship-accept-remote-gate-lanes off the Windows scoped shard (bin shebang spawns; run 42302497169)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Review-Finding: p2/specialist-batched#1 reader=red-team
Review-Finding: p2/specialist-batched#2 reader=testing
Review-Finding: p2/specialist-batched#3 reader=testing
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: remote-gate stand-in requires origin/<base> provenance and a clear within 24h (review fix-check)

Review-Finding: p4/outside-voice#2 reader=outside-voice
Review-Finding: p4/adversarial-native#4 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-06 23:55:54 +00:00

40 lines
3.4 KiB
TypeScript

/**
* GST-1688: a cleared pregate.json/remote_gate stands in for a /ship lane whose ship_lanes_from inventory refuses HEAD's own
* tree (gbrain's runs_may_attest refuses, on purpose, a tree that changes CI-reuse files). It stands in only when the
* repository's pregate.json was recorded for HEAD's exact tree and verifies (gstack-pregate verify), and its remote_gate
* receipt is the gate's own report for that same tree: `remote-gate tree=<HEAD's tree> tests=<n>`, n > 0, run on that tree
* rather than accepted from a stamp-only predecessor, cleared within the evidence max age, by a pregate run against
* origin/<base> under origin/<base>'s own policy (the one whose ship_lanes_from maps the lane, so the gate command is that
* policy's). Anything else returns why, and the lane stops as before.
*/
import * as path from 'node:path';
import { REMOTE_GATE_LANE } from './pregate/checks/lanes';
import { PREGATE_OUT_REL, readPregateFile, verifyPregate } from './pregate/index';
import { DEFAULT_MAX_AGE_HOURS } from './run-evidence';
export type RemoteGateStandIn = { ok: true; receipt: string; tests: number; clearedAt: string } | { ok: false; why: string };
export function remoteGateStandIn(repoRoot: string, tree: string, base: string, now = Date.now()): RemoteGateStandIn {
const short = tree.slice(0, 12);
const baseRef = `origin/${base.replace(/^origin\//, '')}`;
const { file } = readPregateFile(path.join(repoRoot, PREGATE_OUT_REL));
if (!file) return { ok: false, why: `no readable ${PREGATE_OUT_REL}` };
if (file.tree !== tree) return { ok: false, why: `${PREGATE_OUT_REL} was recorded for tree ${file.tree.slice(0, 12)}, not HEAD's tree ${short}` };
if (!file.base?.startsWith(`${baseRef}@`)) return { ok: false, why: `${PREGATE_OUT_REL} was recorded against ${file.base ?? 'no base'}, not ${baseRef}` };
if (!file.policy?.startsWith(`${baseRef}@`)) return { ok: false, why: `${PREGATE_OUT_REL} ran under policy ${file.policy ?? 'unrecorded'}, not ${baseRef}'s .gstack/pregate.json` };
const verified = verifyPregate(repoRoot, file);
if (!verified.ok) return { ok: false, why: `gstack-pregate verify refuses it (${verified.code}: ${verified.message})` };
const gate = file.requires_remote?.find(r => r.lane === REMOTE_GATE_LANE);
if (!gate?.receipt) return { ok: false, why: `${PREGATE_OUT_REL} holds no cleared ${REMOTE_GATE_LANE}` };
if (gate.substitution) return { ok: false, why: `its receipt was accepted from gated tree ${gate.substitution.gated_tree.slice(0, 12)}, not run on ${short}` };
const report = /^remote-gate tree=([0-9a-f]{12}) tests=(\d+)(?: |$)/.exec(gate.receipt);
if (!report) return { ok: false, why: `its receipt (${gate.receipt}) is not the remote gate's own report` };
if (report[1] !== short) return { ok: false, why: `its receipt names tree ${report[1]}, not HEAD's tree ${short}` };
if (Number(report[2]) <= 0) return { ok: false, why: 'its receipt reports 0 tests' };
const clearedAt = /(\S+)$/.exec(gate.cleared_by ?? '')?.[1];
if (!clearedAt || Number.isNaN(Date.parse(clearedAt))) return { ok: false, why: `its clear records no time (cleared_by: ${gate.cleared_by ?? 'none'})` };
const age = now - Date.parse(clearedAt);
if (!(age >= 0 && age <= DEFAULT_MAX_AGE_HOURS * 3600_000)) return { ok: false, why: `its clear at ${clearedAt} is not within the last ${DEFAULT_MAX_AGE_HOURS}h; rerun the remote gate` };
return { ok: true, receipt: gate.receipt, tests: Number(report[2]), clearedAt: new Date(clearedAt).toISOString() };
}