Skip to content

Enforce the git chokepoint invariant, and close the two remaining bypasses #390

Description

@pbean

AGENTS.md states the invariant:

All git subprocess calls go through the _run_git chokepoint in verify.py (timeouts,
LC_ALL=C) — no bare subprocess git.

Nothing enforces it. tests/test_portability_guard.py is the only AST scan over
src/bmad_loop, and it collects six finding kinds — ["tmux", …] argv, hardcoded POSIX paths,
SIGKILL, os.kill, start_new_session=True, shell=True. There is no git detector and no
subprocess.run detector at all. AGENTS.md is asymmetric about this too: the tmux quarantine
line explicitly names its enforcer, the git line names none.

That is how #389's bypass survived as long as it did — it was found by reading, not by CI.

What exists to build on

The ["tmux", …] detector is structurally the same shape: an ast.List whose first element is
the string constant. A git-flavoured copy would fire on ["git", "-C", str(repo), *args]
(still an ast.List with "git" first). verify.py is the allowlisted owner.

Two live bypasses remain after #389

  • tui/app.py:_commit_subject — the one worth fixing first. text=True with no
    LC_ALL=C, guarded by except (OSError, subprocess.SubprocessError). UnicodeDecodeError
    is a ValueError and is in neither arm, so a commit subject carrying bytes invalid in the
    run's codec crashes the TUI render. This is exactly the hole _run_git's decode fault escapes the GitError taxonomy on -z output #377 closed at the chokepoint,
    still open here. It also carries its own 5s timeout, ignoring limits.git_timeout_s, and has
    no comment explaining the exemption.
  • install._warn_if_policy_trackedgit ls-files --error-unmatch with cwd= and a 10s
    timeout. Benign (best-effort, rc-only), but undocumented as an exemption.

Scope

  1. Add the git-argv detector to tests/test_portability_guard.py, allowlisting verify.py.
    Ablate it: reintroduce a bare git spawn elsewhere and confirm the guard FAILS.
  2. Route both sites above through the chokepoint (git_bytes for _commit_subject, since the
    TUI must not raise on a subject it cannot decode), re-deriving their guards the way Route install._shield_git through verify.git_bytes now that #377 has landed #389
    did — a timeout is GitError, a spawn fault GitSpawnError.
  3. If either site genuinely cannot be routed, the allowlist entry needs a comment saying why,
    so the invariant and the code stop disagreeing.

Follow-up to #389 / #385.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions