feat(agent-loop): audit signed-state drift read-only#204
Conversation
📝 WalkthroughWalkthroughAdds a scheduled, read-only signed-state drift audit with pinned branch validation, categorized diagnostics, regression tests, workflow safety checks, and WS-ENG-008-02 assurance metadata. ChangesSigned-state audit
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Scheduler as GitHub Actions
participant API as GitHub API
participant Checkout as Pinned state checkout
participant Audit as Drift audit CLI
Scheduler->>API: Read main and automation/loop-memory tips
Scheduler->>Checkout: Materialize exact signed-state SHA
Scheduler->>Audit: Run audit with expected repository and state SHAs
Audit->>API: Re-check branch tips
Audit->>Checkout: Validate signature, tree, semantics, contract, and ancestry
Audit-->>Scheduler: Return pass or categorized diagnostics
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/loop-memory-drift-audit.yml (1)
64-77: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winRoute step outputs through
env:instead of inline template expansion.
${{ steps.tips.outputs.main_sha }}/state_shaare expanded directly into the shell script text. Static analysis (zizmor) flags this as template-injection risk. The upstream regex validation (^[0-9a-f]{40}$) at capture time mitigates it today, but passing these throughenv:and referencing"$MAIN_SHA"/"$STATE_SHA"removes the injection surface entirely as defense-in-depth, independent of that validation staying correct forever.🔒 Proposed fix
- name: Audit signed state without mutation authority shell: bash env: GH_TOKEN: ${{ github.token }} + MAIN_SHA: ${{ steps.tips.outputs.main_sha }} + STATE_SHA: ${{ steps.tips.outputs.state_sha }} run: | set -euo pipefail python3 scripts/audit_loop_memory_drift.py \ --repository-root "${GITHUB_WORKSPACE}" \ --state-root "${GITHUB_WORKSPACE}/loop-memory-state-audit" \ --public-key .agent-loop/keys/loop-memory-signing-public.pem \ --repository "${GITHUB_REPOSITORY}" \ - --expected-main-sha "${{ steps.tips.outputs.main_sha }}" \ - --expected-state-sha "${{ steps.tips.outputs.state_sha }}" \ + --expected-main-sha "${MAIN_SHA}" \ + --expected-state-sha "${STATE_SHA}" \ --diagnostic-output "${RUNNER_TEMP}/loop-memory-drift-audit.json"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/loop-memory-drift-audit.yml around lines 64 - 77, Update the “Audit signed state without mutation authority” step to pass the tips step outputs through its env block using dedicated MAIN_SHA and STATE_SHA variables, then replace the inline template expressions in the Python command with quoted shell references to those variables; leave the existing validation and audit arguments unchanged.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/audit_loop_memory_drift.py`:
- Around line 63-72: Update _state_main_sha to validate that the parsed
STATE.json value is a dictionary before calling state.get or indexing its source
field. Treat any non-dictionary top-level JSON value as corruption by raising
AuditError with the existing bounded diagnostic, while preserving the current
main_sha validation for valid dictionary states.
---
Nitpick comments:
In @.github/workflows/loop-memory-drift-audit.yml:
- Around line 64-77: Update the “Audit signed state without mutation authority”
step to pass the tips step outputs through its env block using dedicated
MAIN_SHA and STATE_SHA variables, then replace the inline template expressions
in the Python command with quoted shell references to those variables; leave the
existing validation and audit arguments unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 500ff338-af32-46a5-bb3c-cd3ff77800fd
📒 Files selected for processing (10)
.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-02-internal-review-evidence.md.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-02-pr-trust-bundle.md.agent-loop/merge-intents/WS-ENG-008-02.json.agent-loop/policies/repository-engineering-policy.md.github/workflows/loop-memory-drift-audit.ymldocs/operations_post_merge_memory.mdscripts/audit_loop_memory_drift.pyscripts/test_agent_gates.pyscripts/test_audit_loop_memory_drift.py
| def _state_main_sha(state_root: Path) -> str: | ||
| try: | ||
| state = json.loads((state_root / ".agent-loop/STATE.json").read_text("utf-8")) | ||
| event = state.get("event") | ||
| value = event.get("main_sha") if isinstance(event, dict) else state["source"]["main_sha"] | ||
| except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError) as exc: | ||
| raise AuditError("corruption", "canonical state has no readable main identity") from exc | ||
| if not isinstance(value, str) or len(value) != 40: | ||
| raise AuditError("corruption", "canonical state has an invalid main identity") | ||
| return value |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Uncaught AttributeError breaks the fail-closed contract for malformed STATE.json.
If the canonical state's top-level JSON isn't a dict (e.g. corrupted into a list, string, or null), state.get("event") raises AttributeError, which isn't in the caught exception tuple. This crashes the audit with an unhandled traceback instead of emitting the bounded "corruption" diagnostic the whole tool is designed to produce — precisely for the kind of state corruption this audit exists to detect. It also bypasses main()'s except AuditError handler, so no diagnostic JSON is ever written for this case.
🛡️ Proposed fix
def _state_main_sha(state_root: Path) -> str:
try:
state = json.loads((state_root / ".agent-loop/STATE.json").read_text("utf-8"))
+ if not isinstance(state, dict):
+ raise TypeError("canonical state is not a JSON object")
event = state.get("event")
value = event.get("main_sha") if isinstance(event, dict) else state["source"]["main_sha"]
except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError) as exc:
raise AuditError("corruption", "canonical state has no readable main identity") from exc📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _state_main_sha(state_root: Path) -> str: | |
| try: | |
| state = json.loads((state_root / ".agent-loop/STATE.json").read_text("utf-8")) | |
| event = state.get("event") | |
| value = event.get("main_sha") if isinstance(event, dict) else state["source"]["main_sha"] | |
| except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError) as exc: | |
| raise AuditError("corruption", "canonical state has no readable main identity") from exc | |
| if not isinstance(value, str) or len(value) != 40: | |
| raise AuditError("corruption", "canonical state has an invalid main identity") | |
| return value | |
| def _state_main_sha(state_root: Path) -> str: | |
| try: | |
| state = json.loads((state_root / ".agent-loop/STATE.json").read_text("utf-8")) | |
| if not isinstance(state, dict): | |
| raise TypeError("canonical state is not a JSON object") | |
| event = state.get("event") | |
| value = event.get("main_sha") if isinstance(event, dict) else state["source"]["main_sha"] | |
| except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError) as exc: | |
| raise AuditError("corruption", "canonical state has no readable main identity") from exc | |
| if not isinstance(value, str) or len(value) != 40: | |
| raise AuditError("corruption", "canonical state has an invalid main identity") | |
| return value |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/audit_loop_memory_drift.py` around lines 63 - 72, Update
_state_main_sha to validate that the parsed STATE.json value is a dictionary
before calling state.get or indexing its source field. Treat any non-dictionary
top-level JSON value as corruption by raising AuditError with the existing
bounded diagnostic, while preserving the current main_sha validation for valid
dictionary states.
|
Closed by owner-directed simplification. Signed-state drift auditing is paused while the engineering loop is reduced to a simple, rebuildable, non-blocking model. |
PR Trust Bundle: WS-ENG-008-02
Chunk
WS-ENG-008-02— Scheduled Signed-State Drift AuditMerge intent:
.agent-loop/merge-intents/WS-ENG-008-02.jsonGoal
Detect later signed loop-memory custody or semantic drift independently, without
granting the audit repair, signing, dispatch, publication, or write authority.
Human-approved intent
The signed contract is
.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md.What changed and why
semantic, ledger, projection, ancestry, and active-contract validation.
WS-ENG-008-03.Design chosen
The workflow captures immutable branch tips through the read-only GitHub API,
checks out exact main and state commits with pinned actions and non-persisted
credentials, runs canonical validators, then rechecks both tips. It never imports
or invokes reducer, signer, event, recovery, repair, or publication commands.
Alternatives rejected
workflow_dispatch: callers can select feature-ref workflow code.Scope control and product behavior
Eight authorized repository-assurance files changed. Backend, frontend, API,
database, authorization grants, payments, artifacts, product review decisions,
coverage thresholds, start/cancel authority, signing, and branch protection are
unchanged.
Acceptance criteria proof
active contracts: canonical validators plus live audit pass.
shallow-history fixtures pass.
WS-ENG-008-03with explicit start.Tests/checks run
Fourteen focused tests and 105 Agent Gate regressions passed. Machine scope,
merge intent, Ruff, compilation, Markdown links, stale wording/authorization/
artifact scans, live signed-state audit, and diff checks passed.
Test delta and CI integrity
No test was removed, skipped, deselected, or weakened. No dependency, package
script, coverage floor, existing workflow, permission, or required check was
weakened. New actions are commit-pinned and both checkouts disable credential
persistence.
Reviewer results
Reviewed code SHA:
9c04beb154ef316307ef3f3896d006ccd87f6e8aAll nine required internal tracks passed: senior engineering, QA/test,
security/auth, product/ops, architecture, CI integrity, docs, reuse/dedup, and
test delta. Earlier High/Medium findings about feature-ref dispatch,
unauthenticated private-repository reads, preflight diagnostics, and concrete
contract fixtures were repaired and re-reviewed.
External review
CodeRabbit and hosted GitHub checks remain pending until publication. They
supplement, rather than replace, the completed internal review.
Remaining risks and follow-up work
Scheduled execution still depends on GitHub Actions and GitHub API availability.
Failures remain visible and non-mutating.
WS-ENG-008-03is only the declaredsame-initiative successor and requires a separate explicit signed start.
Human review focus
repository_dispatchdefault-branch trust boundary.Human merge ownership
The user owns approval and merge of the exact PR. Merge automation will stop
ENG-008 and will not start
WS-ENG-008-03automatically.Summary by CodeRabbit
New Features
Documentation
Tests