feat: governance gate — benchmark scores enforce execution policy#65
feat: governance gate — benchmark scores enforce execution policy#65djimit wants to merge 1 commit into
Conversation
GovernanceGateService is consulted in ExecutionEngine.executeTask after the policy evaluation: when the executing agent/model's latest OpenMythos score is below GOVERNANCE_GATE_FLOOR (default 3/5), an 'allow' decision is tightened to require_approval, with the benchmark evidence captured (source 'governance-gate') and the reason surfaced to the approver. The gate never loosens policy outcomes. Three consecutive below-floor runs with a declining trend mark the agent as a retirement candidate in the verdict evidence. Default-off (GOVERNANCE_GATE_ENABLED). Evidence lookup: the task's agent_id, then nightly:<model> via GOVERNANCE_GATE_MODEL_MAP, then metadata subject_model. No evidence -> allow: the gate acts only on measured behavior. Also trues up the test approvals/audit_events schemas to production — the approval branch of executeTask had never been exercisable in unit tests because the test tables were missing columns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Kilo Code Review could not run — your account is out of credits. Add credits or switch to a free model to enable reviews on this change. |
There was a problem hiding this comment.
Code Review
This pull request introduces the GovernanceGateService to tighten task execution policy decisions to require approval when an agent's benchmark score falls below a specified floor. It also updates the database schema for audit events and approvals, adds corresponding unit tests, and registers a new evidence source. Feedback on these changes suggests optimizing database queries in GovernanceGateService by filtering directly in SQL and avoiding redundant calls, decoupling evidence capturing from decision tightening so that benchmark evidence is always recorded, and refining the retirement candidate logic to verify a true persistent decline across three runs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private recentRuns(agentId: string | null, subjectModel: string | null, limit = 3): RunRow[] { | ||
| const rows = this.db.prepare(` | ||
| SELECT agent_id, overall_score, finished_at, metadata | ||
| FROM openmythos_eval_runs | ||
| WHERE status = 'completed' | ||
| ORDER BY finished_at DESC | ||
| `).all() as RunRow[]; | ||
|
|
||
| const matches = rows.filter((row) => { | ||
| if (agentId && row.agent_id === agentId) return true; | ||
| if (subjectModel) { | ||
| if (row.agent_id === `nightly:${subjectModel}`) return true; | ||
| try { | ||
| const metadata = JSON.parse(row.metadata || '{}') as { subject_model?: string }; | ||
| if (metadata.subject_model === subjectModel) return true; | ||
| } catch { /* skip malformed */ } | ||
| } | ||
| return false; | ||
| }); | ||
| return matches.slice(0, limit); | ||
| } |
There was a problem hiding this comment.
The recentRuns method currently queries all rows from the openmythos_eval_runs table where status = 'completed' and filters them in-memory. If the database grows large, this will cause significant performance degradation and high memory usage on every task execution.
We should optimize this by filtering directly in the SQL query using the agent_id index or narrowing down the rows using a LIKE filter before parsing JSON metadata.
private recentRuns(agentId: string | null, subjectModel: string | null, limit = 3): RunRow[] {
if (agentId) {
return this.db.prepare(
"SELECT agent_id, overall_score, finished_at, metadata " +
"FROM openmythos_eval_runs " +
"WHERE status = 'completed' AND agent_id = ? " +
"ORDER BY finished_at DESC " +
"LIMIT ?"
).all(agentId, limit) as RunRow[];
}
if (subjectModel) {
const rows = this.db.prepare(
"SELECT agent_id, overall_score, finished_at, metadata " +
"FROM openmythos_eval_runs " +
"WHERE status = 'completed' " +
"AND (agent_id = ? OR metadata LIKE ?) " +
"ORDER BY finished_at DESC"
).all("nightly:" + subjectModel, "%" + subjectModel + "%") as RunRow[];
const matches = rows.filter((row) => {
if (row.agent_id === "nightly:" + subjectModel) return true;
try {
const metadata = JSON.parse(row.metadata || "{}") as { subject_model?: string };
if (metadata.subject_model === subjectModel) return true;
} catch { /* skip malformed */ }
return false;
});
return matches.slice(0, limit);
}
return [];
}| if (gateVerdict.action === 'require_approval' && evaluation.decision === 'allow') { | ||
| evaluation = { ...evaluation, decision: 'require_approval', explanation: gateVerdict.reason }; | ||
| this.evidenceService.captureEvidence({ | ||
| task_id: taskId, | ||
| evidence_type: EvidenceType.POLICY_DECISION, | ||
| severity: EvidenceSeverity.WARNING, | ||
| title: 'Governance gate tightened execution to require approval', | ||
| summary: gateVerdict.reason, | ||
| details: { | ||
| agentKey: gateVerdict.agentKey, | ||
| score: gateVerdict.score, | ||
| floor: gateVerdict.floor, | ||
| trend: gateVerdict.trend, | ||
| retirement_candidate: gateVerdict.flagRetirement, | ||
| executorKind, | ||
| }, | ||
| source: 'governance-gate', | ||
| }); | ||
| } |
There was a problem hiding this comment.
The governance gate evidence is currently only captured if evaluation.decision === 'allow'. If the policy decision is already 'require_approval' (or 'deny'), the governance gate check is skipped entirely, and no evidence is captured.
This means the operator/approver will not see the benchmark evidence or the retirement candidate flag when reviewing the task, even though the model is below the floor.
We should decouple the decision tightening from the evidence capturing so that governance gate evidence is always captured when the model is below the floor, regardless of the initial policy decision.
if (gateVerdict.action === 'require_approval') {
if (evaluation.decision === 'allow') {
evaluation = { ...evaluation, decision: 'require_approval', explanation: gateVerdict.reason };
}
this.evidenceService.captureEvidence({
task_id: taskId,
evidence_type: EvidenceType.POLICY_DECISION,
severity: EvidenceSeverity.WARNING,
title: 'Governance gate tightened execution to require approval',
summary: gateVerdict.reason,
details: {
agentKey: gateVerdict.agentKey,
score: gateVerdict.score,
floor: gateVerdict.floor,
trend: gateVerdict.trend,
retirement_candidate: gateVerdict.flagRetirement,
executorKind,
},
source: 'governance-gate',
});
}| const runs = task.agent_id | ||
| ? this.recentRuns(task.agent_id, null).length > 0 | ||
| ? this.recentRuns(task.agent_id, null) | ||
| : this.recentRuns(null, subjectModel) | ||
| : this.recentRuns(null, subjectModel); |
There was a problem hiding this comment.
When task.agent_id is provided, recentRuns is called twice (once to check if the length is greater than 0, and once to retrieve the runs). This results in redundant database queries. We can optimize this by fetching the runs once and reusing the result.
const agentRuns = task.agent_id ? this.recentRuns(task.agent_id, null) : [];
const runs = agentRuns.length > 0 ? agentRuns : this.recentRuns(null, subjectModel);| }; | ||
| } | ||
|
|
||
| const flagRetirement = runs.length >= 3 && runs.every((run) => run.overall_score < floor) && trend === 'declining'; |
There was a problem hiding this comment.
The flagRetirement logic currently checks if trend === 'declining'. However, trend is only calculated by comparing the two most recent runs (runs[0] and runs[1]). If the middle run (runs[1]) was a temporary peak but the oldest run (runs[2]) was actually lower, this is not a persistent decline across all three runs.
To ensure a true persistent decline, we should verify that the scores are strictly decreasing across all three runs (e.g., runs[0].overall_score < runs[1].overall_score && runs[1].overall_score < runs[2].overall_score).
const flagRetirement = runs.length >= 3 &&
runs.every((run) => run.overall_score < floor) &&
runs[0].overall_score < runs[1].overall_score &&
runs[1].overall_score < runs[2].overall_score;There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1553498698
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| this.persistRiskAssessment(taskId, assessment, `${parsedTask.title}: ${parsedTask.description}`); | ||
|
|
||
| // Governance gate: benchmark evidence can only TIGHTEN the policy decision. | ||
| const gateVerdict = this.governanceGate.assess(parsedTask, executorKind); |
There was a problem hiding this comment.
Re-run the gate after selecting a fallback executor
When the requested executor's circuit is open, executeTask later reassigns executorKind to a fallback before starting the session. Because the governance gate is evaluated here against the original executor, a request that passes/no-evidence for opencode can fail over to claude/codex/gemini whose mapped benchmark score is below the floor and still execute without approval. This bypasses the new enforcement during circuit-breaker failover; move or repeat the gate after any fallback executor is selected.
Useful? React with 👍 / 👎.
| if (row.agent_id === `nightly:${subjectModel}`) return true; | ||
| try { | ||
| const metadata = JSON.parse(row.metadata || '{}') as { subject_model?: string }; | ||
| if (metadata.subject_model === subjectModel) return true; |
There was a problem hiding this comment.
Preserve nightly evidence precedence over metadata fallback
When both a canonical nightly:<model> run and another completed run with matching metadata.subject_model exist, this combined predicate lets the newest row win regardless of source. That violates the documented lookup order above (nightly:<model> before metadata fallback) and can require or skip approval based on an ad-hoc validation run instead of the nightly score. Query the nightly key first and only scan subject_model metadata when no nightly evidence exists.
Useful? React with 👍 / 👎.
Summary
Pillar 1 of Closed-Loop Governance: certification with teeth. The nightly OpenMythos scores stop being a dashboard number and become a live security control in the task-execution path.
GovernanceGateService, consulted inExecutionEngine.executeTaskafter the policy evaluation. When the executing agent/model's latest benchmark score is belowGOVERNANCE_GATE_FLOOR(default 3/5), anallowdecision is tightened torequire_approval— the existing approval machinery (WS pause event, audit record, approval queue) handles the rest, and the approver sees the benchmark evidence as the reason.deny/require_approvaloutcomes stand regardless of score.source: governance-gate) — auto-invoking the retirement workflow is the documented upgrade once operators trust the signal.agent_id→nightly:<model>viaGOVERNANCE_GATE_MODEL_MAP(maps executor kinds to benchmarked models) → run-metadatasubject_model. No evidence → allow: the gate acts only on measured behavior.GOVERNANCE_GATE_ENABLED=trueto arm).Also fixed
The unit-test
approvals/audit_eventsschemas had drifted from production — the approval branch ofexecuteTaskwas untestable before this PR (inserts failed on missing columns). Trued both up to the real schema, which is what let the integration tests below exist.Validation
allowintoawaiting_approvalwith governance-gate evidence; an above-floor model executes untouchedEvidenceSourceunion extended with'governance-gate'(shared package, additive)Pairs with #61 (nightly scores feed the gate) and #62 (routing) — the enforcement leg of the same loop.
🤖 Generated with Claude Code