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, qwen-code and the other CLI backends) or a lane (for openai-compat and 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 in Seats: separating agents that share a machine; the money side is Cost management.

The machine verbs are listed in saphan machine. The ones 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), and retire (withdraw a machine — retirement is a mark on the row, never a delete, and casting onto a retired machine is refused).

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

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 run that did would carry no information about whether the named seat paid for it.

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.

Two mechanisms respond to this:

  • The engine's own pre-spawn check: before casting onto a claude-code seat, the engine asks the vendor for the seat's usage through the profile — a read that spends no model turn and costs nothing — and refuses the spawn before it starts if the credential is dead. A good answer stands for two minutes, so a spawn right after another does not ask again.
  • 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 — check, do not assume

Two config-dir profiles on the same vendor account can carry separate session budgets, so one can be refused for a session limit while the other runs. Do not assume either direction on your own fleet: saphan fleet usage, which reads every seated profile's own usage, answers it per seat.

One run per slot

A slot carries exactly one live run. A second dispatch onto a slot that already has one is refused, never queued; the refusal names the machine, the slot and the live run, and ends "double-dispatch refused, never a silent queue". The retired machine set-max-sessions verb refuses by name: there is no per-slot capacity to raise. See Runners: what happens during a run for the mechanism that makes this race-proof.

A second machine on the same host

machine admit --mint-device-key writes the device private key to ~/.saphan/machine/device.key by default, and never overwrites a key that is already there. Admitting a second machine on the same host with the default path is refused: the refusal names the existing key and prints the two commands that reuse it (machine csr --reuse-key, then machine admit --csr). To give the second machine its own key, pass an explicit --device-key <path>.

On this page