Menu

Situation · Database deletes

Stop an AI agent from deleting a production database — refused before the statement runs

An agent with a database connection can delete a table the moment it decides to. Read-only credentials prevent that and also prevent the work you wanted. The alternative is to put destructive calls behind a decision made at the call, and keep a record of what was refused.

Answers the question: “How do I stop an AI agent from deleting my production database?”

  • Also: “How do I require approval before an AI agent runs DROP TABLE?”
  • Also: “How do I stop an AI agent running DELETE without a WHERE clause?”
  • Also: “Can I prove an AI agent's destructive query was blocked?”
Unregistered
database.delete · hosted API
Offline
Check a signed refusal, no account
Untouched
Rows read back after each refusal
Tier
The offline policy decides on tier, not SQL

A connection that can write can drop

Database permissions decide which statements a credential may run. They do not decide whether this statement, against this table, should run now. An agent that is allowed to update rows is often allowed to delete them too.

A DELETE with no WHERE: one statement removes every row the credential can reach.

A schema change run as cleanup: DROP TABLE or a destructive migration issued in a working session.

Approval of a tool, not a statement: a person allows “database access” and the agent issues a statement nobody read. The offline boundary holds the call, but the approver still has to read the statement.

No record of what was checked: afterward there is a query log, not evidence a third party can verify.

Check it, scan for it, then pick the id

The first two steps need only a laptop. The third explains why the database path is offline today.

1 · Check the refusals yourself, offline — no account, no network

git clone https://github.com/Tarshann/solo-builder-core
cd solo-builder-core && pip install -e .
solo strix-wire verify docs/evidence/database-refused-v1/.strix/evidence/receipts.jsonl --state-dir docs/evidence/database-refused-v1/.strix

Signed local receipts for real SQL against a throwaway in-memory SQLite database seeded with synthetic rows: a DELETE with no WHERE held for approval, a DROP TABLE held for approval, the same DROP TABLE denied by a configured rule even with approval, and the same DELETErunning once approval is granted. After each run the database is read back, so “the rows were not touched” is observed, not inferred. The key is local to the machine that minted them: this shows the mechanism, not Strix-operated custody.

2 · Find the destructive calls in your own code

solo govern scan --format text

The scanner reports each call it recognizes and, where its pattern can tell, marks whether it is scoped (has a WHERE), to rank which call to wrap first. It finds calls that match its patterns; a statement built at runtime, or a call made outside the code it reads, is not covered.

3 · Which id to use depends on where the call is checked

IdWhere it is usedHosted APINote
database.deleteScanner and offline policyRefused as unknownWhat the scanner emits for a DELETE and the offline policy classifies. Not a hosted id.
database.migrateScanner and offline policyRefused as unknownWhat the scanner emits for DROP and schema changes. Not a hosted id.

An id the hosted API refuses as unknown fails closed, but it is refused at the contract check: no decision exists and so no signed record does. Today the database path is the offline one.

What the offline boundary does, and what it does not

These describe the local boundary and its committed evidence. They are not hosted guarantees.

CapabilityHow Strix delivers it
Refused before the statement runsThe wrapped operation is only called after the boundary admits it. In the evidence package the call counter stays at zero on every refusal, and the table and its rows are read back unchanged.
Held and denied are different recordsA call held pending approval is recorded as pending, not as a denial. A configured deny wins over a granted approval. The default rules hold destructive calls for approval and deny nothing outright.
Signed and tamper-evidentEach refusal is signed and hash-chained. Editing a receipt to claim the statement ran fails verification. The key is local to the machine that minted the receipts.
Not a reading of the SQLThe offline policy decides on the action's risk tier. A DELETE scoped to one row and a DELETE of every row get the same decision. A test in solo-builder-core pins this; if the policy starts reading statements, that test fails.
Hosted kerneldatabase.delete is not a registered capability on the hosted API. The hosted kernel does register delete capabilities for the console's own records, among them the ids ending in _delete: contact_delete (HIGH, 2 approvers; production CRITICAL, 3); onboarding_workspace_delete (CRITICAL, 3 approvers; production CRITICAL, 3); organization_delete (HIGH, 2 approvers; production CRITICAL, 3); sales_pipeline_deal_delete (CRITICAL, 3 approvers; production CRITICAL, 3); sales_pipeline_shell_delete (CRITICAL, 3 approvers; production CRITICAL, 3); vault_item_delete (HIGH, 2 approvers; production CRITICAL, 3); vertical_delete (HIGH, 2 approvers; production CRITICAL, 3). Those govern Strix's console, not your database, and this page does not report whether any of them has executed in production.
Where it standsA destructive SQL statement wrapped by the offline boundary was refused before it ran, the refusal was signed by a local key, and the rows were observed untouched. The offline policy decides on the action's tier, not on the SQL. The hosted kernel has no generic database capability, so this is not a hosted or production record, and it does not protect a database the boundary does not wrap.

Proof, not promises

Every claim Strix makes is backed by an artifact you can independently verify.

Offline refusal package

Signed local receipts for a held DELETE, a held DROP TABLE, a denied DROP TABLE and an approved DELETE, with the database read back after each. Verifies with the private key deleted.

https://github.com/Tarshann/solo-builder-core/tree/main/docs/evidence/database-refused-v1

What the policy does not read

A test that a scoped and an unscoped DELETE get the same decision. If the policy starts reading statements, it fails by name.

https://github.com/Tarshann/solo-builder-core/blob/main/tests/test_database_refused_evidence.py

Binding to the exact request

How the hosted kernel binds approval to one canonicalized payload. Shown on refunds; there is no hosted database capability for it to bind to yet.

/situations/bind-approval-to-exact-request

Frequently asked questions

How do I stop an AI agent from deleting my production database?+

Two layers. Give the agent a credential that cannot reach production tables it has no reason to change. Then wrap the destructive calls it can make so each one is decided at the call: held for a person, denied by rule, or allowed, with a signed record either way. The offline boundary in solo-builder-core does the second today, under a local key, and decides on the action's risk tier rather than reading the SQL. It covers the calls you wrap, not every path to the database.

Does Strix read my SQL?+

Not to decide. The offline policy decides on the action's risk tier, so it cannot tell a one-row DELETE from a DELETE of the whole table. The scanner can often tell them apart, but only to rank which call to wrap first.

Can I use this against the hosted API?+

Not for your database today. database.delete is refused as an unknown action on the hosted API, so no decision and no signed record is produced. Adding a hosted database capability is a policy change with a version bump and an approval, and it has not been made.

What does the signed record prove?+

That the boundary refused the call before it ran, and that the record has not been edited since. It is signed by a key on the machine that produced it, so it does not prove Strix-operated custody, a trusted timestamp, or that the machine owner could not replace the runtime.

Does this protect a real database?+

The evidence uses a throwaway in-memory database to show the mechanism. Protecting yours means wrapping your own calls with the boundary. A call that does not go through it is not covered; the scanner and coverage report exist to find those.

Does this replace backups, roles and point-in-time recovery?+

No. Those limit and repair damage. This decides whether one specific destructive call should run now, and keeps a record of the answer.

Start with the refusal you can check yourself.

Verify the offline receipts, scan your own code, then bring one destructive call you cannot yet allow.