Recon deny-list + DB-error sanitization for the SQL guard#120
Open
sandeep-agami wants to merge 2 commits into
Open
Recon deny-list + DB-error sanitization for the SQL guard#120sandeep-agami wants to merge 2 commits into
sandeep-agami wants to merge 2 commits into
Conversation
sandeep-agami
force-pushed
the
ACE-038-resource-limits-timeout
branch
from
July 13, 2026 00:27
0319246 to
bd84345
Compare
sandeep-agami
force-pushed
the
ACE-039-recon-error-hardening
branch
from
July 13, 2026 01:20
c63692f to
27cbff4
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new “recon / metadata” safety gate to the SQL guardrail pipeline and prevents operational DB error text from leaking across the assistant boundary by sanitizing refusals while storing raw details server-side in the guardrail audit trail.
Changes:
- Introduces
sql_guard.check_no_reconand wires it at the executor chokepoint and tool-layer precheck to refuse catalog/introspection/fingerprinting queries asrecon. - Sanitizes DB operational failures into value-free refusal messages via
_classify_db_error, while capturing raw stderr/messages into a newguardrail_audit.error_detailcolumn (with jsonl fallback). - Extends guardrail refusal vocabulary and expands/updates test corpora to pin both security and false-positive behavior.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/test_sql_guard.py | Adds recon deny-list corpus (must-refuse + must-pass) and executor e2e recon refusal assertion. |
| tests/test_guardrail.py | Updates refusal-kind drift test to include new recon and refined operational kinds. |
| tests/test_guardrail_audit.py | Verifies raw operational error text is stored only in audit (DB/jsonl) and never in the response envelope; adds pre-013 degrade test. |
| tests/test_execute_sql_envelope.py | Replaces exit-code-only behavior with stderr classification + sanitization assertions across multiple dialect wordings. |
| tests/test_contracts.py | Updates GuardrailAuditRecord roundtrip sample to include error_detail. |
| tests/test_ah012_executor_seam.py | Ensures injected in-process executor path also sanitizes operational errors (no raw detail in refusal reason). |
| plugins/agami/lib/sql_guard.py | Vendors check_no_recon and recon regex sets into plugin library copy. |
| plugins/agami/lib/guardrail.py | Updates documented REFUSAL_KINDS to include new operational kinds. |
| plugins/agami/lib/execute_sql.py | Wires recon gate into executor guarded pipeline (plugin library copy). |
| packages/agami-core/src/tools.py | Adds recon precheck, implements _classify_db_error, threads error_detail to audit only, and sanitizes in-process path parity. |
| packages/agami-core/src/store.py | Adds Store.rollback() to support retrying inserts after Postgres transaction aborts. |
| packages/agami-core/src/sql_guard.py | Implements core check_no_recon deny-list and regex matching over neutralized SQL. |
| packages/agami-core/src/model_store.py | Adds error_detail audit insert with fallback for pre-013 schemas (with rollback). |
| packages/agami-core/src/guardrail.py | Extends REFUSAL_KINDS with network, column_not_found, table_not_found. |
| packages/agami-core/src/execute_sql.py | Wires recon gate into core executor guarded pipeline. |
| packages/agami-core/src/contracts.py | Extends GuardrailAuditRecord contract with error_detail. |
| migrations/core/013_guardrail_audit_error_detail.sql | Adds guardrail_audit.error_detail column for server-side raw driver text retention. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+377
to
+381
| except Exception: | ||
| # Pre-013 schema (the error_detail column isn't there yet, e.g. new code against an | ||
| # un-migrated DB): DON'T drop the audit row — write it without the raw detail. Roll back | ||
| # first so a Postgres aborted-transaction doesn't fail the retry. | ||
| self._store.rollback() |
sandeep-agami
force-pushed
the
ACE-038-resource-limits-timeout
branch
from
July 13, 2026 02:09
bd84345 to
17fe585
Compare
Two hardening gates at the shared execution chokepoint (execute_guarded), so both the subprocess wire and the in-process handler inherit them identically. 1. Recon / metadata deny-list. sql_guard.check_no_recon refuses server-fingerprinting and system-catalog introspection (version(), current_user, information_schema, pg_* relations) as a distinct `recon` refusal — wired right after the read-only guard, a hard gate NOT bypassable via --no-safety. 2. DB-error sanitization. A driver failure can echo schema / column / value names (or a DSN) in its message. Both surfaces now classify an operational failure into a VALUE-FREE refusal (_classify_db_error → fixed _ERROR_KINDS reason + remediation) and route the RAW driver text to the server-side audit trail ONLY, via a new guardrail_audit.error_detail column (migration 013). The subprocess path does this in _refusal_from_stderr; the in-process path now does the same in _run_in_process (returns (Refusal, error_detail)) — closing the leak the AH-012 seam had surfaced by riding exc.msg on the reason. Rebased onto the reconciled ACE-038 seam: the recon gate raises the typed GuardRefused; the two AH-012 seam tests that asserted the old raw-message-in-reason behavior now assert the sanitized, value-free contract (parity with the subprocess wire). Spec: ACE-039 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3 case only Copilot review: DbActivitySink.record_guardrail_audit retried the INSERT on ANY exception, then re-ran it without error_detail. A first-INSERT failure for a NON-schema reason (a deadlock, a constraint violation, a dropped connection) would spuriously trigger the fallback — masking the real DB error AND silently dropping error_detail. Narrow the retry to the missing-error_detail-column signature only (sqlite "no column", Postgres "does not exist" / UndefinedColumn); any other failure re-raises to the best-effort caller (which now warns once, per the ACE-035 review fix). Test pins that a non-schema error propagates on the first attempt — one execute call, no rollback, original error re-raised — while the existing pre-013 degrade test still passes. Spec: ACE-039 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sandeep-agami
force-pushed
the
ACE-039-recon-error-hardening
branch
from
July 13, 2026 02:13
27cbff4 to
733028f
Compare
5 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
Closes two information-leak channels that remain open even under read-only + object-scope:
Recon / metadata deny-list — a new
sql_guard.check_no_recongate refuses queries that fingerprint the server (version(),current_user,inet_server_addr()), enumerate or dump the system catalog (information_schema,pg_catalog, anypg_*relation — includingpg_statssampled VALUES andpg_authidpassword hashes), read catalog DDL (pg_get_viewdef/pg_get_functiondef), or probe object existence (has_table_privilege,to_regclass). The read-only role can't revoke catalog access on most engines, so this is the intended app-side backstop. A distinctreconrefusal, wired at the single executor chokepoint (execute_sql.py::main) + a fail-fast tool-layer pre-check, so both transports inherit it.DB-error sanitization — a raw driver error returned to the caller leaks schema, column names, or data values across the assistant boundary. The tool layer now classifies an operational failure into a value-free message (rich
kindmirroringdb_error_classifier.md), and the raw detail goes only to a newguardrail_audit.error_detailaudit column, keyed byaudit_id.Changes
sql_guard.py:check_no_recon+ data-driven recon sets, regex over the same neutralized SQL ascheck_read_only(no second parser).pg_is a reserved relation/function prefix (mirrors the Redshiftstl_/…prefixes); qualified columns (t.pg_class) pass via a(?<!\.)lookbehind. Fails closed when the SQL can't be neutralized.execute_sql.py/tools.py: wire the recon gate;_classify_db_error+_refusal_from_stderrreturns(Refusal, error_detail); the raw stderr is threaded to the audit row only.guardrail.py: addnetwork/column_not_found/table_not_foundtoREFUSAL_KINDS(+ the drift test now enumerates every emitted kind).model_store.py/store.py/ migration013: theerror_detailcolumn + a graceful INSERT degrade (survive a pre-013 schema instead of dropping the audit row) +Store.rollback.Verification
pg_leak closures + neutralization bypasses (comment/case/quoted-identifier/set-op/subquery); a false-positive corpus pins the legit look-alikes (bareversion/user/schemacolumns,t.pg_class, tokens in literals/comments, window fns/CTEs) with the accepted residuals pinned explicitly.error_detail, absent from the response).pg_stats/pg_authid/pg_get_viewdef/has_table_privilege/inet_server_addr()→recon; a qualifiedo.pg_classcolumn returns data).Review
Ran the Agami review panel (correctness · silent-failure · test-quality · High-lane security). Error-sanitization sign-off satisfied; the panel found real primary-engine recon false-negatives (the
pg_catalog relations + introspection functions) and a pre-013 audit-row-drop — all fixed here, with the corpus expanded to prove them closed.Checklist
Spec: ACE-039execute_sql/tool_execute_sqlchokepointplugins/agami/libin sync (dev.py sync-lib; drift-checked)Stacked on #115; retarget to
mainonce #111 / #112 / #115 merge.Spec: ACE-039
🤖 Generated with Claude Code