Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
# The module-load import closure of the 4 scripts (all stdlib-only). tests/test_plugin_lib_resolution.py
# fails if a script gains a module-level import that's missing here; a new *lazy* import from the package
# would also need adding to this list.
_VENDORED = ["agami_paths.py", "execute_sql.py", "sql_guard.py", "semantic_model/__init__.py", "semantic_model/units.py"]
_VENDORED = ["agami_paths.py", "execute_sql.py", "sql_guard.py", "guardrail.py", "semantic_model/__init__.py", "semantic_model/units.py"]


def run(cmd: list[str], *, allow_fail: bool = False) -> int:
Expand Down
23 changes: 23 additions & 0 deletions migrations/core/012_guardrail_audit.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- The **guardrail audit trail** — one row per execute_sql call, keyed by the response Envelope's
-- `audit_id`. Written best-effort at the shared executor chokepoint (`tools.tool_execute_sql`) so
-- every ok/refused result is recorded on BOTH surfaces (stdio + HTTP), not only the HTTP path that
-- writes `tool_calls`. This is the trail the Envelope's `audit_id` points at.
--
-- Deliberately thin for now — status + refusal kind + the query context; a richer verdict/action
-- list is a follow-on. Portable CREATE TABLE (runs on SQLite + Postgres unchanged).

CREATE TABLE guardrail_audit (
audit_id TEXT PRIMARY KEY, -- == the response Envelope's audit_id
ts TEXT NOT NULL, -- UTC ISO8601
datasource TEXT,
status TEXT NOT NULL, -- 'ok' | 'refused'
refusal_kind TEXT, -- the refusal kind when status = 'refused'
sql TEXT, -- the query the caller sent (may be NULL on a bad request)
row_count INTEGER, -- on an ok result
execution_ms INTEGER,
correlation_id TEXT, -- the turn (one user question), self-reported; may be NULL
source TEXT -- 'mcp_server'
);

-- The admin views read newest-first; index the time access path.
CREATE INDEX idx_guardrail_audit_ts ON guardrail_audit (ts);
2 changes: 1 addition & 1 deletion packages/agami-core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ server = [
package-dir = { "" = "src" }
# Flat top-level modules (not under a parent package) — listed explicitly so the import
# names stay flat regardless of disk layout.
py-modules = ["agami_paths", "async_offload", "execute_sql", "sql_guard", "mcp_harness", "mcp_http", "tools", "ports", "contracts", "oss_adapters", "store", "model_store", "model_deploy", "deploy_preflight", "oauth_server", "oidc", "passwords", "user_store", "admin", "ui", "onboarding"]
py-modules = ["agami_paths", "async_offload", "execute_sql", "sql_guard", "guardrail", "mcp_harness", "mcp_http", "tools", "ports", "contracts", "oss_adapters", "store", "model_store", "model_deploy", "deploy_preflight", "oauth_server", "oidc", "passwords", "user_store", "admin", "ui", "onboarding"]
packages = ["semantic_model"]

[tool.setuptools.package-data]
Expand Down
31 changes: 19 additions & 12 deletions packages/agami-core/src/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,12 @@ class ExecuteSqlRequest(_Contract):


# ---------------------------------------------------------------------------
# Errors (every tool may return {"error": {"kind", "remediation"}, ...})
# Errors: execute_sql returns the shared guardrail Envelope (status / refusal — see
# `guardrail.py`); the other tools return a plain `{"error": {kind, remediation}}` body. Neither
# needs a pydantic type here.
# ---------------------------------------------------------------------------


class ToolError(_Contract):
kind: str
remediation: str


class ErrorResult(_Contract):
error: ToolError
sql: str | None = None
execution_ms: int | None = None


# ---------------------------------------------------------------------------
# list_datasources
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -210,3 +201,19 @@ class ToolCallRecord(_Contract):
agent_query: str | None = None
thread_id: str | None = None
correlation_id: str | None = None # the turn: one user question -> N agent sub-queries


class GuardrailAuditRecord(_Contract):
"""The verdict trail for one execute_sql call, keyed by the response Envelope's `audit_id`.
Written best-effort at the shared executor chokepoint on every surface (see `guardrail_audit`)."""

audit_id: str
ts: str
status: str # 'ok' | 'refused'
datasource: str | None = None
refusal_kind: str | None = None
sql: str | None = None
row_count: int | None = None
execution_ms: int | None = None
correlation_id: str | None = None
source: str | None = None
Loading
Loading