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? Which streams a client may read is a separate human act — see the MCP server. Enrolling a client binding requires --stream, and a binding that carries no grant 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 on the same signed identity log as machines — signed by the root key, or by the identity-signing key the root granted. 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 seat is one run — enforced by the database (see Runners), so parallelism is bounded by a number you set (how many seats) rather than by whatever happens to be running. Concurrency is bought in seats, not in sessions: three seats on one machine are three separate accounts running three concurrent runs, and adding a fourth concurrent run means adding a fourth seat. A machine may additionally cap the total across all its slots.

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 and not a separate operating system user. Two seats on one machine run as the same user: a confined run cannot write another seat's profile, but it can read it. 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 Documented limits.

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

ConditionWhy it is refused
Machine not admitted, or retiredNo 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 homeRe-seat it with a configuration home
Configuration path failing validationChecked on write and on every read

The configuration path is checked when it is written and again on every read. 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 Documented limits.

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 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 without a billing class — 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