Skip to content

guardrails 2.x: detect tampering of plugin-owned state instead of parsing every shell spelling (OC-N6) #354

Open

Description

@terisuke

Owner decision (Terada Kousuke, 2026-10-25), verbatim: 「(S) + (P)でいきましょう。」 — (S) ship #353 round 3 at a8d90ac299 after a regression-only acceptance; (P) move the guarantee for .opencode/guardrails/state.json from prevention by parsing shell text to detection of any change.

Why

#353 rounds 1–3 (deny-list, then the positive shape of ADR-012) and 8 Codex route-A passes found 14 P1 spellings in total that reach sed or a writer through quoting, escapes, ANSI-C quotes, comments, line continuations and command substitution. Each fix narrowed the guard; none converged. A shell cannot be judged from its text with a bounded rule set, so the text guard stays as early, best-effort refusal and is not repaired further (ADR-012 consequences, aidd-governance). The guarantee has to come from a layer where the problem is finite: the file either changed or it did not.

Proposal

  1. Every write the plugin makes to state.json also records sha256(state.json) (and mtime) in a plugin-private location outside the project, e.g. $XDG_STATE_HOME/opencode/guardrails/<project-id>.integrity.json (the opencode2 launcher's XDG state dir, not inside the repo and not under .opencode/).
  2. Every read of state (stash(state) in guardrail-context.ts) compares the file's hash with the recorded one before trusting it.
    • Match: proceed.
    • Mismatch, or no record: treat the state as reviewed=false, factcheck=false, edits_since_review unknown (the conservative values), rewrite it, append a ledger row event=block decision=deny rule=state.tampered with the two hashes, and mark last_block: "state.tampered".
  3. The integrity record is keyed by project id, so a worktree or a copy of the repo does not inherit another's record.
  4. A tamper is also surfaced to the user the same way the headless launcher shows H17 verdicts (ADR-010: ledger tail), so a model that rewrote the state cannot pass /ship quietly.

Done when

  • A red test: write {"reviewed":true} into state.json directly (any means — sed -i, printf >, python -c) between two plugin reads; the second read reports state.tampered, the state is back to reviewed=false, and the ledger row exists. Before the change the second read trusts the file.
  • The existing 148-test suite stays green; the text guard is untouched.
  • A note in packages/guardrails/README.md: the text guard is best-effort; integrity detection is the guarantee; known bypasses of the text guard are listed here, not fixed.

Known bypasses of the text guard (information, not to be fixed one by one)

  • Codex route A r8 on a8d90ac299: a { inside a word resets the comment state in rawSegments.
  • Anything the independent acceptance of round 3 lists under "known bypasses".

Refs #349, #351, #352, #353; aidd-governance ADR-012.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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