Skip to content

gate: one policy per env var — remove the ambiguity instead of policing it - #342

Merged
pdbethke merged 2 commits into
mainfrom
fix/gate-policy-format
Sep 13, 2026
Merged

pdbethke merged 2 commits into
mainfrom
fix/gate-policy-format

Conversation

@pdbethke

Copy link
Copy Markdown
Owner

Three guards were written against a ; inside cmd=, and a cold reviewer defeated all three — each time letting a truncated, weaker command post a wrongful success:

round guard defeated by
two split on ; before isolating cmd= go vet ./... ; go test ./... ran only the vet
three keyed on the absence of = any command with a flag: -tags=integration
four keyed on the absence of repo= true; env repo=x false — an incidental repo=

The fourth answer is not a fourth guard. CORRALAI_GATE_POLICIES is retired; each policy now gets its own CORRALAI_GATE_POLICY_<NAME>, mirroring the existing CORRALAI_AGENT_<NAME> convention. The operating system supplies the separator, so a ; in a command cannot collide with anything.

The headline test is the inverse of the ones it replaces: a semicolon in a command must now survive verbatim.

Also fixed — pre-existing, found in round four: CheckCmd was a []string from strings.Fields, rejoined with spaces at two call sites. That destroyed newlines, so true # comment\nfalse collapsed onto one line and the failing step vanished behind the comment; quoted arguments with spaces would have been mangled too. It is one string from parser to jail now.

What this costs, since it isn't free

  • The retired variable is refused, not parsed and not silently ignored — ignoring it would turn a configured gate into no gate at all. Supporting both formats would mean two parsers for one rule, which is the defect class that has produced fifteen findings in this package.
  • One variable was easier to paste into a CI config than N. Real ergonomic loss.
  • Policy.CheckCmd []string → string breaks brain Options.GatePolicies callers.
  • Ordering is now across variables, so ParsePolicyEnv sorts by name. Ranging a map and taking what comes is the exact non-determinism that produced two findings here in a day, and policies decide which check runs against a PR.
  • No expressiveness is lost, and two things improve: a malformed policy is isolated to its own variable (degrade-never-block gets easier), and commands with newlines or quoting now work.

Method

Four negative controls, all firing. The harness gained a third invalidity check: a -run pattern matching no tests now reports invalid rather than passing. It called the semicolon control vacuous when the pattern was simply misspelled — the third time this session my control harness, rather than the control, was the broken part.

Full suite green, vet clean, security gate OK, site builds.

🤖 Generated with Claude Code

https://claude.ai/code/session_011NgkB3eLPBhgfqU1XopR1V

pdbethke and others added 2 commits September 13, 2026 07:19
…ng it

Three guards were written against a ';' inside cmd= and a cold reviewer
defeated all three, each time letting a truncated, WEAKER command post a
wrongful success — the worst thing this package can do:

  round two   split on ';' before isolating cmd=, so
              "go vet ./... ; go test ./..." was accepted as "go vet ./..."
  round three keyed the guard on the absence of '=' — defeated by any command
              containing a flag like -tags=integration
  round four  keyed it on the absence of "repo=" — defeated by a command
              containing "repo=" incidentally: "true; env repo=x false"

The fourth answer is not a fourth guard. CORRALAI_GATE_POLICIES is retired and
each policy gets its own CORRALAI_GATE_POLICY_<NAME>, mirroring the existing
CORRALAI_AGENT_<NAME> convention. The OPERATING SYSTEM supplies the separator,
so a ';' in a command cannot collide with anything. The ambiguity is gone
rather than policed, and the headline test is now the INVERSE of the ones it
replaces: a semicolon in a command must survive verbatim.

Also fixed, pre-existing and found in round four: CheckCmd was a []string
produced by strings.Fields and rejoined with spaces at two call sites, which
destroyed newlines — "true # comment\nfalse" collapsed onto one line and the
failing step vanished behind the comment — and would have mangled any quoted
argument containing a space. It is one string from the parser to the jail now.

WHAT THIS COSTS, recorded because it is not free:
  - The retired variable is REFUSED, not parsed and not silently ignored.
    Silently ignoring it would turn a configured gate into no gate at all.
    Supporting both formats would mean two parsers for one rule, which is the
    defect class that has produced fifteen findings in this package.
  - One variable was easier to paste into a CI config than N.
  - Policy.CheckCmd []string -> string breaks brain Options.GatePolicies
    callers.
  - Ordering is now across variables, so ParsePolicyEnv SORTS BY NAME. Ranging
    a map and taking what comes is the exact non-determinism that produced two
    findings here in a day; policies decide which check runs against a pull
    request and may not arrive in a different order on different runs.
  - No expressiveness is lost. Two things improve: a malformed policy is
    isolated to its own variable, so degrade-never-block gets easier, and
    commands with newlines or quoting now work.

Four negative controls, all firing. The harness gained a third invalidity
check: a -run pattern that matches NO TESTS now reports as invalid rather than
as a pass. It reported the semicolon control vacuous when the pattern was
simply misspelled — the third time in this session my control harness, rather
than the control, was the broken part.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NgkB3eLPBhgfqU1XopR1V
…from the binaries

The CLI reference is GENERATED from each binary's real -h output, and CI
checks it for drift. I updated `corral`'s env-var help and not
`corral-wrangler`'s, then hand-edited the generated markdown to match — so the
docs disagreed with the binary that produces them, and the gate failed.

A rule applied at one door and not its sibling, in the commit whose entire
subject is rules applied at one door and not their siblings. The gate caught
it, which is the system working: this is exactly the drift it exists to stop.

Both binaries now carry the SAME text from one source, and the generated docs
come from scripts/gen-cli-docs.sh rather than from me editing the output.
`gen-cli-docs.sh --check` passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011NgkB3eLPBhgfqU1XopR1V
@pdbethke
pdbethke merged commit cf9bc52 into main Sep 13, 2026
7 checks passed
@pdbethke
pdbethke deleted the fix/gate-policy-format branch September 13, 2026 11:52
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant