kisenon
Agent-Safe Change Control

Common pitfalls

Anti-patterns that quietly defeat scale-to-zero and agent safety — and how to avoid them.

These patterns don't error. They pass CI, they look reasonable in a diff, and they quietly cancel a guarantee you're paying for — scale-to-zero savings or agent safety. Each one below: what happens, why it hurts, and the fix.

Keep-alive code defeats scale-to-zero

An agent "helpfully" adds code that touches the database on a timer. Every periodic query counts as activity and resets the suspend timer, so an endpoint you designed to scale to zero never sleeps — and idle stops being free.

It arrives in several shapes. A frontend or backend heartbeat:

// runs forever, holds the compute awake forever
setInterval(async () => {
  await pool.query("SELECT 1");   // "health check"
}, 30_000);

An external uptime monitor pointed at an endpoint whose handler happens to query the DB — so every probe wakes compute:

app.get("/health", async (_req, res) => {
  await pool.query("SELECT 1");   // monitored every 60s → never idle
  res.send("ok");
});

Or connection-pool settings that keep a live connection with validation queries — a min above zero, or a serverless "warmer":

const pool = new Pool({
  min: 2,                         // keeps ≥2 connections open + validated
  // a validation/keep-alive query on each idle connection resets the timer
});

Fix: audit agent-written diffs for periodic database access. Keep liveness checks at the HTTP layer — probe the app, not the DB — and let the pool drain to zero (min: 0). The console compute view shows why an endpoint isn't scaling to zero, which surfaces a stray heartbeat fast.

Handing the agent main credentials short-circuits ASCC

Giving an agent the main-branch connection string — or letting it run keon logged in as your own user — bypasses everything ASCC enforces: sandbox isolation, LEASH budgets, LINT and BLAST checks, promote gates, attribution, and undo anchors. The agent is now writing production directly, and none of the loop that makes agent changes safe ever runs.

The agent should never hold a credential that can write main. Promotion is server-side by design — cp is the only writer to production.

Fix: mint an agent-scoped API key (capability = agent, scope = a project, short expiry) at Settings → API keys, hand the agent sandbox connection strings only, and remove stronger credentials from its environment (no full DATABASE_URL, no ~/.pgpass, no logged-in keon session in the same shell). See Hand an agent a scoped credential and the SCOPE section of Guardrails.

Retry storms around wake-up

The first connection after a suspend has to wake compute, so it's slower than a warm one. An agent that sees a single slow connect may "fix" it — with aggressive client timeouts and retry loops that hammer the endpoint, or by adding a keep-alive (pitfall #1) so it never has to wake at all. Both trade the savings away to paper over a one-time cost.

Fix: set a client connect_timeout around 10s and be patient on the first connect. Cold start is sub-second same-region once warm paths are in play — one slightly-slow connect is expected, not a fault to engineer around.

postgresql://…?connect_timeout=10

Credentials committed to code

Agents hard-code connection strings and API keys into source and .env files that then reach git. Leaked keys get harvested within hours of hitting a public (or later-public) repo — the window is measured in hours, not days.

Fix: inject secrets via the environment, never commit them. Keep .env files out of git. On any leak, rotate immediately from Settings → API keys — revoke the exposed key and mint a fresh one.

Direct DDL on main bypasses the promote loop

Running a migration straight against main skips the capture that makes a change reversible and auditable. You lose the emitted up/down migration (EMIT), the signed ledger entry (LEDGER), and the undo anchor (UNDO). The schema changed, but there's no record the platform can replay or roll back.

Fix: sandbox → promote, even for a "trivial" schema change. The promote loop is what produces the migration, the ledger entry, and the anchor; a direct ALTER produces none of them.

Writes inside a rolled-back transaction still land

Capture records the statements the agent runs — it has no transaction-boundary awareness. Statements executed inside a BEGIN … ROLLBACK are still captured, and if that sandbox is promoted, they are replayed and applied. A ROLLBACK in the sandbox does not un-capture them.

BEGIN;
UPDATE users SET plan = 'pro';   -- captured
ROLLBACK;                        -- does NOT remove it from the capture

Fix: don't rely on ROLLBACK inside a sandbox to "undo" exploratory work. If you want work gone, discard the sandbox — that's the disposal path capture respects.

Parking work in a sandbox past its budget or TTL

Sandboxes are ephemeral. When a sandbox exhausts its LEASH budget it is auto-discarded (status=discarded), and any un-promoted work in it is lost. Treating a sandbox as a long-lived branch you'll "get back to" is how a day's changes vanish.

Fix: promote it, or re-capture the work in a fresh sandbox. Treat sandboxes as disposable by default — the durable artifact is the promote, not the sandbox.

Misreading the guarantees

Three specific traps in what the checks actually promise:

  • The blast-radius dry-run is advisory. It measures what a promote would do — real locks, durations, row counts — but it never gates the promote. A clean dry-run is information, not a green light; and whether a dry-run is required before promote is POLICY's decision, not the dry-run's.

  • Approve can't override a policy block. A human Approve can never wave through a policy block or an out-of-lane statement. The only way past a block is an audited policy change — there is no manual override switch, by design.

  • A masked fork masks values, not shape. On a masked fork the values are masked, so data-dependent DML the agent writes against those masked values won't match the real rows on the parent at replay. Schema-lane work is value-independent and replays faithfully; value-dependent DML developed against masked data does not.