Skip to content

Commit eea7ce9

Browse files
Pigbibicodex
andcommitted
feat: harden autonomy and review gates
Co-Authored-By: Codex <noreply@openai.com>
1 parent 05c3932 commit eea7ce9

13 files changed

Lines changed: 648 additions & 43 deletions

cloudflare/ai-gateway-dash/src/index.mjs

Lines changed: 12 additions & 4 deletions
Large diffs are not rendered by default.

docs/architecture.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,37 @@ POST /v1/ai/review
115115
→ return {results, consensus, recommended_action}
116116
```
117117

118+
## Autonomy and Merge Safety
119+
120+
AiGateway is designed as a supervised autopilot, not as an unrestricted
121+
self-merging agent. AI systems may diagnose failures, propose patches, create
122+
PRs, and collect evidence. Deterministic policy, CI, and repository protection
123+
own the final merge decision.
124+
125+
Default autonomy boundaries:
126+
127+
| Risk | Allowed default action | Human audit requirement |
128+
|---|---|---|
129+
| Low (`docs/`, `tests/`, README) | `auto_merge` after policy + CI pass | Not required unless CI/review flags an issue |
130+
| Medium (`scripts/`, report helpers) | `auto_pr` | Required before merge unless explicitly allowlisted by repo policy |
131+
| High (source, strategy, app logic) | `auto_pr` only | Required before merge |
132+
| Critical (secrets, credentials, policy/permission expansion) | `escalate` | Always required |
133+
134+
Review and gate failures are risk-aware:
135+
136+
- Low-risk docs/tests changes may pass through when AI review infrastructure is
137+
unavailable, so routine maintenance is not blocked by the reviewer service.
138+
- Medium/high/critical changes fail closed or receive a human-review decision
139+
when AI review cannot complete.
140+
- Auto-merge policy, GitHub Actions, secrets, OIDC, deployment, and permission
141+
changes must remain human-audited because they can expand the system's own
142+
authority.
143+
144+
Closed-loop feedback records every autonomous change with action, risk,
145+
confidence, changed paths, and before/after metrics when available. Degraded
146+
outcomes set a rollback-review intent for auditability; the service does not
147+
directly roll back production state.
148+
118149
## Security
119150

120151
- **Authentication**: GitHub Actions OIDC JWT with allowlisted repos, workflows,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Autonomy Health Auto-Merge Hardening Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Make AIAuditBridge safer for mostly-AI-operated monitoring, remediation PR creation, and guarded auto-merge while keeping high-risk changes auditable by humans.
6+
7+
**Architecture:** Keep the current layered model: AI proposes/fixes, deterministic policy gates decide autonomy, CI and GitHub branch protections own final merge, health/feedback observe outcomes. This plan avoids a rewrite and tightens risk boundaries, persistence, and dashboard explainability.
8+
9+
**Tech Stack:** Python stdlib service/scripts, GitHub Actions, pytest, ruff, actionlint, Cloudflare Worker dashboard.
10+
11+
---
12+
13+
## File/Module Map
14+
15+
- `service/autonomy.py`: single autonomy decision envelope, safer default matrix, policy loading/normalization helpers.
16+
- `.github/codex_auto_merge_policy.json`: repo-level default policy and review-failure behavior.
17+
- `scripts/run_monthly_codex_audit.py`: guarded auto-merge classification alignment and feedback registration metadata.
18+
- `scripts/run_codex_pr_review.py`: risk-aware fail-open/fail-closed behavior.
19+
- `scripts/gate_codex_app_review.py`: risk-aware Codex App timeout behavior.
20+
- `service/feedback.py`: durable effectiveness records and rollback-issue intent metadata.
21+
- `service/health.py`: keep online health separate from background job latency; expose decision-friendly health reasons.
22+
- Dashboard worker/static files: add Autonomy Decisions, Pending Human Audit, Effectiveness presentation if this repo owns dashboard UI; otherwise document target repo handoff.
23+
- `tests/`: cover autonomy matrix, policy alignment, review-failure behavior, feedback persistence, health status.
24+
- `docs/architecture.md` / README: document tiers and human-audit boundary.
25+
26+
---
27+
28+
### Task 1: Policy Inventory and Branch Protection Baseline
29+
30+
**Files:**
31+
- Modify: `docs/superpowers/plans/2026-07-04-autonomy-health-auto-merge-hardening.md`
32+
- No production code change.
33+
34+
- [ ] Confirm current PR/check/protection state for `QuantStrategyLab/AIAuditBridge`.
35+
- [ ] Record whether branch protection/rulesets are missing.
36+
- [ ] Produce exact branch protection recommendation; do not mutate GitHub settings without explicit user confirmation if tool requires admin-side irreversible policy change.
37+
38+
Commands:
39+
```bash
40+
gh pr view 11 --json mergeStateStatus,statusCheckRollup,reviewDecision
41+
gh api repos/QuantStrategyLab/AIAuditBridge/branches/main/protection || true
42+
gh api repos/QuantStrategyLab/AIAuditBridge/rulesets || true
43+
```
44+
45+
Expected:
46+
- PR checks green or pending identified.
47+
- Current protection gaps documented.
48+
49+
---
50+
51+
### Task 2: Safer Autonomy Policy Defaults
52+
53+
**Files:**
54+
- Modify: `service/autonomy.py`
55+
- Modify: `.github/codex_auto_merge_policy.json`
56+
- Test: `tests/test_autonomy.py` or extend existing tests if present.
57+
58+
- [ ] Add tests proving high/critical risk never returns `auto_merge` by default.
59+
- [ ] Add tests proving low-risk high-confidence can return `auto_merge`.
60+
- [ ] Add tests proving medium-risk defaults to `auto_pr` or `escalate` unless explicit policy override says otherwise.
61+
- [ ] Adjust `DEFAULT_DECISION_MATRIX` to remove high-risk auto-merge.
62+
- [ ] Ensure critical/secrets always escalate.
63+
- [ ] Document reason strings accurately.
64+
65+
Validation:
66+
```bash
67+
python -m pytest tests/test_autonomy.py -q
68+
```
69+
70+
---
71+
72+
### Task 3: Risk-Aware Review Failure Behavior
73+
74+
**Files:**
75+
- Modify: `scripts/run_codex_pr_review.py`
76+
- Modify: `scripts/gate_codex_app_review.py`
77+
- Test: `tests/test_run_codex_pr_review.py`
78+
- Test: `tests/test_gate_codex_app_review.py`
79+
80+
- [ ] Add/adjust tests: docs/tests-only PR may pass if AI review unavailable.
81+
- [ ] Add/adjust tests: source/workflow/policy PR must not silently pass when AI review unavailable; should request human review or fail check depending current workflow contract.
82+
- [ ] Keep developer velocity for low-risk surfaces.
83+
- [ ] Avoid API/secret logging.
84+
85+
Validation:
86+
```bash
87+
python -m pytest tests/test_run_codex_pr_review.py tests/test_gate_codex_app_review.py -q
88+
```
89+
90+
---
91+
92+
### Task 4: Feedback Loop Durability and Action Envelope
93+
94+
**Files:**
95+
- Modify: `service/feedback.py`
96+
- Modify: `service/ai_gateway_service.py` if API response shape needs envelope fields.
97+
- Test: existing feedback tests or create `tests/test_feedback.py`.
98+
99+
- [ ] Make shadow disagreement persistence durable under the same job dir pattern, or document why not possible in this pass.
100+
- [ ] Ensure every registered autonomous change includes action, risk, confidence, changed paths, policy version, source PR/issue URL when available.
101+
- [ ] When effect is degraded, store rollback-required metadata or issue intent; do not directly roll back.
102+
- [ ] Expose aggregated stats for dashboard without secrets.
103+
104+
Validation:
105+
```bash
106+
python -m pytest tests/test_ai_gateway_service_get_routes.py tests/test_health.py -q
107+
```
108+
109+
---
110+
111+
### Task 5: Dashboard Information Architecture
112+
113+
**Files:**
114+
- Modify dashboard-owned files if present in this repo.
115+
- If dashboard UI is generated/deployed from another repo, create a handoff note in `docs/architecture.md` instead of guessing.
116+
117+
- [ ] Locate dashboard source ownership.
118+
- [ ] Add sections or data mapping for:
119+
- Autonomy Decisions
120+
- Pending Human Audit
121+
- Effectiveness / Recent Auto Actions
122+
- [ ] Keep mobile header distributed and card empty states centered.
123+
- [ ] Verify in browser if dashboard source is editable here.
124+
125+
Validation:
126+
```bash
127+
python -m pytest tests/test_ai_gateway_service_get_routes.py -q
128+
```
129+
Browser smoke test on dashboard URL if deploy is performed.
130+
131+
---
132+
133+
### Task 6: Final Validation and Delivery
134+
135+
**Files:**
136+
- No new production changes unless fixing validation failures.
137+
138+
- [ ] Run targeted tests.
139+
- [ ] Run full test suite if targeted tests pass.
140+
- [ ] Run lint/actionlint if available.
141+
- [ ] Inspect git diff for accidental secrets or unrelated changes.
142+
- [ ] Report changed files, validation results, risks, and any manual actions required.
143+
144+
Commands:
145+
```bash
146+
python -m pytest -q
147+
ruff check .
148+
actionlint
149+
python -m compileall service scripts
150+
```

scripts/gate_codex_app_review.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,40 @@ def check_metadata(files: list[dict[str, Any]], policy: dict[str, Any]) -> list[
6262
return _check_metadata(files, policy)
6363

6464

65+
def fetch_pr_files(token: str, repo: str, pr_number: int) -> list[dict[str, Any]]:
66+
files: list[dict[str, Any]] = []
67+
page = 1
68+
while True:
69+
payload = github_request(
70+
token,
71+
"GET",
72+
f"/repos/{repo}/pulls/{pr_number}/files?per_page=100&page={page}",
73+
)
74+
if not isinstance(payload, list) or not payload:
75+
break
76+
files.extend(payload)
77+
if len(payload) < 100:
78+
break
79+
page += 1
80+
return files
81+
82+
83+
def changed_files_are_low_risk(files: list[dict[str, Any]], policy: dict[str, Any]) -> bool:
84+
low = policy.get("risk_policy", {}).get("low", {})
85+
low_prefixes = tuple(low.get("prefixes", []))
86+
low_exact = set(low.get("exact", []))
87+
if not files:
88+
return False
89+
for file in files:
90+
path = str(file.get("filename", "")).strip().lstrip("./")
91+
if not path:
92+
return False
93+
if path in low_exact or any(path.startswith(prefix) for prefix in low_prefixes):
94+
continue
95+
return False
96+
return True
97+
98+
6599
def env(name: str, default: str = "") -> str:
66100
return os.environ.get(name, default).strip()
67101

@@ -180,6 +214,20 @@ def app_decision(review: dict[str, Any] | None) -> tuple[int, str, str]:
180214
f"Codex state `{state}` at {at}. Not blocking. [View review]({url})")
181215

182216

217+
def infra_failure_decision(files: list[dict[str, Any]], review_error: str) -> tuple[int, str, str]:
218+
if changed_files_are_low_risk(files, load_policy(POLICY_PATH)):
219+
return (
220+
0,
221+
"Codex: review infra unavailable — low-risk pass",
222+
f"Codex review could not complete ({review_error}). Low-risk docs/tests-only change passed through.",
223+
)
224+
return (
225+
1,
226+
"Codex: review infra unavailable — human review required",
227+
f"Codex review could not complete ({review_error}). Human review is required before merge.",
228+
)
229+
230+
183231
# ─── main ────────────────────────────────────────────────────────────────────
184232

185233
def main() -> int:
@@ -204,6 +252,7 @@ def main() -> int:
204252
return 0
205253

206254
print(f"PR #{pr_number} sha={head_sha[:12]} event={event_name}")
255+
policy = load_policy()
207256

208257
# ── Phase 1: Static guard (skip on review-only events) ────────────
209258
if event_name != "pull_request_review":
@@ -226,16 +275,26 @@ def main() -> int:
226275
return rc
227276

228277
# WAIT: poll for existing or upcoming review
278+
changed_files = fetch_pr_files(token, repo, pr_number)
279+
low_risk_change = changed_files_are_low_risk(changed_files, policy)
229280
try:
230281
existing = get_codex_review(token, repo, pr_number)
231-
except RuntimeError:
282+
except RuntimeError as exc:
232283
existing = None
284+
review_error = str(exc)
285+
else:
286+
review_error = ""
233287

234288
if existing is not None:
235289
rc, title, summary = app_decision(existing)
236290
print(f"EXISTING → exit={rc}: {title}")
237291
step_summary(f"## {title}\n\n{summary}")
238292
return rc
293+
if review_error and not low_risk_change:
294+
rc, title, summary = infra_failure_decision(changed_files, review_error)
295+
print(f"INFRA → exit={rc}: {title}")
296+
step_summary(f"## {title}\n\n{summary}")
297+
return rc
239298

240299
poll_s = env_int("CODEX_GATE_POLL_SECONDS", 30)
241300
max_w = env_int("CODEX_GATE_MAX_WAIT_MINUTES", 5)
@@ -246,7 +305,8 @@ def main() -> int:
246305
time.sleep(poll_s)
247306
try:
248307
review = get_codex_review(token, repo, pr_number)
249-
except RuntimeError:
308+
except RuntimeError as exc:
309+
review_error = str(exc)
250310
continue
251311
if review is not None:
252312
rc, title, summary = app_decision(review)
@@ -255,9 +315,15 @@ def main() -> int:
255315
return rc
256316

257317
# Timeout
258-
print(f"TIMEOUT → Codex did not respond in {max_w}min; passing through")
259-
step_summary(f"## Codex: timeout after {max_w}min\n\nPassed through to avoid blocking development.")
260-
return 0
318+
if low_risk_change:
319+
print(f"TIMEOUT → Codex did not respond in {max_w}min; passing through")
320+
step_summary(f"## Codex: timeout after {max_w}min\n\nPassed through to avoid blocking development.")
321+
return 0
322+
323+
rc, title, summary = infra_failure_decision(changed_files, review_error or f"timeout after {max_w}min")
324+
print(f"TIMEOUT → {title}")
325+
step_summary(f"## {title}\n\n{summary}")
326+
return rc
261327

262328

263329
if __name__ == "__main__":

scripts/run_codex_pr_review.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ def classify_file_risk(
193193
return ("high", "source code change")
194194

195195

196+
def changed_files_are_low_risk(paths: list[str], policy: dict[str, Any]) -> bool:
197+
"""Return True when every changed path is low-risk under the policy."""
198+
return bool(paths) and all(classify_file_risk(path, policy)[0] == TASK_COMPLEXITY_LOW for path in paths)
199+
200+
196201
# ---------------------------------------------------------------------------
197202
# PR diff fetching
198203
# ---------------------------------------------------------------------------
@@ -901,9 +906,7 @@ def main() -> int:
901906
print(f"::warning::Policy errors: {policy['policy_errors']}")
902907

903908
# First pass: classify files. If all files are low-risk, skip review.
904-
all_low_risk = all(
905-
classify_file_risk(p, policy)[0] == "low" for p in changed_paths
906-
)
909+
all_low_risk = changed_files_are_low_risk(changed_paths, policy)
907910
if all_low_risk and changed_paths:
908911
print("All changed files are low-risk (docs/tests). Skipping Codex review.")
909912
decision = {
@@ -936,16 +939,27 @@ def main() -> int:
936939
)
937940
except ReviewError as exc:
938941
print(f"::error::Codex review failed: {exc}", file=sys.stderr)
939-
# Don't block on review failures — post a warning comment
942+
if all_low_risk:
943+
# Don't block low-risk docs/tests when the review infra is unavailable.
944+
warning_body = (
945+
"<!-- codex-pr-review -->\n"
946+
"## 🤖 Codex PR Review\n\n"
947+
"⚠️ **Review skipped**: The Codex review could not be completed.\n\n"
948+
f"```\n{exc}\n```\n"
949+
)
950+
upsert_pr_comment(token, repo, pr_number, warning_body)
951+
return 0
952+
953+
# High-risk changes should not fail open on review infrastructure errors.
940954
warning_body = (
941955
"<!-- codex-pr-review -->\n"
942956
"## 🤖 Codex PR Review\n\n"
943-
"⚠️ **Review skipped**: The Codex review could not be completed.\n\n"
957+
"⚠️ **Human review required**: The Codex review could not be completed.\n\n"
944958
f"```\n{exc}\n```\n\n"
945959
"Please ensure a human reviewer checks this PR before merging.\n"
946960
)
947961
upsert_pr_comment(token, repo, pr_number, warning_body)
948-
return 0 # Don't block on infrastructure failures
962+
return 1
949963

950964
print(f"Codex output: {len(output)} chars")
951965

service/ai_gateway_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,6 +947,8 @@ def _handle_feedback_evaluate(self, payload: dict[str, Any]) -> None:
947947
"status": "ok",
948948
"effect": record.effect,
949949
"effect_detail": record.effect_detail,
950+
"rollback_issue_required": record.rollback_issue_required,
951+
"rollback_intent": record.rollback_intent,
950952
})
951953
except FileNotFoundError:
952954
_json_response(self, HTTPStatus.NOT_FOUND, {"status": "error", "error": "change_id not found"})

0 commit comments

Comments
 (0)