---
title: Sessions and input control
description: 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](/docs/reference/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.

```ts
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:

```ts
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](/docs/reference/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.
