mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
fix(release): digest regen is an explicit --regen-digest opt-in, not presence-sniffed code exec
Review (security) caught the cycle-1 fix executing any repo's scripts/gen-agents-digest.ts on plain 'write' — arbitrary code exec from a hostile clone on a routine bump, contradicting the binary's own containment posture. The regen still runs the TARGET repo's generator (a 'trusted' copy beside the binary would false-red the freshness gate on version drift), but only under the flag: /ship passes it deliberately, in a repo whose code the operator already executes (its test suite). Plain write is side-effect-free again. Also: uniform output shape (agentsDigest: null on the JSON-manifest branch), a REAL generator round-trip test replacing the misnamed lockstep check, and land-and-deploy's evidence gate gets the same digest allow-path as ship so the two grading surfaces agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b8970a2862
commit
a101bf4093
+30
-13
@@ -20,8 +20,13 @@
|
||||
// Exit 0 on a decidable state (incl. DRIFT_UNEXPECTED — it's a real state
|
||||
// the caller must handle), exit 2 on bad args / unresolvable base.
|
||||
//
|
||||
// write --version <X.Y.Z.W> [--version-path <p>]
|
||||
// write --version <X.Y.Z.W> [--version-path <p>] [--regen-digest]
|
||||
// Validates the 4-digit pattern, writes VERSION + package.json.version.
|
||||
// --regen-digest (opt-in): additionally run the target repo's own
|
||||
// scripts/gen-agents-digest.ts when present, so the version-stamped
|
||||
// committed digest stays freshness-green through the bump. Executes
|
||||
// target-repo code — callers pass it only for repos whose code they
|
||||
// already execute (/ship does).
|
||||
// Use for the FRESH bump (or an approved queue rebump). Exit 3 on a
|
||||
// half-write (VERSION written, package.json failed) so the caller knows
|
||||
// drift exists; the next classify() will report DRIFT_STALE_PKG.
|
||||
@@ -32,12 +37,12 @@
|
||||
//
|
||||
// Contract: classify NEVER writes. write/repair mutate VERSION + the manifest
|
||||
// + npm lockfiles (package-lock.json / npm-shrinkwrap.json, when present) —
|
||||
// plus, in the gstack repo only, the committed agents digest (whose first
|
||||
// line embeds VERSION and is byte-freshness-gated by test/agents-digest.test.ts
|
||||
// and the Skill Docs Freshness CI check, so the write that changes VERSION
|
||||
// must regenerate it or every release commit goes red). No git mutation, no
|
||||
// network. Mirrors gstack-next-version's reader/writer split so /ship
|
||||
// composes them.
|
||||
// plus, under the explicit --regen-digest opt-in only, the committed agents
|
||||
// digest (whose first line embeds VERSION and is byte-freshness-gated by
|
||||
// test/agents-digest.test.ts and the Skill Docs Freshness CI check, so the
|
||||
// write that changes VERSION must regenerate it or every release commit of
|
||||
// the gstack repo goes red). No git mutation, no network. Mirrors
|
||||
// gstack-next-version's reader/writer split so /ship composes them.
|
||||
//
|
||||
// Manifest resolution (all three subcommands accept --package-json-path):
|
||||
// --package-json-path <p> → .gstack/package-json-path → ./package.json
|
||||
@@ -340,14 +345,22 @@ function cmdClassify(args: string[], cwd: string): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* gstack-repo-only side effect: agents-digest/gstack-AGENTS.md embeds VERSION
|
||||
* in its first line and is freshness-gated (committed bytes must equal the
|
||||
* generated bytes), so the bump that changes VERSION regenerates it in the
|
||||
* same write. Any repo without the generator + committed digest skips this.
|
||||
* gstack-repo-only side effect, EXPLICITLY OPT-IN (--regen-digest):
|
||||
* agents-digest/gstack-AGENTS.md embeds VERSION in its first line and is
|
||||
* freshness-gated (committed bytes must equal the generated bytes), so the
|
||||
* bump that changes VERSION regenerates it in the same write. The regen runs
|
||||
* the TARGET repo's own generator (its bytes must match the target's
|
||||
* freshness test, so a "trusted" copy beside this binary would false-red on
|
||||
* version drift) — which is code execution from the bumped repo. That is why
|
||||
* the flag exists: presence-sniffing alone would turn a routine bump in a
|
||||
* hostile clone into arbitrary code exec. /ship passes the flag deliberately,
|
||||
* in a repo whose code the operator is already executing (its test suite).
|
||||
* Without the flag, write has NO side effects beyond VERSION/manifest/locks.
|
||||
* Regen failure warns rather than failing the bump — the deterministic
|
||||
* freshness test stays red until it's rerun, so nothing rots silently.
|
||||
*/
|
||||
function regenAgentsDigest(versionPath: string): boolean | null {
|
||||
function regenAgentsDigest(versionPath: string, optedIn: boolean): boolean | null {
|
||||
if (!optedIn) return null;
|
||||
const root = dirname(versionPath);
|
||||
const gen = join(root, "scripts", "gen-agents-digest.ts");
|
||||
if (!existsSync(gen) || !existsSync(join(root, "agents-digest", "gstack-AGENTS.md"))) return null;
|
||||
@@ -419,6 +432,10 @@ function cmdWrite(args: string[], cwd: string): void {
|
||||
versionPath: versionRel,
|
||||
packageJson: true,
|
||||
packageLock: lockSynced.length > 0,
|
||||
// Uniform output shape with the plain-VERSION branch. Always null
|
||||
// here: a JSON-manifest version source implies no plain VERSION file
|
||||
// for the digest generator to stamp.
|
||||
agentsDigest: null,
|
||||
}) + "\n",
|
||||
);
|
||||
return;
|
||||
@@ -427,7 +444,7 @@ function cmdWrite(args: string[], cwd: string): void {
|
||||
const pkgPath = resolvePkgPath(cwd, argVal(args, "--package-json-path"));
|
||||
const hasPkg = existsSync(pkgPath);
|
||||
writeFileSync(versionPath, version + "\n");
|
||||
const digestRegen = regenAgentsDigest(versionPath);
|
||||
const digestRegen = regenAgentsDigest(versionPath, args.includes("--regen-digest"));
|
||||
let lockSynced: string[] = [];
|
||||
// Decision 11: the manifest (and its lockfiles) carry the npm-valid
|
||||
// 3-digit translation — npm rejects a fourth component, so mirroring the
|
||||
|
||||
Reference in New Issue
Block a user