mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 14:34:42 +01:00
* pregate: starter policy, benign-finding allowlist, environment failures, per-run scratch
A repository with no .gstack/pregate.json now gets a warn (PREGATE_NO_POLICY)
with a starter policy printed, instead of PREGATE_NO_LANE on every touched
file; gstack-pregate init remains the explicit write. The policy gains an
allowlist for benign secrets/pii findings (env.value and secret never
allowlistable). Checks that cannot run because of the machine report
PREGATE_ENVIRONMENT (incomplete, with kind and evidence) apart from diff
failures. Every run writes its scratch under the state root, per run id.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: rename run-scratch module (strays rule) and map starter.ts to its test
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate literals: match a module as a path fragment, require the constant name for a one-character value
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
guards: an exited guard is the machine's only on exit 127, never from its output;
secrets: clear an interrupted run's added-line copies before scanning;
environment: one fix table per kind, one ANSI strip, hoisted not-found vocabulary;
starter returns its lines; allowlistDigest reuses sha256; stale headers and the
PREGATE_INCOMPLETE fix clause updated; tests for the mixed verify branch, disk
precedence, allowlist boundaries, a positionless finding, head-policy strictness,
run-id edges and the unwritable state root.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: count a module's full file name without a directory as a consumer
consumersOf anchored both the stem and the full basename on a path
separator, so run('install.sh') or bash install.sh without a directory
stopped counting for a multi-character old value. Only the bare stem
needs the separator; the full file name is specific on its own.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: a diff cannot clear itself as an environment failure; a taken --run id gets its own directory
Adversarial review of the environment classifier and the scratch layout:
- exit 127 and 'command not found' are missing-binary only when the
command's own binary is off PATH (runner, guard and remote gate alike);
a script the diff controls that exits 127 is the diff's failure
- a local lane with no test summary is classified from its output only
when the runner was not seen starting a test file; a remote gate only
from its first 40 lines, before any test output could stream
- environment.proof records launch (certain) or output (inferred); an
output-inferred cause never says 'the diff is not at fault', its line
says '(inferred from output)', and verify names such checks
- a runner killed by a signal is PREGATE_INCOMPLETE, not missing-binary
- a --run id whose directory exists takes <id>-2 .. -9 instead of
sharing (and truncating) a live run's logs
- engine.* findings are never allowlistable; a pattern entry the scanner
gave no position for is reported as not tested instead of silently inert
- docs: synopsis --run, no-policy lanes wording, verify's code list,
guard exit 127 carve-out, PREGATE_SECRETS allowlist pointer, scratch
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(pregate): PREGATE_INCOMPLETE names a signal-killed runner; CHANGELOG lists every doc edit
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>
70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
/**
|
|
* pregate/run-scratch — one scratch directory per run under the state root
|
|
* (Lane D, GSTA-112): `<state root>/projects/<slug>/pregate/runs/<run id>/`.
|
|
* Every check keeps its working files there (the secrets scan's added-line
|
|
* copies, the lane and guard logs, the remote gate's file list) instead of
|
|
* os.tmpdir(): two pregates running at once never share a path (a taken
|
|
* `--run <id>` takes a numbered sibling), and the log a `fix:` line names
|
|
* survives a host that wipes /tmp between wakes (gbrain 16). The record names
|
|
* the directory. Runs older than the retention are pruned
|
|
* best-effort when a new run starts; the current run is never pruned.
|
|
*/
|
|
import { randomBytes } from 'node:crypto';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
|
|
export const SCRATCH_RETENTION_MS = 7 * 24 * 3600 * 1000;
|
|
const MAX_RUN_ID = 96;
|
|
|
|
/** A filesystem-safe run id: `--run <id>` reduced to [A-Za-z0-9._-], else `<utc stamp>-<pid>-<4 hex>`. */
|
|
export function scratchRunId(requested?: string, pid: number = process.pid): string {
|
|
const cleaned = (requested ?? '').replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+|[.-]+$/g, '').slice(0, MAX_RUN_ID);
|
|
if (cleaned) return cleaned;
|
|
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
return `${stamp}-${pid}-${randomBytes(2).toString('hex')}`;
|
|
}
|
|
|
|
/** How many numbered siblings a taken run id may fall back to before the run refuses. */
|
|
const MAX_RUN_ID_SIBLINGS = 9;
|
|
|
|
/**
|
|
* `<runsRoot>/<run id>`, created 0700 (and its parents), after pruning old
|
|
* siblings. The leaf is created without `recursive`, so a directory that
|
|
* already exists (a second `--run <id>` beside a live run, or a finished one
|
|
* under the same name) is never shared: the run takes `<run id>-2` … `-9`
|
|
* instead, and the record names the directory it got.
|
|
*/
|
|
export function createScratch(runsRoot: string, runId: string): string {
|
|
fs.mkdirSync(runsRoot, { recursive: true, mode: 0o700 });
|
|
pruneScratch(runsRoot, runId);
|
|
for (let n = 1; n <= MAX_RUN_ID_SIBLINGS; n++) {
|
|
const dir = path.join(runsRoot, n === 1 ? runId : `${runId}-${n}`);
|
|
try { fs.mkdirSync(dir, { mode: 0o700 }); } catch (e: any) { if (e?.code === 'EEXIST') continue; throw e; }
|
|
try { fs.chmodSync(dir, 0o700); } catch { /* best effort on hosts without POSIX modes */ }
|
|
return dir;
|
|
}
|
|
throw new Error(`run directory ${path.join(runsRoot, runId)} and its ${MAX_RUN_ID_SIBLINGS - 1} numbered siblings all exist; choose another --run id`);
|
|
}
|
|
|
|
/** Remove sibling run directories older than the retention, never `keep` (the current run); every failure is ignored. */
|
|
export function pruneScratch(runsRoot: string, keep: string, now: number = Date.now()): void {
|
|
let names: string[];
|
|
try { names = fs.readdirSync(runsRoot); } catch { return; }
|
|
for (const name of names) {
|
|
if (name === keep) continue;
|
|
const dir = path.join(runsRoot, name);
|
|
try {
|
|
const st = fs.lstatSync(dir);
|
|
if (!st.isDirectory() || now - st.mtimeMs < SCRATCH_RETENTION_MS) continue;
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
} catch { /* another run may hold it; leave it */ }
|
|
}
|
|
}
|
|
|
|
/** A fresh subdirectory of the run's scratch for one check (`lanes`, `guards`, `secrets`, `remote`). */
|
|
export function checkScratch(scratch: string, check: string): string {
|
|
const dir = path.join(scratch, check);
|
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
return dir;
|
|
}
|