feat: agent guardrails (verify_commit, dispatch audit, opencode guardrails plugin) #110

Merged
alee merged 45 commits from feat/agent-guardrails into main 2026-10-05 01:31:12 +00:00
Owner

What

Turns the worker rules that live in prose in AGENTS.md into code a model cannot skip:

  • scripts/verify_commit.py: one command that answers "is this commit good?" (tests the commit touched, run against the committed tree; ruff new-findings gate), a few lines of output.
  • scripts/oc_dispatch_audit.py: audits an orchestrator session's task() dispatches (dead dispatch, banned agent, missing WORKTREE line, idle child).
  • deploy/opencode-plugin/guardrails.js: an opencode plugin whose tool.execute.before hook blocks or rewrites tool calls that break the rules, plus a plan_tick_gate that refuses a plan tick while verify_commit.py fails.
  • deploy/opencode-plugin/guardrails-replay.mjs: replays real recorded sessions through the plugin before install, read-only, to calibrate false positives. Results are in plans/agent-guardrails-replay.md.
  • Docs in docs/agent-guardrails.md; example config in deploy/opencode-plugin/guardrails.example.json.

25 files, +8511 / -0. Nothing under src/ or config/ changes; the plugin is not installed by this PR (operator step, below).

Why

Each rule is the code form of something that broke on this repo: 12 dispatches to oh-my-claudecode:writer did no work; task() with no category/subagent_type reported "completed"; a worker with no worktree rewrote the live config.yaml; Atlas pushed and opened a PR unasked; a todo was ticked over 3 failing tests; a git stash moved the owner's uncommitted changes between worktrees. The evidence table is in docs/agent-guardrails.md.

How it was verified

Built through the opencode pipeline (Prometheus plan, Claude review, Atlas execution) over seven audited rounds. Every round's "APPROVE" was re-checked independently, and each audit found defects the previous round missed, so the final state was probed at the real hook boundary rather than trusted from tests:

  • Full suite on a clean git archive extraction of HEAD: 2605 passed.
  • python3 scripts/verify_commit.py --repo <wt> --base origin/main --full HEAD: exit 0 (PASS on tests and lint).
  • node --test deploy/opencode-plugin/: 328 of 328.
  • Real-shape probes (the hook is called as opencode calls it: input = {tool, sessionID, callID}, output = {args}) for every defect class found along the way, e.g. a category-only task must be allowed; grep -n commit AGENTS.md, pytest -k restore and python3 -c "... > 300" must not be mistaken for git operations or redirects; git -c k=v commit, env VAR=x git stash and git commit -a -m x must still block.
  • A fresh read-only replay of about 4,800 real recorded tool calls: 28 blocks, 26 true positives, 2 false positives accepted with reasons; bash_protected_port has zero blocks.

Known gaps (accepted, documented)

  • bash -c '...' / sh -c '...' wrappers are not inspected.
  • A script file that calls port 8080 internally is not detected.
  • A commit message that names localhost:8080 is blocked by bash_protected_port.
  • A relative cd chain after an absolute one can be mis-anchored (1 replay block).

Operator steps after merge (not part of this PR)

  1. cp deploy/opencode-plugin/guardrails.js ~/.config/opencode/plugins/ and cp -n deploy/opencode-plugin/guardrails.example.json .omo/guardrails.json.
  2. Restart opencode. The plugin must export only functions, or opencode rejects it (failed to load plugin in opencode.log if not).
  3. Every rule defaults to block; set a rule to warn or off in .omo/guardrails.json. Consider warn on bash_main_checkout for the first week and watch .omo/guardrails.log.

Merge advice

History has warts from the pipeline (an add and remove of two .omc/ state files, three duplicate F841 commits, a few style commits). Squash merge is the cleaner landing; a merge commit works too.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa

## What Turns the worker rules that live in prose in `AGENTS.md` into code a model cannot skip: - `scripts/verify_commit.py`: one command that answers "is this commit good?" (tests the commit touched, run against the committed tree; ruff new-findings gate), a few lines of output. - `scripts/oc_dispatch_audit.py`: audits an orchestrator session's `task()` dispatches (dead dispatch, banned agent, missing WORKTREE line, idle child). - `deploy/opencode-plugin/guardrails.js`: an opencode plugin whose `tool.execute.before` hook blocks or rewrites tool calls that break the rules, plus a `plan_tick_gate` that refuses a plan tick while `verify_commit.py` fails. - `deploy/opencode-plugin/guardrails-replay.mjs`: replays real recorded sessions through the plugin before install, read-only, to calibrate false positives. Results are in `plans/agent-guardrails-replay.md`. - Docs in `docs/agent-guardrails.md`; example config in `deploy/opencode-plugin/guardrails.example.json`. 25 files, +8511 / -0. Nothing under `src/` or `config/` changes; the plugin is **not installed** by this PR (operator step, below). ## Why Each rule is the code form of something that broke on this repo: 12 dispatches to `oh-my-claudecode:writer` did no work; `task()` with no category/subagent_type reported "completed"; a worker with no worktree rewrote the live `config.yaml`; Atlas pushed and opened a PR unasked; a todo was ticked over 3 failing tests; a `git stash` moved the owner's uncommitted changes between worktrees. The evidence table is in `docs/agent-guardrails.md`. ## How it was verified Built through the opencode pipeline (Prometheus plan, Claude review, Atlas execution) over seven audited rounds. Every round's "APPROVE" was re-checked independently, and each audit found defects the previous round missed, so the final state was probed at the real hook boundary rather than trusted from tests: - Full suite on a clean `git archive` extraction of HEAD: **2605 passed**. - `python3 scripts/verify_commit.py --repo <wt> --base origin/main --full HEAD`: **exit 0** (PASS on tests and lint). - `node --test deploy/opencode-plugin/`: **328 of 328**. - Real-shape probes (the hook is called as opencode calls it: `input = {tool, sessionID, callID}`, `output = {args}`) for every defect class found along the way, e.g. a category-only `task` must be allowed; `grep -n commit AGENTS.md`, `pytest -k restore` and `python3 -c "... > 300"` must not be mistaken for git operations or redirects; `git -c k=v commit`, `env VAR=x git stash` and `git commit -a -m x` must still block. - A fresh read-only replay of about 4,800 real recorded tool calls: 28 blocks, 26 true positives, 2 false positives accepted with reasons; `bash_protected_port` has zero blocks. ## Known gaps (accepted, documented) - `bash -c '...'` / `sh -c '...'` wrappers are not inspected. - A script file that calls port 8080 internally is not detected. - A commit message that names `localhost:8080` is blocked by `bash_protected_port`. - A relative `cd` chain after an absolute one can be mis-anchored (1 replay block). ## Operator steps after merge (not part of this PR) 1. `cp deploy/opencode-plugin/guardrails.js ~/.config/opencode/plugins/` and `cp -n deploy/opencode-plugin/guardrails.example.json .omo/guardrails.json`. 2. Restart opencode. The plugin must export only functions, or opencode rejects it (`failed to load plugin` in `opencode.log` if not). 3. Every rule defaults to `block`; set a rule to `warn` or `off` in `.omo/guardrails.json`. Consider `warn` on `bash_main_checkout` for the first week and watch `.omo/guardrails.log`. ## Merge advice History has warts from the pipeline (an add and remove of two `.omc/` state files, three duplicate F841 commits, a few style commits). Squash merge is the cleaner landing; a merge commit works too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa
alee added 21 commits 2026-10-04 08:45:17 +00:00
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
alee added 9 commits 2026-10-04 10:23:36 +00:00
Defects fixed:
- (k) Rewrite targets output.args.prompt, not output.prompt
- (l) Prepended line uses actual main checkout directory, not parent of worktree
- (m) Parser accepts 'path. cd there', 'path -- cd there', 'path' forms;
      dotted paths (e.g. feat.v2) are no longer truncated
Defect n: check_lint now filters ruff output to only match real finding
lines (pattern: <path>:<line>:<col>: <CODE> <message>). Summary lines
like 'All checks passed!', 'Found N error(s).', and '[*] N fixable ...'
are ignored.
Defect o: When tests FAIL, the detail is at most 20 lines total.
- Fixed ERROR prefix: pytest prints 'ERROR ' (not 'ERRORS ')
- Truncation with '... and N more' when summary exceeds 20 lines
- Fallback (last 20 lines) unchanged
Defect p: guardrails-replay writes synthetic config/boulder to a temp
directory (mkdtempSync) via new omoDir parameter, never under --directory.

Defect q: getStatBucket groups "bash_banned" and "bash_protected_port"
into "Always-Scope" table; all other rules into "(B) Scoped".

ruleId enrichment: every Error thrown by a guardrail rule now carries
err.ruleId (one of the 8 config IDs).

omoDir threading: Guardrails factory takes an omoDir param (defaults to
<directory>/.omo). loadConfig, readBoulder, _openLog, and all scope-gated
checks (checkWorktreeLine, checkWriteOutsideWorktree, checkBashMainCheckout,
checkPlanTickGate) thread a 'base' arg so they read artifacts from omoDir.

Cache invalidation: added _boulderBase and _configBase alongside _boulderDir
/_configDir. Cache key is (directory, base, mtime) to prevent stale reads
when omoDir differs across sessions.

Per-session scope in replay: Guardrails reads boulder.json once at factory
creation. Replay loop creates one factory per session, writing that session's
boulder to disk before instantiation.

Tests: 219 pass (guardrails.test 184 + guardrails-replay.test 5 +
guardrails.contract.test 30).
alee changed title from fix(opencode-plugin): replay reads the real opencode API; add the real calibration report to fix(opencode-plugin): replay isolation, omoDir, ruleId enrichment, per-session scope 2026-10-04 10:24:18 +00:00
alee added 15 commits 2026-10-04 20:58:51 +00:00
Defect r: a newline inside a quoted string was splitting bash commands, causing false blocks.
- Replaced regex split with a manual parser tracking single, double, and backtick quotes.
- Added support for heredocs (<<EOF ... EOF) so their bodies stay as single segments.
- All existing tests pass; added cases for multi-line node/python calls and heredocs.
Fix defect t: checkBashMainCheckout redirect exemption now uses
join(directory, '.omo') matching checkWriteOutsideWorktree.

Fix defect u: replay deletes its temp omoDir on exit via try/finally.

Add R15 missing tests:
- read-only --directory: verifies .omo files unchanged, no new files
- one-call-per-rule: fixture triggers each rule, checks Always/(B) tables
- per-session scope: two sessions with different worktrees don't block
alee changed title from fix(opencode-plugin): replay isolation, omoDir, ruleId enrichment, per-session scope to feat: agent guardrails (verify_commit, dispatch audit, opencode guardrails plugin) 2026-10-04 22:06:21 +00:00
alee merged commit eb090f1e64 into main 2026-10-05 01:31:12 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/6krrt#110