Sessions and input control
One holder of the keyboard at a time, and how a person takes it back from an agent.
Two objects sit between a client and a machine’s keyboard: a session, which is a connection, and an input lease, which is permission to type. Reads never need either.
Sessions
POST /v1/sessions open one on a machine
GET /v1/sessions list them
DELETE /v1/sessions/{session_id} close one
A session has a type — agent, human or view — and a state of active, idle or
closed. It records which user or API key opened it, which is how the machine’s event log can
say who took control.
Both SDKs open one for you on the first call that needs it, and close it in machine.close() or
on leaving a Python with block. Do call that: an abandoned session holds a lease until it
expires, and nobody else can type in the meantime.
Concurrent sessions are a plan entitlement. See billing.
The input lease
POST /v1/machines/{id}/input-lease acquire or renew
GET /v1/machines/{id}/input-lease read the current holder
DELETE /v1/machines/{id}/input-lease release
At most one session may send input to a machine at a time. That is what makes human takeover safe: taking control revokes the agent’s lease rather than interleaving with it.
const lease = await client.api.inputLeases.acquire({
params: { machine_id: machine.id },
body: { session_id, ttl_seconds: 60, force: false },
});
lease.fence; // increments on every grant
lease.expires_at;
ttl_seconds is 5 to 3600, default 60. A lease is renewed by acquiring it again.
The fence
Every grant increments a counter, and every input message carries the fence it was sent under. The gateway drops a message whose fence is lower than the machine’s current lease — that client is a former controller — and it drops one that is higher too, so a client cannot promote itself by inventing a number. An expired lease is a lower fence by definition, because the next grant increments it.
That is the whole takeover mechanism, and it is enforced at one point rather than by hoping each
client behaves. A client whose fence is stale gets one lease_lost message and must fall back to
read-only.
force
force: false is the default and is what an SDK always sends. Taking a lease from its holder is
human takeover — a deliberate act by a person, not something a library does on a click. The
dashboard sends force: true when a person asks for the keyboard; an agent gets a conflict
naming the current holder.
The SDKs never take a lease back
Both SDKs acquire the lease on the first call that types or clicks, renew it at half its TTL, and stop if a renewal fails:
machine.holdsInput; // false after a takeover
Not re-acquiring is the important half. A lost lease means a person took over, and silently
taking it back would put an agent and a person on the same keyboard, which is the one thing
single-holder input exists to prevent. The next input call surfaces the API’s own conflict,
which says who holds it.
Where policy and enforcement live
Lease policy is the API’s: who may hold one, for how long, and whether a force is allowed. Lease enforcement is the gateway’s: the fence check on every input message. Two places that can grant a lease is one more than can be reasoned about.
Terminals are not leased
A terminal is a session of its own. Two people holding separate ptys on one machine is normal, so the terminal is deliberately outside the lease. The lease governs the desktop, where there is one mouse and one keyboard.
Screen streams are not leased either — a viewer that cannot type can still see.