ACE-035: shared guardrail contract — Verdict / policy / Envelope#111
Open
sandeep-agami wants to merge 2 commits into
Open
ACE-035: shared guardrail contract — Verdict / policy / Envelope#111sandeep-agami wants to merge 2 commits into
sandeep-agami wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a shared, stdlib-only “guardrail contract” (Verdict → policy() → Envelope) and refactors SQL execution guardrails plus the execute_sql tool wire format (stdio + HTTP MCP) to consistently return a single response shape. It also adds an audit trail written at the shared tool chokepoint to ensure both transports are covered.
Changes:
- Adds
guardrail.py(vendored stdlib-only) definingVerdict,Refusal,Envelope, andpolicy(), and updates safety gates to returnVerdict | None. - Refactors
execute_sql/tool_execute_sqlto emit/assemble the unifiedEnvelope(status=ok|refused,refusal,data,audit_id, etc.) and updates tests accordingly. - Adds
guardrail_auditpersistence (migration + sink method) and tests validating best-effort audit writes (DB + jsonl fallback).
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_table_scope_gate.py | Updates assertions for table-scope gate to the new `Verdict |
| tests/test_sql_guard.py | Migrates check_read_only tests from reason strings to Verdict.detail and updates refusal envelope expectations. |
| tests/test_ports.py | Updates governance seam expectations to list[Verdict] (OSS default empty list). |
| tests/test_plugin_lib_resolution.py | Ensures guardrail.py is included in vendored plugin lib set. |
| tests/test_mcp_harness.py | Updates MCP harness assertions to the Envelope refusal shape. |
| tests/test_guardrail.py | Adds contract tests for Verdict/Refusal/Envelope serialization and policy() behavior. |
| tests/test_guardrail_audit.py | Adds tests for guardrail_audit migration + best-effort write behavior. |
| tests/test_guard_context.py | Updates guard-context parity tests to compare `Verdict |
| tests/test_execute_sql_envelope.py | Adds focused tests for executor stderr refusal parsing into a refused Envelope. |
| tests/test_contracts.py | Removes retired ErrorResult coverage and adds GuardrailAuditRecord roundtrip tests. |
| tests/test_column_scope_gate.py | Updates column-scope and SELECT * gate tests to the new `Verdict |
| tests/test_column_scope_adversarial.py | Updates adversarial scope tests to match the `Verdict |
| tests/test_ace051_fail_closed.py | Updates fail-closed tests to assert {"refusal": ...} instead of {"error": ...}. |
| tests/test_ace044_bounded_fetch.py | Updates bounded-fetch assertions to Envelope.data.truncated and Envelope.applied. |
| tests/e2e/test_safety_envelope.py | Adds cross-transport E2E test ensuring both stdio and HTTP return the same Envelope shape. |
| plugins/agami/lib/sql_guard.py | Refactors read-only guard to return a safety Verdict wrapper over _read_only_reason(). |
| plugins/agami/lib/guardrail.py | Adds vendored guardrail contract module for plugin distribution. |
| plugins/agami/lib/execute_sql.py | Emits {"refusal": ...} to stderr and maps safety gate verdicts to refusal lines. |
| packages/agami-core/src/tools.py | Assembles/returns Envelope, parses executor refusal lines, and records guardrail audit at a shared chokepoint. |
| packages/agami-core/src/sql_guard.py | Refactors read-only guard to return `Verdict |
| packages/agami-core/src/semantic_model/runtime.py | Updates safety gates to return `Verdict |
| packages/agami-core/src/ports.py | Retires GovernanceVerdict and re-types governance seam to list[Verdict]. |
| packages/agami-core/src/oss_adapters.py | Updates OSS governance adapter to return an empty list[Verdict]. |
| packages/agami-core/src/model_store.py | Adds DB sink method record_guardrail_audit(...). |
| packages/agami-core/src/guardrail.py | Adds core guardrail contract module (vendored stdlib-only). |
| packages/agami-core/src/execute_sql.py | Emits {"refusal": ...} to stderr and maps safety gate verdicts to refusal lines (core executor). |
| packages/agami-core/src/contracts.py | Retires ad-hoc execute_sql error contract and adds GuardrailAuditRecord. |
| packages/agami-core/pyproject.toml | Adds guardrail to flat py-modules. |
| migrations/core/012_guardrail_audit.sql | Adds guardrail_audit table + index. |
| dev.py | Updates vendored-lib drift guard to include guardrail.py. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+100
to
+101
| OSS default = warn-only, and returns no findings ("basic governance warning"); enforcement is | ||
| a paid tier that supplies its own adapter.""" |
Comment on lines
+73
to
+76
| class WarnOnlyGovernancePolicy: | ||
| """Default ``GovernancePolicy`` — never blocks (``allowed=True``); any warnings are advisory | ||
| ("basic governance warning"). Enforcement is a paid tier.""" | ||
| """Default ``GovernancePolicy`` — emits no governance findings (an empty ``Verdict`` list), | ||
| so nothing is warned, rewritten, or blocked. Enforcement is a paid tier that supplies its own | ||
| adapter returning governance-class ``Verdict``s.""" |
Comment on lines
+107
to
+110
| """The ``refusal`` block of a refused ``Envelope``: why the query was rejected, with a | ||
| remediation. Keep raw SQL and raw DB error text out of ``reason`` — it crosses the | ||
| boundary between the model and the customer's database, and error-text sanitization is | ||
| hardened separately.""" |
Comment on lines
+107
to
+110
| """The ``refusal`` block of a refused ``Envelope``: why the query was rejected, with a | ||
| remediation. Keep raw SQL and raw DB error text out of ``reason`` — it crosses the | ||
| boundary between the model and the customer's database, and error-text sanitization is | ||
| hardened separately.""" |
Collaborator
Author
|
Addressed the Copilot review (all four were docstring-accuracy comments, no behavior change) in the latest commit:
Full gate green after the change. |
5 tasks
ashwin-agami
approved these changes
Jul 12, 2026
This was referenced Jul 12, 2026
The one typed result shape for the SQL guardrails — Verdict, policy(verdict, tier),
and the response Envelope (ok | refused) with a nested ExecuteSqlResult payload +
audit_id. The safety gates (check_read_only + the runtime scope/PII checks) return a
Verdict; execute_sql returns one Envelope on every surface; a guardrail_audit row is
recorded at the single chokepoint. The governance plug-point is kept (re-typed to
Verdict); the data-protection / governance policy branches are stubbed for F10/F11.
Reconciled onto the execute_guarded executor seam (AH-012) + the in-process default
(ACE-028) that merged to main while this was in review:
- execute_guarded returns the typed Envelope and raises GuardRefused carrying a typed
Refusal (replacing the placeholder {"error": ...} dict); _model_safety RETURNS its
Refusal instead of writing it to stderr.
- So the in-process path (tools._run_in_process) gets the SAME typed refusal the
subprocess path does — closing the follow-up main deferred ("_model_safety to return
its envelope"): a model-safety refusal keeps its structured detail on BOTH surfaces
instead of degrading to a generic string in-process.
- One success shape too: _finalize_execution builds Envelope.ok(data=...) for both the
subprocess and in-process paths; subprocess main() emits {"refusal": ...} to stderr
from the typed refusal (the byte-identical wire the tool relays).
Full gate green (1525 tests); the seam / fail-closed / envelope / audit tests updated to
the unified typed contract, plus a conftest reset for the process-global injected
executor so a seam test can't leak into a subprocess-path test.
Spec: ACE-035
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sandeep-agami
force-pushed
the
ACE-035-shared-guardrail-contract
branch
from
July 12, 2026 17:39
bd445a0 to
3950f08
Compare
Comment on lines
1001
to
+1004
| truncated = True | ||
| return columns, data_rows, truncated | ||
| def _refusal_from_stderr(stderr: str | None, returncode: int) -> Refusal: | ||
| """Build a Refusal from the executor's stderr. A guardrail refusal is a `{"refusal": {...}}` |
Comment on lines
1071
to
1075
| @@ -1015,20 +1074,24 @@ | |||
| envelope is identical either way (a guard refusal's envelope is not yet identical across paths — | |||
| see `_finalize_execution` and the tracked structured-refusal-parity follow-up). | |||
Review-response to the Agami panel + Copilot on the reconciled guardrail contract. No behavior change to the guard decision; observability, docstring accuracy, naming, and test precision. - Audit observability (panel should-fix): the guardrail-audit sinks were best-effort AND silent, so a persistently dead sink (a DB role without INSERT, an unwritable log dir) lost the security record undetectably. Add a one-time (per-process, per-key) value-free stderr warning in _finish (record-build failure) and _record_guardrail_audit (DB-sink error + jsonl-fallback failure); the query stays best-effort. Conftest resets the dedup set per test. - Refusal docstring (Copilot): state the value-free-reason contract for BOTH guardrail and operational refusals (raw driver text goes to the audit trail only), instead of the stale "operational failures carry raw text" note. - Governance naming (Copilot): rename WarnOnlyGovernancePolicy -> NoopGovernancePolicy (the OSS default emits no findings; the name now matches), fix the contradictory ports/module docstrings, and move the annotation-only Verdict import under TYPE_CHECKING. - tool_execute_sql docstring (Copilot): both paths now build the same typed refused Envelope; drop the stale "not yet identical across paths" note. Add the E305 blank lines before _refusal_from_stderr. - _resolve_guard_model (panel): a store.close() error no longer discards a model that loaded fine (false refusal); the loader import moved inside the disk-path try so an import failure fails closed. - REFUSAL_KINDS (panel): document that unscopable_sql/resource_limit/recon are forward-declared for the dependent slices, so the emit-site test's emitted-subset-of-documented direction is intended. Tests: real PII gate -> refused Envelope (kind sensitive_columns, end-to-end, executor never runs); HTTP refusal writes a guardrail_audit row (audit-on-both-surfaces, read back from the table); column-scope adversarial tests assert Verdict.rule == "column_scope" (gate identity); audit sink failure warns exactly once (both jsonl + DB branches), value-free. Spec: ACE-035 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces one shared result shape for the SQL-execution guardrails and refactors the safety gates + the
execute_sqlresult wire onto it. Every gate now produces aVerdict; onepolicy(verdict, tier)maps a verdict to an action by class + deployment tier; and every surface (stdio + HTTP MCP) returns oneEnvelope. The ad-hoc gate return shapes and the hand-rolled error dicts are deleted, not wrapped. This is the root of the safe-SQL-execution work — the data-protection and governance gates build to these same types in their own changes.Changes
guardrail.py(stdlib-only, vendored):Verdict(a gate's finding),Refusal,Envelope(the one result shape), andpolicy(verdict, tier) -> action. The safety branch is fully implemented (reject in every tier; fail-closed on uncertainty); the data-protection and governance branches are documented stubs for later work.Verdict:sql_guard.check_read_only+runtime.{check_table_scope, check_no_select_star, check_column_scope}now returnVerdict | None(None = allow); the three ad-hoc*Resultdataclasses are deleted.Envelopeon both surfaces: the executor emits a{"refusal": …}line;tools.tool_execute_sqlassembles theEnvelope— a safety violation →status=refused+refusal{kind,reason,remediation}+ no data; a clean query →status=ok+data(the existingExecuteSqlResultpayload) +audit_id.ErrorResult/ToolErrorfolded intoRefusal.guardrail_audittable (migration 012); every ok/refused result writes one row keyed byaudit_idat the shared chokepoint (so both surfaces are covered), best-effort.GovernanceVerdictresult type is retired; theGovernancePolicyinjection seam is kept, re-typed to returnlist[Verdict](a no-op default for later governance work).Design decisions
Verdict) rather than deleting it — later governance work injects through it.Envelopeis the single top-level result; the well-modeledExecuteSqlResultis reused as itsdatapayload (not flattened). Pre-release, so no compatibility shims.Refusal.kindis an openstrdocumented byREFUSAL_KINDS— operational failures (syntax/auth/driver_missing) come from the DB driver, not a fixed guardrail vocabulary — with a test pinning the emit sites against the documented set.Test plan
uv run dev.py check— 1494 tests, ruff, format, gitleaks, and vendored-lib drift.test_guardrail.py(types + policy),test_guardrail_audit.py(audit trail),test_execute_sql_envelope.py(the executor→tool refusal relay), andtests/e2e/test_safety_envelope.py(one Envelope over the real stdio + HTTP transports).Checklist
Envelopeand are tested on bothexecute_sqlenforcement point is preservedSpec: ACE-035
🤖 Generated with Claude Code