Enforce guard on each write

Task 5933 · Done · Coder · created · updated

Epic 5896 · child C5b of the Architectural Guard — the per-write hook, Claude runners and console sessions. Full design: architecture (Task 5898 `architecture`) §5.3, §5.5, §5.6, §6; PRD Task 5897 `prd`. The Codex/Gemini/Antigravity legs are a later child (C5d); build the hook runtime-neutral now. ## Epic requirement (verbatim) > Build a solution for this (an advanced feature of Compliance - Advanced Compliance Plan activated): > In large enterprises, improvisation breaks design systems. Jaah can win by offering **absolute, unyielding architectural enforcement**. > - **The Mechanism:** Instead of just checking if code passes a test, Jaah enforces rigid code blueprints, strict linting arrays, and structural enterprise design standards across thousands of repositories simultaneously. > - **The Breakthrough Feature:** A real-time "Architectural Guard." If an agent tries to write a functioning feature but uses an unapproved API pattern or deviates from the corporate Monorepo style guide, Jaah's harness blocks the agent mid-execution, rewrites its context, and forces compliance before a single line is even committed to a branch. ## This child's requirements (PRD verbatim) - R3.1: **Each write.** Every file write/edit tool call by an agent is evaluated against the in-scope gate/lock rules BEFORE it lands. A violation denies the tool call, and the file on disk is unchanged. - R2.1 (enforcement half): A structural rule at gate/lock is enforced by the guard on the agent's working tree whenever the rule's check can be evaluated on that tree. - R3.3: **Coverage:** all agent runtimes Jaah launches (Claude, Codex, Gemini, Antigravity), and both fleet task runners and human-driven console sessions on in-scope projects. (Claude runners + console sessions here; others C5d.) - R3.4: **Only new violations block (diff-aware).** - R4.1: A denial returns to the agent, as injected context: the rule name and key, its mode, the file:line of each new violation, the rule's rationale, and its **approved alternative / fix guidance**. The agent then retries compliantly in the same session. It is not restarted. - R4.2: **Up-front injection:** at session start, every guarded session gets a concise list of the gate/lock rules in scope for its repo (name, one-line intent, approved alternative). The agent knows the blueprint before writing. - R1.2/R1.3/R2.3 enforce legs: unavailable → enforces nothing; switch/rule edits reach running sessions within 5 minutes. ## Approach - `tools/architectural_guard.py`, run as `/usr/bin/python3 -I` with the `packaging/jaah-audit` sys.path setup. Modes `--pre-tool` (stdin hook JSON; non-write/non-shell tool or `JAAH_GUARD_BUNDLE` unset → exit 0 in <50 ms) and `--session-start` (prints `guard.brief`). Leave clean seams for C5c's shell/`--pre-push` modes and C5d's `--runtime codex` arm. - Calls `guard.evaluate_write` (C2) with `toolinput` changes; facts = confirmed ∪ inferred (cached per HEAD sha in the state file) − rejected; paths outside the worktree ignored; undecidable → allow + record `error` (G-1). - Deny output (Claude shape): `hookSpecificOutput.permissionDecision=deny` + `permissionDecisionReason` = the pinned C2 message. - Records denied/error/waived (waived once per session per rule, with `detail.waiver_id`) via `guard.record`; unsent → spool (C5a). - State file (`JAAH_GUARD_STATE`, keyed by session_id; pinned S6 keys): `{session_ref: {rule_ref: consecutive_refusals, escalated: {rule_ref: "pending"|"answered"}, brief_sent}}` — a denial increments each denied rule; an allowed write matching a rule's globs resets it. (Escalation behaviour itself is C6.) - Wiring: `fleet_launch._lifecycle_chains` appends `architectural_guard.py --pre-tool` to `on_pre_tool` and `--session-start` to `on_session_start` for BOTH `role="runner"` and `role="user"`, only when `guard=True` (C5a). Wire `on_pre_tool` ("unwired today", `base.py:39-40`) in the Claude renderer (`agents/claude.py`) with a render test. - Additive chain entry — never replaces `check_gh_pr_guard.py`, the access guard or `--no-verify` refusal. ## Acceptance criteria (verbatim) - AC-6: **Per-write block, Claude runner.** A forbidden-pattern gate rule denies a write that introduces the pattern. The file is unchanged on disk, and the agent receives the rule, file:line, rationale and fix guidance, then retries compliantly. A compliant write is allowed. - AC-7 (console-session leg here): Same as AC-6 for … a human-driven console session on the project. - AC-9: **Diff-aware.** A file already containing the forbidden pattern can be edited elsewhere without denial. Adding a second occurrence is denied. - AC-10: **Up-front injection:** a guarded session's start context lists the in-scope gate/lock rules (name, intent, approved alternative). An unguarded or unavailable org's session has no such list. - AC-5: Scale: one rule scoped by predicate (e.g. language=python) applies to every matching project in the org, and to none outside the scope, with no per-repo step. - AC-15: **Switch-off and edit propagation:** turning the switch or the rule off, or changing its mode, takes effect in new sessions immediately and in running sessions within 5 minutes. Rules and modes are preserved across off→on. - AC-1 (enforce part): a guarded session blocks only for on+on. - AC-17 (live leg): **Quality of the block message:** names the rule in words, quotes the offending line, gives the approved alternative; a reviewer reading only the message can tell what to change. Credentials & reach: - AC-1 (enforce leg), AC-5, AC-6, AC-9, AC-10, AC-15 (live Claude runner on a guarded test project) | `jaah/debug/main/operator-login` / dev; Claude runner accounts (Machines › Brains: `acct1`, `acct2`, API-key accounts all "available") | verified: the Brains tab shows available Claude accounts. - AC-7 console session | same credential / dev console | verified. - Test org with both switches on | same credential / dev; org-b | verified. - Test projects and repos: ≥2 in-scope Python projects + 1 out-of-scope project (AC-5); one repo seeded with `import requests` (AC-9) | `users/roman@gmind.tech/github` / GitHub user `ruzhukov` / github.com. Create throwaway repos, add them as org-b projects through the console, seed the files | identity verified (on the roster, `task_credential_get` succeeded, read-only `GET /user` returned `ruzhukov`). Repo-creation rights deferred to this child: probe them first and ASK if refused. (`GitHub (robgmind)` returned 401 and is not used.) - Browser host | — | `https://app.dev.jaah.ai`. Out of scope: Do NOT auto-rewrite the agent's code — refuse and guide only. Do NOT block on pre-existing violations (D-6). Do NOT require per-repo setup or a committed file in managed repos. Do NOT weaken or replace existing deny hooks (`check_gh_pr_guard.py`, pre-push access guard, `--no-verify` refusal). Do NOT change any launch for an unavailable org (byte-identical settings). No commit/push check or lint (C5c), no non-Claude renderers (C5d), no escalation question (C6). Do NOT edit `infra/host/fleet-harness/settings.json` to wire the guard. Done means (dev oracles after your PR's Jaah Deploy to dev succeeds; org-b with both switches on): - AC-6 — create rule "forbid `import requests` in `**/*.py`, use `httpx`" at gate on a test project; start a coder task asking for an HTTP call; in Audit › Sessions observe the denial with guidance, then an `httpx` write that lands; file unchanged after the denial; Guard activity entry present. - AC-7 console leg — repeat from a console session on the project; observe the denial in the transcript. - AC-9 live — seeded `import requests` file: unrelated edit allowed; a second `import requests` denied. - R2.1 live — a deterministic structural rule at gate: a write flipping it pass→fail is denied. - AC-10 — session-start context lists the rule (name, intent, approved alternative); a session in an org with Advanced Compliance off has no list. - AC-5 — 2 in-scope Python projects deny, 1 out-of-scope project allows, no per-repo step. - AC-15 — toggle the switch off during a live session; within ≤5 min the write is allowed; toggle back, rules and modes intact, write denied again. - AC-1 enforce — block observed only for on+on. - AC-17 live — paste the transcript excerpt of the denial into your report. - Unit: Claude render test for `on_pre_tool`; `guard=False` launch byte-identical; <50 ms early exit; `./dc check full` green.

Outcome

Per-write Architectural Guard hook shipped (Claude runners + console). PR #5200 merged, Jaah Deploy succeeded, full gate green. Proven on a real Claude CLI (deny→httpx retry, diff-aware, brief). Dev oracles not run: app.dev.jaah.ai unreachable from this host.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)