Security model

A governance tool that overstates what it enforces is worse than no governance tool, because people trust it. This page is the honest account.

The one guarantee

An AI agent cannot complete a governed action on its own.

Everything else here supports that sentence. It holds because approval lives in a separate process that the model has no way to address: there is no approve tool on the MCP surface, no approved: true input field, and no auto-approval on timeout. The test suite pins the exact set of generated tool names, so a future change cannot quietly add one; validate separately rejects colliding tool names and reserved action parameter names such as approver and proposal_id.

What serve enforces

ControlHow
RoleChecked before the warehouse is touched; asserted once at startup, never read from tool input
PreconditionEvaluated against live data, in the same query that fetches the record — and again at execution
ApprovalA proposal is stored; only the CLI can approve it
Sensitive propertiesNever selected, so the values never leave the database
Read limitsClamped server-side, ordered, truncation reported
AuditAppend-only, intent recorded before the effect fires
Effect targetURL rebuilt from the template each time; identity values percent-encoded; scheme, host and port re-checked after substitution; redirects never followed
Double executionA single conditional UPDATE claims the proposal; exactly one caller can win

What it does not enforce

Warehouse access. Anyone with database credentials can query your tables directly. Ontologiq governs the path through it — the path you give to agents — not the database. If you need enforcement at the data layer, use your warehouse’s own row filters and column masks; compiling policy down into them is on the roadmap.

Caller identity. MCP over stdio has no authentication primitive. The role is asserted by whoever launched the process, from --role or ONTOLOGIQ_ROLE. The server says so in its startup banner and records the role, its source and the OS user in every audit row. Treat a serving process as running as one role, and give each role its own process.

Handler effects. A type: handler effect is your Python, imported into the serving process, running with its privileges — including its database connection. The “actions never write SQL” guarantee is enforced for webhooks; for handlers it is a convention you are choosing to keep. validate warns on every handler effect for exactly this reason.

Audit immutability against an operator. The append-only log is enforced by SQLite triggers. That stops accidental code. It does not stop someone with write access to the file. Ship the audit somewhere append-only if you need more.

Deliberate design choices

Governance is read from the compiled models, never from target/. That directory is generated and gitignored: if policy were read from catalog.json, editing one untracked file would silently rewrite roles and approval gates with no diff in git. serve compiles in process.

A lost response is unknown, never failed. If the request left and no response came back, whether the effect applied is genuinely unknown. Reporting failure invites a second refund, so that case ends the attempt and says so rather than retrying.

Retries are therefore narrow: a connect error, which provably sent nothing, and a 5xx, which is the endpoint asking to be asked again — up to three attempts, all carrying the same Idempotency-Key, which is what makes the 5xx case safe. A 4xx or a redirect ends the attempt immediately.

Proposals are pinned to their arguments and their ontology. The arguments a human approved are hashed on the proposal and re-verified before execution, so the executed call cannot differ from the reviewed one. Approving across a change to the ontology requires --force.

Expiry is a deadline, not a hint. Proposals expire (24h by default) and an expired proposal can never be approved.

Threats considered

ThreatResponse
SQL injection through model-supplied valuesValues only ever become literals, never structure; strings go through sqlglot’s escaping, numbers through a strict pattern. Object, relation and state names are resolved against the ontology, never formatted into SQL.
Effect URL retargeting (../../admin/purge, ?force=true, @evil.com)Identity values percent-encoded; scheme, host and port asserted unchanged after substitution; credentials in the host refused; redirects not followed.
Secret leakageEnv-interpolated URLs are resolved at the moment of use and never stored. Audit and tool results carry the ontology template and the host, never the resolved URL. Raw exceptions never reach the model.
Prompt injection via warehouse dataControl characters, ANSI escapes, Unicode tag and bidi characters stripped; fields capped. Structurally: injected instructions still cannot approve anything.
Transport corruptionstdout carries only JSON-RPC; every diagnostic and any driver output is redirected to stderr.
Forged approval queue entriesValues are rendered as JSON in approvals list, so a newline in a model-supplied parameter cannot fabricate a second entry on the reviewer’s screen.

Reporting a vulnerability

See SECURITY.md. Please do not open a public issue for something exploitable.