Saphan StudioDocs
Security

Seats: separating agents that share a machine

A seat is a named executor identity assigned to one machine and one execution slot, for one agent backend, with its own configuration home.

Enrolling an OAuth client, not a seat? The row-level grant — which streams this client may read — is a separate human act and it is documented in the MCP server's authorization page. It is named here because an operator enrolling an identity is likelier to open this document than that one, and because the grant fails closed: an identity enrolled without --stream reads zero rows, not every row. --stream '*' is how you say "the whole fleet", and saying it is deliberate.

A seat is a named executor identity assigned to one machine and one execution slot, for one agent backend, with its own configuration home. Seats are created by an owner act and recorded, root-signed, on the same identity log as machines. Each identity holds at most one machine-and-slot pair fleet-wide.

The problem seats solve

Agent backends keep their session state, credentials, authentication tokens and history in a single per-user configuration directory. Run two agents on one machine as the same operating system user and they silently rejoin into that directory: they share sessions, they share credentials, and — the part that surprises people — whichever authenticated last wins for both. There is no error and no warning. You discover it when work appears under the wrong account, or when one session's logout signs the other one out.

A seat gives each executor identity its own configuration home, bound through the environment variable that the specific backend reads for its profile location. In practice this is a soft chroot for agent identity: the same machine, the same user, the same binary — but each seat sees only its own profile, its own credentials, its own session history, and its own authentication state.

What this buys you operationally:

  • Several different accounts on one machine, concurrently. Two subscriptions, a personal and a team account, or one account per client — each on its own seat, running at the same time, without touching each other's tokens.
  • Different backends side by side. One seat driving one agent tool, another seat driving a different one, each reading its own profile variable.
  • No cross-contamination of session state. A crash, a logout, a token refresh, or a corrupted profile is contained to one seat.
  • Concurrency you can reason about. Slot count is what a machine advertises; a seat occupies exactly one slot; and a slot carries as many live runs as its max_sessions allows — one, unless you say otherwise — enforced by the database (section 8), so parallelism is bounded by two numbers you set rather than by whatever happens to be running. Capacity is per slot, not per machine: three seats on one machine are three separate accounts, and each carries its own number.

Be precise about how hard this boundary is. It is a soft chroot, and the adjective is load-bearing. What it separates is profile state — configuration, credentials, sessions, history. It is not a kernel boundary, it is not a separate operating system user, and on a remote machine it is not backed by the sandbox described in section 8. Two seats on one machine can still read each other's files if they go looking, because they run as the same user with the same filesystem access. Seats prevent accidental rejoin, which is the failure that actually happens in practice and happens silently; they do not contain a determined process. Where you need a hard boundary between two workloads on one machine, use separate operating system users or separate machines — and see section 11.

Binding a run to a seat is fail-closed. It refuses:

ConditionWhy it is refused
Machine not admittedNo identity to bind to
Identity not seated on the named machineSeats are fleet-wide unique
Ambiguity — zero or two-plus seated identitiesThe engine names the candidates and requires an explicit choice. It does not guess.
Explicit slot disagreeing with the identity's actual seatOne identity must not occupy another's seat
Backend / seat mismatchCasting the wrong tool exports the configuration home under a variable that tool never reads — landing back in the shared directory
Seat with no configuration homeCan only be one created before this control existed
Configuration path failing validationChecked on write and on every read

That last check is deliberate: a record signed before the check existed must still pass on every read, and a conformance test fails if either call site is removed. Separately, everything that reaches a remote command line is shell-quoted, so a hostile value that somehow entered a record would be inert.

Summary of the boundary: a seat separates profile state, credentials and sessions, and does so reliably. It is a configuration boundary, not an operating-system boundary — see section 11.

The economic guard: billing class

A seat can carry a billing class: subscription, metered, or local-energy, set with machine seat --billing-class (a closed vocabulary — an unrecognized value refuses, nothing is written). It exists because a vendor account's billing relationship is part of that account's identity, and changing which relationship a seat uses is consequential enough that it must be an explicit owner act, never something a tool infers from behaviour or defaults to for convenience. Concretely: nothing should ever be able to move a subscription seat onto metered, pay-per-token billing without the owner deciding so, on the record.

local-energy names a seat whose model endpoint the owner runs directly: there is no vendor account to meter, and marginal USD is declared zero by the owner's own act — never measured, never inferred. It is an affirmative member in its own right, not an alias for subscription: a consumer that gates spend must name it explicitly alongside subscription rather than treating "anything but metered" as free.

The field lives on the same signed seat row every other seat fact does — not in configuration, not in an environment variable. Changing it means appending a new root-signed machine seat record, exactly like changing a seat's backend or configuration home. There is no lower-ceremony path to it.

Absent reads as unknown, never as permission. A seat admitted before this field existed, or seated without --billing-class, carries no tag at all — and an absent tag is projected as unknown, not as subscription and not as "anything goes." This is deliberate and is the single most load-bearing default in this section: a billing-aware consumer that only proceeds on an explicit subscription treats every seat that predates this field exactly as it would treat an explicit metered one — refused until an owner act says otherwise. A permissive default here would make the guard decorative on every seat that already existed.

machine list and fleet show both project the current billing class alongside the rest of a seat's identity, so an operator can audit it without a separate query.


On this page