kisenon
Agent-Safe Change Control

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 sandbox and keon ledger commands 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 main

The 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_stateok 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:

ModeWho commits to main
self (default)The agent promotes once its checks pass — within its bounds.
humanThe 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:

FieldMeaning
statements[].lock_levelStrongest lock the statement newly takes in the promote transaction (e.g. AccessExclusiveLock for a table rewrite).
statements[].lock_wait_est_msMeasured execution time on the head fork — the floor of how long the lock is held on main.
statements[].rows_measuredRows 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.conflictsEvery 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_totalAggregate lock strength and total replay time.
capture_advanced / parent_head_lsnStaleness 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 state

The 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 --wait

The 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 trust

keon 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.