Read the script
#!/usr/bin/env python3
"""Plugin guard layer (design.md piece 3 + RC1/RC4 + invocation control).
One dispatcher, selected by argv[1] — registered in hooks/hooks.json.
Exit 0 = allow · exit 2 = block (stderr is the redirect-to-`harness` message).
Per-guard policy (declared, a "keeping" from the original):
bash fail-open on unparseable payload — the HMAC chain (RC4) is
the guarantee for authority files; this guard is fast-fail.
Its raw-commit/merge/rebase/.../push block (GIT_VERB_RE) is
a STANDING, workspace-scoped invocation rule, not a
run-state guard: it applies for the life of a harness
workspace — from the moment `/init-workspace` completes,
regardless of whether any `ai/<run>/` currently exists —
unlike `spawn` below, there is no "no run yet" carve-out
for it (adversarial-review finding: previously
undocumented, easy to mistake for a run-scoped check like
the others in this list). It is NOT session- or repo-wide
beyond that: `_is_harness_workspace` gates it on the
`/init-workspace` bootstrap marker, so a session touching
an unrelated, never-initialized repo sees raw git
untouched — see `_is_harness_workspace` for the one
documented residual this leaves open.
write fail-open on unparseable payload; fail-closed on authority
paths (they are never legal via tools).
spawn FAIL-CLOSED: no run -> no harness-shape spawns beyond the
declared out_of_run exceptions; integrity failure blocks
spawn-legality from JUST the corrupt run, not the rest of
the workspace (harness reseal is the recovery verb).
skill fail-closed for user-entry skills from subagent context.
user-prompt never blocks (capture only).
subagent-stop never blocks (capture + stall detection feed events ledger).
If PyYAML is missing, the yaml-needing guards DEGRADE OPEN with one visible
remediation line on stderr (exit 0 — see main()'s YamlMissing handler; the
yaml-free bash/write guards keep blocking); init-workspace verifies the
dependency up front, and the HMAC chain (RC4) still detects authority-file
tampering even with guards down — defense in depth, guard = fast-fail,
chain = guarantee. The same posture covers a missing INTERPRETER: if the
hook launcher pair (hooks/run-guard + run-guard.cmd, registered in
hooks.json) finds no runnable python at all — including
the Windows Store alias that answers to `python`/`python3` but only prints
an install nag — the hook errors non-2 and the platform treats it as
non-blocking. Accepted: pre-venv, nothing harness-y can execute anyway.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import sys
import tempfile
from pathlib import Path
PLUGIN_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PLUGIN_ROOT))
# stdlib-only modules at import time. `transitions` qualifies: it imports
# nothing but json/sys/pathlib plus these two, and reaches for PyYAML only
# lazily, inside the declared-data readers this file calls from paths that
# have already loaded surfaces.yaml (i.e. already required it). `gates`
# qualifies outright — hashlib + re, no I/O, no env — and is imported for
# `session_digest` alone, so the capture hook and the CLI that writes the
# stamp cannot drift into two different hashes of the same session id.
from harness import chain, gates, ndjson, transitions # noqa: E402
class YamlMissing(Exception):
pass
def load_yaml(path: Path):
"""Lazy YAML — only the spawn/skill guards need it. The bash/write/capture
guards are pure regex+payload and must keep working (and blocking!) on a
Python without PyYAML, e.g. macOS system python3 before setup."""
try:
import yaml
except ImportError:
raise YamlMissing(
"ai-sdlc-harness: PyYAML missing for this hook's interpreter — "
"/init-workspace bootstraps the plugin venv; until then this "
"guard degrades open.") from None
with path.open(encoding="utf-8") as fh:
return yaml.safe_load(fh)
_TERMINAL_CACHE: tuple = ()
def _terminal_statuses() -> tuple:
"""The task FSM's declared terminal statuses — the SAME `terminal:` list
`harness.transitions.terminal_statuses` reads, not a second copy. A guard
that disagreed with the engine about what "finished" means would refuse a
spawn the engine considers legal, or wave one through that it doesn't."""
global _TERMINAL_CACHE
if not _TERMINAL_CACHE:
fsm = load_yaml(PLUGIN_ROOT / "pipeline" / "task-fsm.yaml") or {}
_TERMINAL_CACHE = tuple(fsm.get("terminal") or ())
return _TERMINAL_CACHE
# A single "word" a flag's value can take — a whole quoted string (its
# space(s) included) counts as ONE token, not just up to the first
# whitespace (adversarial-review round 3 finding: plain `\S+` matched only
# `-C "my` out of `-C "my repo" commit`, leaving `repo" commit` unable to
# reach the verb — silently reopening the bypass for any quoted,
# space-containing flag value). Round 4 finding: a token can also be a MIX
# of bare and quoted segments the way the shell itself tokenizes —
# `-c user.name="My Name"` is ONE shell word, but the round-3 alternation
# (whole-quoted OR \S+) consumed only `user.name="My` and the parse died
# before the verb, reopening the same bypass one level down. One-or-more
# runs of (quoted segment | bare segment) matches shell word semantics.
_GIT_TOKEN = r"""(?:"[^"]*"|'[^']*'|[^\s"'|;&])+"""
# `git` not immediately preceded by a quote char (adversarial-review round
# 3 finding): without this, `grep -rn 'git reset --hard' .` — a pure read,
# searching for the LITERAL PHRASE — blocked, because "git reset --har