Saphan StudioDocs
Configuration

Seats: binding an executor profile to a run

What a seat is, the only way to bind one to a run, and why a status read is not proof.

A seat is a row in the machine registry: (identity, machine, slot) plus a config-dir (for claude-code/codex seats) or a lane (for openai-compat/ agent-loop seats) — exactly one of the two, never both. It binds a tool profile — CLAUDE_CONFIG_DIR for claude-code, CODEX_HOME for codex — to a machine slot, so that a named identity always spawns against the same credentials instead of whatever profile the process happened to inherit. The security model of seats — including the role that can reserve a seat and the billing class that tags its economics — is Security §7; the money side is Cost management.

The machine verb family (from saphan machine --help — re-check it rather than trusting this list, it grows): admit · csr · list · pin · unpin · probe · probe-confinement · renew · retire · seat · set-binary · set-max-sessions · set-price · set-repo-dir · set-slots · set-worktrees-root · refresh-repo · deliver-branch · trust-stamp · unseat. The five that touch seating directly: admit (register a machine), seat (write the identity+machine+slot row), set-slots (resize how many identities a machine carries), unseat (free one slot), retire (withdraw a machine — retirement is a mark on the row, never a delete, and casting onto a retired machine is refused separately).

The only way to bind a seat to a run

saphan run --machine <machine-id> --identity <seat> ...

--identity alone, without --machine, is refused — it does not fall back to anything:

run: --identity "somebody" was given without --machine — a seat is bound TO a machine
(`machine seat` writes identity+machine+slot as one row), and with no --machine named
here there is no row for this run to cast onto: add --machine <machine-id> to bind
"somebody"'s seat, or drop --identity to run unseated on this process's own default
profile

(The sibling case — a bare --slot with no --machine — refuses with the same shape.)

Why this refusal exists

Before the rule, --identity given without --machine was silently discarded: the run ended ok, exit 0, and landed on whichever profile the process's own environment happened to carry — usually the operator's own default, never the named seat's. A green run carried zero information about whether the named seat actually paid for it.

That is the rule this section exists to state: the engine never chooses a profile for the operator. It either binds the seat you named, or it refuses by name. There is no third outcome where it quietly substitutes a default and calls it success.

A status read is not proof a seat is usable

A green machine probe or a vendor auth status reporting "loggedIn": true tells you the profile directory parses and holds some stored credential. It does not tell you that credential still works — token expiry is a separate axis neither check reads. Measured on our own fleet (2026-08-08): a seat whose auth status said "loggedIn": true failed a real cast minutes later with 401 OAuth access token has expired; the same verb against a non-existent profile correctly reports false, so it is not blindly returning true — it simply never checks whether the stored token is still valid.

Two mechanisms respond to this:

  • The engine's own pre-spawn check (R23): before casting onto a seat, the engine runs the cheapest legal renewal probe against that seat's profile (a minimal one-word call, capped at $0.02) and refuses the spawn before it starts if the session is dead — with a TTL cache so a spawn a minute later does not pay for a second probe.
  • The only full verification is a real spawn:
saphan run --machine <machine-id> --identity <seat> \
  --backend claude-code --stream <throwaway-stream> \
  --profile execute --wall-clock 2m ...

A recorded run that completes — or fails with a dated auth error rather than a stale loggedIn: true — is the signal that the seat's credential is live right now.

Limits are per-profile, not per-account

Counter-intuitive, and reported as one measurement, not a law: two config-dir profiles on the same vendor account can carry separate session budgets. Measured on our own fleet (2026-08-08, same minute, same account): one profile returned a session-limit refusal while the other ran normally. Check it on your own fleet rather than trusting either direction; saphan fleet usage (which probes every seated profile's own /usage reading) is the verb that answers it per seat.

One run per slot — and how far one slot stretches

A second dispatch onto a slot that already has a live run is refused, never queued:

machine <id> slot <n> already has a live run (run <run-id>, pid <pid>) — double-dispatch
refused, never a silent queue (R8c, sibling of R8)

A slot carries one concurrent run unless its capacity is raised explicitly, per slot, with machine set-max-sessions — see Security §8 for the database mechanism that makes this race-proof.

The --mint-device-key trap

machine admit --mint-device-key writes the device private key to ~/.saphan/machine/device.key by default. Admitting a second machine on the same host with the default path overwrites the first machine's key. Pass an explicit --device-key <path> for every machine beyond the first on a shared host.

On this page