Sandboxes & promote
Point an AI coding agent at your production database safely — captured, observed, promoted through a gate, and reversible.
Part of Agent-Safe Change Control. The
keon sandboxandkeon ledgercommands and the console Sandboxes view described here are live — a recent keon is required (cli-v0.1.46+).
Kisenon lets you hand a coding agent (Claude Code, Cursor, your own) a database it can change freely — without risking production. A sandbox is a per-run fork of your branch with a scoped credential, a live action log, and a server-side promote step. This page covers the full loop: capture → work → promote → land (reversibly) → prove.
Why it's safe
Two guarantees, both deterministic — no LLM sits in the trust path:
- The agent can't touch production. It connects with a scoped,
non-superuser credential to a fork. It never holds a credential that can
write
main. Promotion runs server-side and only applies changes that already passed in the sandbox. - You see exactly what it did. Every statement is attributed and streamed to a read-only console — the real SQL, not a summary.
The loop
keon sandbox run \
--migrate "alembic upgrade head" \
--verify "pytest tests/db"
# → green/red verdict + schema diff + a sandbox you can inspect
keon sandbox promote <id> # cp applies the validated changes to mainThe agent stays in control of the loop; the bound is what makes it safe.
Durable capture
The action log is the source of truth for promote, so it has to be complete.
Each sandbox carries a capture_state — ok or lost. If capture is ever
compromised the state flips to lost and stays there: a lost sandbox
can't promote (409 sandbox_capture_lost) and never silently applies a
partial change. There is no auto-heal — discard it and re-run the work in a
fresh sandbox. keon sandbox get / list show the capture health.
Promote: self-serve or human-approved
Each project chooses a promote_mode:
| Mode | Who commits to main |
|---|---|
self (default) | The agent promotes once its checks pass — within its bounds. |
human | The agent proposes; an owner/admin reviews the diff + action log and clicks Approve. |
Blast-radius dry-run
Before you promote, you can ask the platform to measure what the promote would do — real lock levels, real lock-hold durations, real row counts — instead of guessing from the statement text:
keon sandbox dry-run <id>The platform replays the sandbox's exact statement set against two short-lived
throwaway forks of the parent — one at the parent's current HEAD (where locks,
durations, and rows are measured) and one at the sandbox's fork point (the
divergence baseline) — then destroys both. It never touches main, and the
agent never receives a credential to either fork. The dry-run is advisory:
it produces the report, it never blocks a promote.
It also answers the question a static diff can't: did reality move under the
agent? Any statement that errors, or affects a different number of rows, at
parent HEAD than it did at the fork point is surfaced as a conflict — e.g.
an UPDATE ... WHERE id = 2 whose target row was deleted on main after the
fork.
The completed report carries:
| Field | Meaning |
|---|---|
statements[].lock_level | Strongest lock the statement newly takes in the promote transaction (e.g. AccessExclusiveLock for a table rewrite). |
statements[].lock_wait_est_ms | Measured execution time on the head fork — the floor of how long the lock is held on main. |
statements[].rows_measured | Rows affected on the head fork (0 for DDL). |
statements[].divergence | {kind:"error"} or {kind:"row_count"} when the statement behaves differently at HEAD than at fork time. |
rollup.conflicts | Every divergence, plus a baseline_unavailable marker if the baseline lane couldn't run — so a gate on "no conflicts" fails closed on a degraded measurement. |
rollup.max_lock_level / rollup.duration_ms_total | Aggregate lock strength and total replay time. |
capture_advanced / parent_head_lsn | Staleness signals: the report is a snapshot, and re-running is cheap. |
The endpoints are POST /v1/sandboxes/{id}/dry-run (starts an async run, 202)
and GET /v1/sandboxes/{id}/dry-run (the latest record). A failed dry-run
carries a machine-readable error_code
(capture_fence_failed, incomplete_capture, fork_failed,
compute_unreachable, replay_infra_failed, timeout). The Blast radius
panel on the sandbox detail page renders the same report in the console.
Undo a promote
A promote is reversible. Before cp applies the validated statements to
main, it anchors the parent branch's exact pre-promote state (an LSN). One
command rolls the branch back to that anchor:
keon sandbox undo <id> # restore the parent branch to its pre-promote stateThe parent's connection string is unchanged — clients reconnect to the same
endpoint. Undo is an owner/admin action; agent-capability keys are refused
(403 scope_insufficient), because an undo discards whatever landed on the
branch after the anchor.
keon sandbox get <id> carries an undo object — undoable, restored_lsn,
undone_at, and a blocked_reason when it can't run
(superseded_by_later_promote, already_undone). Undo targets the most
recent promote only, and rejects with 409 (sandbox_not_promoted,
undo_anchor_missing, undo_anchor_invalidated) once the anchor is gone.
It stays available while the anchor is valid and within your storage
retention — there is no separate countdown.
Undo restores by LSN: it rolls back everything after the anchor, not just the promoted change — including direct writes and later agent sessions on that branch. See Common pitfalls.
Emitted migrations
Every landed promote emits a deterministic up/down SQL migration — no LLM, generated from the captured schema change and round-trip validated on a throwaway fork before it is offered. Fetch it:
keon sandbox migration <id> --out ./migrations --waitThe artifact carries the up and down SQL (files[], each with a role), a
coverage flag (full or partial), down_fidelity, any uncovered_seqs,
a validation result, and a sha256. Terminal states are validated,
validation_failed, emission_failed, and no_schema_changes. v1 emits
sql; GET /v1/sandboxes/{id}/migration (and .../migration/files/{role})
serve it. Emission runs after the promote lands and never blocks or reverts
it — a data-only promote just reports no_schema_changes.
Signed ledger
Every promote and undo appends a signed, hash-chained record to a per-project ledger — an offline-verifiable attestation of what changed. Verification needs no network and does not trust Kisenon:
keon ledger list --project <id> # the chain (reports chain_ok)
keon ledger export <id> --out att.json # one attestation document
keon ledger verify att.json # offline; exit 0 iff valid
keon ledger keys --save # pin the signing keys you trustkeon ledger verify checks the Ed25519 signatures and the hash chain and
reports precise failures (signature_invalid, statement_chain_mismatch,
seq_gap, key_untrusted, chain_broken, …); trust is tri-state
(trusted / untrusted / unverified). Routes: GET /v1/sandboxes/{id}/ledger, GET /v1/projects/{projectId}/ledger, GET /v1/ledger/keys.
If cp has no signing key configured, promotes still succeed but are unsigned (
ledger_enabled: false) — signing isn't unconditional.
What a sandbox is not
- Not a code sandbox — it scopes your database, not the agent's process.
- Not an LLM judge — capture, replay, and review are byte-deterministic.
Try it
Try it at kisenon.com and see the quickstart.