fix(source-control): close the flag-abbreviation gap in both babysit guards - #1354
Conversation
…guards Both guarded parsers left argparse abbreviation on, so `--i` resolved to --include-human and `--mer` to --merge while the command text contained neither — defeating every permission condition stated as the literal presence or absence of a flag. Set allow_abbrev=False on both, and make the merge wrapper refuse --allow-unpinned-head by prefix family instead of exact equality, so the wrapper boundary no longer depends on the interpreter behind it. The autonomous resolver additionally refuses severity/security-marked threads (shared structured-severity vocabulary plus the word "security", fail-closed on comment truncation), putting code behind the "never a security or P1 thread" condition the covering permission grants state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 50s —— View job Code Review
The PR closes a real security gap: argparse's default
|
|
Claude finished @kyle-sexton's task in 4m 7s —— View job Security Review
Scope: 5 changed files — IMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d18fc79c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…idden class The first cut reused the shared P0-P3 vocabulary, which flagged the advisory P2/P3 threads the worker is documented to resolve once outdated and would have self-blocked the merge gate on its own advisory threads. The guard now matches only what the covering grants actually forbid unattended: a P0/P1 badge or bracket, the word CRITICAL, or the word "security" -- still fail-closed on truncation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 0s —— View job I'll analyze this and get back to you. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d8b49c0c71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
One word-bounded token now covers badge, bracket, and the bare prose
spellings bots also emit ("P1: blocking regression", "P1 must fix"),
which the badge/bracket-only pair missed. The boundary keeps P2/P3
markers and embedded strings like AP1000 out; a thread merely mentioning
P1 flags to interactive judgment, the safe direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 42s —— View job Code Review
Latest commit (
|
|
Claude finished @kyle-sexton's task in 3m 7s —— View job Security Review
Scope: 5 changed files — SUGGESTION —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60e19d1d7c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Bots emit lowercase forms too (p1:, [p1], priority:p1 fragments); the case-sensitive token let those threads through the never-P1 condition. IGNORECASE on the one P-token regex; CRITICAL stays uppercase-only to match the structured vocabulary rather than ordinary prose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 39s —— View job Code Review
Latest commit (
|
|
Claude finished @kyle-sexton's task in 2m 55s —— View job Security Review
Scope: 5 changed files at HEAD ( SUGGESTION —
|
…point (#1428) Closes #1371. Replaces #1382, which is closed in favour of this. #1382 was opened before #1354 landed and is now a strict superset of it: re-scoped here to the residual only, on a fresh base, with the property bound to the directory rather than to a list of files. ## The gap A permission grant states its condition as the literal presence or absence of a flag in the command text — above all *"no `--merge` means check-only"*. Argparse's default prefix abbreviation lets `--mer` resolve to `--merge` while the command text contains no such flag, so the written command and the resolved behavior diverge. That is exactly what such a condition has to be able to rule out. #1354 closed this on `babysit_merge.py` and `babysit_resolve_thread.py`. Seven entry points still inherited the default: `babysit_findings.py` · `manage_babysit_lease.py` · `manage_feedback_ledger.py` · `pr_queue_snapshot.py` · `prune_babysit_worktrees.py` · `refresh_pr_branch.py` · `request_review.py` All nine now set `allow_abbrev=False`. ## Why a gate, not seven more edits Hardening entry points one at a time is what let the gap survive #1354 for seven files. The guard contract gains a check over the whole catalogue: every catalogued Python entry point is invoked with `--hel` and must not exit 0. `--help` is registered on every parser and short-circuits parsing, so an abbreviation that *resolves* exits 0 before required-argument validation ever runs, while one that does not is a usage error. That makes the exit code a sufficient discriminator without a per-CLI argument shape — which is what makes this a gate over the catalogue rather than a hand-maintained list of cases. The message is deliberately not asserted: several of these parsers have a required mutually exclusive group that errors before any unrecognized argument is reported. A companion test asserts that discrimination against argparse itself rather than assuming it. ## Verification - `python -m unittest discover -s tests` — 387 tests, OK. - Detector verified rather than assumed: reverting `request_review.py`'s `allow_abbrev=False` fails the gate naming that file (`resolved the abbreviation --hel to --help and exited 0`), and it passes again on restore. ## Compatibility Abbreviated invocations that previously worked are now usage errors. That is the intent, and the version takes a minor bump for it — `source-control` 0.29.0 → 0.31.0 (0.30.0 is claimed by #1264, open). ## Related - #1354 — closed the same defect on the first two entry points - #1382 — the superset PR this replaces - #1285 — the guard contract this gate is added to --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#1539) ## Summary `plugins/source-control/bin/source-control-babysit-merge` exists to add exactly one refusal on top of `babysit_merge.py`: it rejects `--allow-unpinned-head` so that no unattended, allow-rule-covered invocation can merge an unvetted head. The guard's own comment says it "must not depend on the interpreter behind it" — but an `=value` spelling of the flag did exactly that. **Reproduced on `origin/main` before touching anything:** ```console $ bash bin/source-control-babysit-merge owner/repo#1 --allow-unpinned-head source-control-babysit-merge: --allow-unpinned-head is not permitted through the wrapper (--allow-unpinned-head or a prefix of it). Invoke babysit_merge.py directly for interactive unpinned use. RC=2 # the WRAPPER refused $ bash bin/source-control-babysit-merge owner/repo#1 --allow-unpinned-head=true usage: babysit_merge.py ... babysit_merge.py: error: argument --allow-unpinned-head: ignored explicit argument 'true' RC=2 # argparse refused — the wrapper let it through ``` Both exit 2, but for different reasons. The guard is `[[ "$arg" == --a* && "--allow-unpinned-head" == "$arg"* ]]` — a test that `$arg` is a *prefix* of the flag. `--allow-unpinned-head=true` is not a prefix of `--allow-unpinned-head` (the `=true` tail breaks the comparison), so the wrapper's own filter never fires; the CLI happens to reject it today only because the flag is `argparse store_true`, which refuses an explicit value. The refusal is real, but incidental — the moment the guarded flag (or an equivalent guarded flag) accepts a value, this same test stops refusing anything while looking identical, and nothing fails loudly to say so. ### Fix Strip a `--flag=value` tail (`stem="${arg%%=*}"`) before the prefix comparison, so the guard tests the option name rather than the raw argument. No exact spelling that previously refused changed behavior — the stem of an already-refused argument is unchanged, and a stemmed sibling flag (`--allow-dependency`, `--allow-unprotected`, `--allowed-owners=<value>`) still isn't a prefix of `--allow-unpinned-head`, so it still reaches the CLI unmolested. ## Test plan - [x] **Red first.** Wrote the `check_wrapper_refusal` rows in `engine.test.sh` and the two new `guard_contract.py` rows against unmodified `origin/main` and confirmed they fail: 3 bash rows FAIL (`--allow-unpinned-head=true`, `--allow-unpinned=1`, `--allow-unpinned-hea=1` all reach argparse instead of the wrapper) and 2 Python `test_every_refusal_row` subtests FAIL the same way. - [x] **Assertions check the wrapper's own refusal *text*, not exit 2.** Exit 2 is overloaded between the wrapper's refusal and argparse's own usage/rejection errors on this path, so an exit-code-only assertion would have passed before and after this fix for different reasons (exactly the trap `--allow-unpinned-head=true` is). `engine.test.sh` gained a `check_wrapper_refusal` helper that greps stderr for the wrapper's refusal text; `guard_contract.py`'s existing framework already asserts no JSON envelope was emitted (proof Python never ran) for `bash-wrapper`-attributed rows. - [x] **New engine.test.sh rows:** `--allow-unpinned-head=true`, `--allow-unpinned=1`, `--allow-unpinned-hea=1` (all refused by the wrapper) plus no-over-refusal rows for `--allow-dependency`, `--allow-unprotected`, and `--allowed-owners=owner` (all still reach the fail-closed CLI, verified via an out-of-scope-owner exit 3 so no network call is needed). - [x] **New guard_contract.py rows:** `merge.equals-value-unpinned-head-refused-by-wrapper` and `merge.equals-value-abbreviated-unpinned-head-refused-by-wrapper`, each citing #1522. Also updated `wrapper_denies()` (used by the doc/parser cross-check `test_every_wrapper_refusal_row_reaches_the_denial_table`) to strip the same `=value` tail before its own prefix check, so the two new rows don't fail that self-consistency test for an unrelated reason. Regenerated `reference/guard-contract.md` via `python tests/guard_contract.py --emit`. - [x] **Green after the fix:** all `engine.test.sh` wrapper rows PASS; full `unittest discover` suite (442 tests, includes both new rows and the existing 5 unpinned-head-family rows) OK; `ruff check` clean; `shellcheck -x` on the wrapper clean. - [x] **CHECK-SKILL babysit-prs: PASS** via `scripts/check-changed-skills.sh origin/main` (trigger phrases preserved, `engine.test.sh` passes). Note: this machine's default `python3`/`python` on PATH resolve to a broken local interpreter install unrelated to this change (a `dataclasses` import failure); the gate was run with a working Python 3.11+ interpreter on PATH, and the identical failure reproduces on unmodified `origin/main` too, confirming it's pre-existing and environmental, not a regression. - [x] Repo gates run locally: `scripts/check-changelog-parity.sh --check` and `--check-bump origin/main` ✔; `scripts/validate-plugins.sh` ✔; `markdownlint-cli2` on the touched docs clean. - [x] Version bumped `0.32.0` → `0.32.1` with the matching CHANGELOG entry. ## Related Closes #1522. Same failure family as #1371 (fixed by #1354, which made the guard prefix-aware) — this closes the one spelling that fix's prefix comparison still missed. Found during independent verification of #1405, per #1522's own description; not introduced by that PR. --- *This was generated by AI during work-loop execution.* 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Both guarded babysit scripts built their parsers with bare
argparse.ArgumentParser(description=__doc__), leavingallow_abbrevat its defaultTrue. Replayed empirically:--iresolves to--include-humanand--merto--merge, while the command TEXT contains neither. Every permission condition stated as the literal presence or absence of a flag — the standard melodic-software/dotfiles#315's auto-mode allow entries are held to — was therefore defeasible by prefix spelling: a lane could resolve a human-participating thread, or land a merge with no grant anywhere, while every stated condition read as satisfied.Changes:
allow_abbrev=Falseon both parsers (babysit_resolve_thread.py,babysit_merge.py), with the constraint documented at each site.--allow-unpinned-headrefusal is now prefix-aware instead of exact-equality —--allow-unpinned-heapreviously sailed past the wrapper and argparse accepted it. The wrapper is the allow-rule boundary and must not depend on the interpreter behind it.babysit_classifyvocabulary — CRITICAL/IMPORTANT surviving negation redaction, shields P0–P3 badge, bracketed [P0]–[P3]) or the word "security" is refused in--autonomousmode, failing closed when the comment page is truncated. Interactive modes are unchanged; severity judgment there stays with the evaluating agent. Newskipped-severity-markedaction andskippedSeverityMarkedsummary counter.Tests
AP1000).Related
No linked issue. Cross-repo counterpart of melodic-software/dotfiles#315 (verifier finding F1 and the two live Codex P1 review threads there); the dotfiles allow entries' flag-literal conditions become sound once this lands in the installed plugin cache.
🤖 Generated with Claude Code