---
title: "The safety envelope"
summary: "How Onebox makes execution resumable and reversible: application and host locks, fencing epochs, append-only journals, state-bound plans, and what each protects against."
description: "Locks, fencing, journals, drift, expiry: the machinery that makes an interrupted operation recoverable."
status: shipped
read_when:
  - "Understanding what happens when a deploy is interrupted"
  - "Evaluating whether Onebox is safe to run from CI or an agent"
  - "Working out why a plan expired or a state binding went stale"
---
Onebox is agentless: no resident daemon and no listening control port runs on the
server. Everything below is achieved with short-lived processes, durable state,
and files on the host.

## The pieces

**Locks.** An application lock, and a host lock above it. Operations that would
interfere are serialized.

**Fencing epochs.** A stale process that wakes up after losing its lock cannot
act: its epoch no longer matches. This prevents a stale process from acting after the CLI loses its connection.

The epoch is durable authority, not a disposable counter. Onebox treats only a
genuinely absent epoch file as a first run. If the file exists but is unreadable,
empty, malformed, negative, or too large to increment, the operation stops
before publishing a new lock or fence. The next value is written to a sibling
temporary file and atomically renamed into place, so an interrupted write can
leave debris but cannot truncate the last valid epoch.

Application operations share one epoch beneath the application's state
directory. Backup mutations add a per-service epoch beneath
`backup/locks/`. Both use the same read and persistence contract; retrying the
same operation identity therefore produces a different fence from the runner it
replaces.

**Append-only journals.** Every operation records ordered steps with deterministic
identities. The record of a failed operation survives the operation that replaced
it, which is what lets `ob resume` and `ob abort` choose only paths the journal
can prove safe.

**State-bound plans.** A plan binds the exact config, Compose, host state, images
and payloads it was created against. It expires after fifteen minutes, and any
drift or local payload change requires a new one.

**Digest-bound approvals.** A grant covers one exact plan, server, input set,
risk classification, operator and expiry. A changed or expired plan needs a new
grant.

**Drift detection.** A server-side artifact differing from what the plan bound is
a typed error **before** mutation, not a silent reconciliation.

The observed set covers the current release and the long-running workloads it
replaces: their image, replica revisions and health. Job containers are excluded
by design. A job starts, finishes, or is killed on its own schedule, so its
presence at any instant says nothing about whether the release still matches the
plan, and comparing it would turn ordinary job churn into a refusal of a plan
that is still valid.

## Why plans expire

An approval says: *this exact change, against this exact observed state, is
authorised*. The moment the observed state may have moved, the approval is
describing something that no longer exists.

The fifteen-minute expiry limits how long an approval can refer to an earlier
observation. After expiry, the operator must review a new plan.

## Retry is safe by construction

Mutations are idempotent under retry. Retrying with the same operation identity
resumes or returns the existing terminal result without creating an untracked
duplicate.

That property is why the CLI is usable from CI and from an agent: a lost
connection can be retried safely. A retry first inspects the
operation identity and uses resume or status behaviour; it never blindly starts a
second deploy.

During a roll, Onebox retires a replica only after discovery has published its
replacement, and stops it only after the proxy has dropped it. This assumes
Traefik loads published routes within the 3s converge buffer. Recovery restores
the previous release before removing the interrupted release's serving containers.

Traefik restarts when its configuration changes and is recreated when its own
image or ports change. Otherwise, `ob` upgrades and discovery repair do neither.
The deploy plan's `proxy:` line says which action it will take.

After upgrading to this release, the first deploy or `ob proxy apply` restarts
Traefik once. The first `ob service apply` recreates each managed service once,
and the first deploy does so for PostgreSQL services with declared extensions.

## What the envelope does not cover

> **Bounded claims**
>
> See the [bounded safety guarantees](/docs/reference/policies#safety-claims-are-bounded).

- **A customer with root can bypass Onebox.** The envelope constrains Onebox's
  own operations, not the machine.
- **A compromised host can lie about its own evidence.** Everything here assumes
  the host reports accurately about itself.
- **Rolling deployment cannot make a failed host available.** It avoids
  interruption while the box is healthy.
- **Arbitrary external side effects are not reversible.** An email sent by a
  migration stays sent.

## The safety boundary of `ob exec`

```sh
ob exec --reason "<why>" <target> -- <command>
```

`--reason` is required. An escape hatch whose use cannot be explained afterwards
is indistinguishable from an intrusion, so the reason is part of the record
before the command runs.

Exec takes the application lock
and writes the mutation fence before it resolves a container, so it cannot run
inside a workload a concurrent deploy is replacing, and it holds that lock for
the life of the stream. It records the operator, the target and its kind, the
exact container, the reason, a digest of the command, and the outcome.

Onebox makes no claim about the result. Nothing exec changes belongs to a
release, so nothing it changes survives the next deploy in any tracked sense,
and nothing about it is reversible by rollback. Every other command states up
front whether it contacts the server and whether it changes anything; `ob exec`
is the one row without a definite answer.

The command text itself never enters the durable record; only its digest does,
because an operator pasting a one-off command is not thinking about whether it
contains a credential.

## Reading the envelope from outside

```sh
ob status --output json   # non-zero on divergence
ob audit                  # recorded operations
ob doctor --output json   # local safety setup and runner compatibility
```

None of these mutate. Observation cannot converge; see
[Evidence, not declaration](/docs/concepts/evidence-not-declaration).