> ## Documentation Index
> Fetch the complete documentation index at: https://tesser.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Behavior

> The exact rules: plan limits, dev runs, snapshots, sleep and wake, dependencies, and localhost routing

The exact rules, for looking things up. The concept pages, such as
[Boxes](https://tesser.sh/docs/boxes.md) and [Services](https://tesser.sh/docs/services.md), explain the ideas.
What a single command does is in the [CLI reference](https://tesser.sh/docs/cli-reference.md), and
timers and box sizes are in [Configuration](https://tesser.sh/docs/config.md).

## Plan limits

| | Hobby | Pro | Team |
| - | - | - | - |
| Boxes per org | 3 | 10 | no cap |
| `--size large` and `xlarge` | no | yes | yes |
| `tesser sleep-after` | 5 minutes to 2 hours | 5 minutes to 24 hours | 5 minutes to 24 hours |
| `tesser keep` | no | yes | yes |
| Usage past what the plan includes | stops | billed, up to the spend limit | billed, up to the spend limit |

* The box cap counts boxes, awake or asleep. At the cap, `make`, `exec`, and
  `dev` fail with `box_limit`. `tesser rm` an idle box or upgrade.
* An admin sets the spend limit with `tesser spend-limit` or on the
  dashboard's Billing tab. At the limit, the org's awake boxes sleep, and
  launches and wakes fail with `credit_exhausted` until the limit is raised
  or the month resets.
* Pool boxes belong to no repo. Any `make`, `exec`, or `dev` in the org
  claims one of its size.

## Dev runs

### When `tesser dev` restarts the server

`tesser dev` reuses the box for this worktree and service. On a box whose
server is already running, it only syncs, and prints when the server started
and that its inputs are unchanged. It starts the server again when:

* the server is not running, or its app crashed under a file watcher
* the manifest or the env changed
* a file `setup` or `dev` depends on changed:
  * the env files the manifest lists
  * any `package.json`, lockfile, or toolchain pin such as `.nvmrc`
  * a `Dockerfile` or compose file
  * any file the `setup` command names
  * any shell script the `dev` command names (a `.sh`, `.bash` or `.zsh`
    file, or one with no extension)
  * every file in the manifest's directory except `.md` files and the other
    services' manifests

A file a command names counts only when it is not gitignored, so a build
output such as `node_modules/.bin/...` never forces a restart. A source file
the `dev` command names, such as `src/server.ts` in
`bun --hot src/server.ts`, does not count either, because the server reloads
it.

### While `tesser dev` waits

* It prints the last line of the server's log every 30 seconds. When the
  health check keeps returning the same 5xx status, the line says for how
  long.
* When `setup` prints nothing new for 5 minutes, it prints a warning with the
  last line and keeps waiting. Raise `[health] timeout` only when that line
  still shows progress.

### A service name instead of a box id

`tesser status`, `tesser logs`, `tesser sync`, and `tesser wait` take a
service name for this worktree's box of that service.

* When the worktree has no box for it, `status`, `logs`, and `wait` use the
  service's shared instance, and `status` and `logs` say so on stderr.
* `sync` never falls back to the shared instance. It fails and suggests
  `tesser dev <service>`.
* For a script, `tesser status <service> --json | jq -r .boxId` prints the
  box id.

## Snapshots

### What a box gets from one

* A snapshot is a disk image of a repo taken after every service's `setup`
  ran. It holds `/home/ubuntu`, `/var/lib/docker`, and `/var/lib/containerd`.
* A repo's current snapshot is the newest ready one of a commit on the
  default branch. Every new box in the repo starts from it.
* A claimed pool box mounts the snapshot at claim time and starts while the
  rest fills in. If the mount fails, the box starts without it.
* The box reads the image as it needs it while the rest streams in, so it
  does not wait for the whole disk.
* The first sync sends only what changed since the snapshot's commit, and
  package stores and `node_modules` are already in place, so the first
  install is incremental. Without a snapshot, the first `setup` installs from
  scratch. Boxes never copy installs from each other.
* Anything `setup` installed outside `/home/ubuntu` and Docker's data is not
  on the new box, except the manifest's `apt` packages, which the box
  reinstalls on every start.
* If a box cannot read its snapshot, it starts on an empty home directory.
* `tesser dev --snapshot <snapshot_id>` starts a new box from a specific
  snapshot of the repo. `tesser dev --no-snapshot` starts one without any.

### Seeing which snapshot a box has

* `tesser ls --json` shows the snapshot each box started from.
* `tesser status <box_id>` prints it on a `snapshot` line as
  `<snapshot_id>@<sha> (<age> old)`, with the first 12 characters of the
  commit, or `-` for a box without one. A claimed pool box shows `-` until
  its snapshot is mounted.

### Building one

`tesser snapshot create` builds a snapshot of HEAD, or of `--sha <commit>`:

1. A builder box launches at the largest size any of the repo's services
   uses, starting from the repo's current snapshot when there is one.
2. It syncs the commit and runs each service's `setup` with the values from
   `tesser env set`.
3. It deletes `~/.tesser`, `~/.ssh/authorized_keys`, `~/.ssh/known_hosts`,
   `~/.bash_history`, `~/.gitconfig`, `~/.git-credentials`, `~/.netrc`,
   `~/.config/tesser`, `~/.config/gh`, `~/.aws`, `~/.claude`,
   `~/.claude.json`, and `~/.codex`, so no one's credentials reach other
   people's boxes.
4. It captures the disk.

* A repo builds one snapshot at a time, and a build that runs past its
  [time limit](https://tesser.sh/docs/config.md#timers) fails.
* The builder needs room under the org's box cap.
* `--wait` streams the setup log from the builder over SSH until the
  snapshot is ready or fails. The log stays on the builder, so `--wait` is
  the only way to read it. Without `--wait`, `tesser snapshot show <snapshot_id>` shows the build's state.

### Automatic builds from `tesser dev`

When `tesser dev` runs for a service without `--snapshot` or
`--no-snapshot`, it checks the repo's snapshots once it has a box, against the
default branch's tip as your clone last fetched it (`origin/HEAD`, else
`origin/main`, else `origin/master`).

* If that commit needs a snapshot, it starts `tesser snapshot create` in the
  background and prints
  `tesser: building a snapshot of <repo> at <sha> in the background` on
  stderr, with the path of its log (`snapshot-build.log` in the
  [state directory](https://tesser.sh/docs/config.md#files-on-the-laptop)).
* `tesser dev` does not wait for the build, and Ctrl-C does not stop it.
* It builds nothing when:
  * no service has a `setup` at that commit
  * a snapshot of the repo is already building
  * the repo has a current snapshot that is not stale
  * the last build at that commit failed (it tries again once the tip moves)
  * the newest ready snapshot of the default branch is at a commit the tip
    does not contain
* A background build leaves room for one more box under the box cap. It
  stops quietly when the org has no room, is out of credit, has reached a
  plan limit, or keeps no snapshots.

When `tesser dev` waits for the server and `setup` took 3 minutes or more,
it prints a hint after the box id:

* If the service has no shared instance, it names the
  `tesser make <service> --ensure-running <sha>` command that would share one
  box's setup with everyone.
* If the default branch's tip needs a snapshot, it names the
  `tesser snapshot create --sha <sha>` command that builds one, or says one
  is already building.

### Listing, staleness, and removal

* Inside a repo, `tesser snapshot ls` lists that repo's snapshots and prints
  `this repo (<repo>): none` when it has none. Outside a repo, it lists the
  org's. `--all` lists every repo's, and `--removed` includes removed and
  purged ones.
* A snapshot of a commit not on the default branch is marked `branch`. New
  boxes start from it only with `--snapshot`.
* `tesser env set` marks the repo's snapshots `stale`, because their setup
  ran with the old values. A stale snapshot is still used until a new one is
  built.
* Tesser keeps the newest ready snapshot of the default branch and the newest
  of other branches, and deletes older ones once no box is reading from
  them. `tesser snapshot rm` removes one sooner, and boxes made from it keep
  running.
* An org that [runs boxes in its own AWS account](https://tesser.sh/docs/self-hosting.md#snapshots)
  keeps snapshots in its own S3 bucket and has none until it names one.

## Sleep and wake

### What counts as idle

A box is in use while any of these is true:

* a visible browser tab is showing it
* something other than a browser sends it requests (curl, a script, a test
  run)
* a CLI command targets it
* an SSH session is open where someone is typing, or something is running or
  printing

A background tab, or an open shell with nothing running, does not count.

* Traffic between boxes does not count on its own, but a box in use keeps
  the boxes it depends on awake, so a server and its database idle together.
* A kept box keeps the boxes it depends on awake while its server runs. Once
  its server stops, they go back to their own idle clock.
* When an instance box starts or wakes, it wakes the boxes it depends on
  that you own or that are the org's shared instances. A dependency wired to
  a teammate's box is never woken this way.

### What survives sleep

* A sleeping box keeps its disk and its id. Any command that targets it wakes
  it first, and opening it on localhost shows a Wake button.
* Sleep is hibernation: memory is written to disk, and on wake processes
  carry on where they stopped. A dev server keeps its state and does not run
  `setup` again.
* After a wake, boxd waits for the health check (see [Timers](https://tesser.sh/docs/config.md#timers)).
  A server that does not answer in time is started again from `setup`.
* A wake that updates boxd to a newer version also starts the dev server
  again, even after a good hibernation.
* A dev server that exited before the box went to sleep stays exited after
  the wake.
* `instance.startedAt` in `tesser status <box_id> --json` is when the current
  dev process started, so you can tell whether a wake restarted it.
* Connections between boxes close on wake, so your app has to reconnect to
  its dependencies.
* These boxes get a plain power-off instead, and their dev server starts
  again on wake:
  * a box still streaming in its snapshot
  * a box put to sleep in its first minute
  * a box whose image or AWS settings do not support hibernation

## Finding and inspecting boxes

### `tesser ls`

* Piped, one line per box: kind, service, size, power state, whether it is
  online, when it was made, its IP address, and the local worktree it was
  made from.
* In a terminal, a live table with service, worktree, owner, size, state,
  created, idle, and box columns. Press a column's number or click its header
  to sort, press `/` to fuzzy search, and click a box twice to use it on
  localhost.
* A worktree shows as its folder name, followed by its repository in
  parentheses when the two differ, such as `src (zulip)`. The repository name
  is the last part of the worktree's `origin` URL, or the repository's folder
  when there is no `origin`.
* `tesser status <box_id>` adds the instance id, IP addresses, and when it
  was last active.

### `tesser ssh` and `tesser exec`

When a command after `--` exits 127 and its command word still has quotes or
spaces in it, both print one extra line on stderr explaining why. The exit
code is passed through unchanged.

### `tesser util`

* The box samples CPU and memory every second and reports a summary each
  minute.
* Each row is one step: mean, 95th percentile, and peak CPU as a percentage
  of all the box's vCPUs, plus mean, peak, and total memory in GiB.
* `--since` takes values like `30m`, `12h`, or `90d`, and defaults to an
  hour. Longer windows use wider steps so the output stays at 360 rows or
  fewer.
* History is kept 90 days and is gone once the box is removed.
* `--json` also includes CPU and memory pressure.

## Dependencies and shared instances

### A dependency with nothing behind it

* `dev`, `make <service>`, and `make --ensure-running` still start the box,
  and print one line per such port on stderr:

  ```text theme={"theme":"css-variables"}
  tesser: dep :5001 (api) has nothing behind it: tesser wire <box_id> api <target> or tesser make api --ensure-running <sha>
  ```

* When a `make api --ensure-running` has started a new shared instance but
  not switched over yet, that instance is **staged**, and the line names it:

  ```text theme={"theme":"css-variables"}
  tesser: dep :5001 (api) waits on api's instance staged at <short_sha> (<box_id>): it routes here once the --ensure-running promoting it finishes; if none is running, `tesser make api --ensure-running <sha>` finishes it (the human's call; ask first)
  ```

  The dependency reaches the new instance as soon as that command finishes,
  with no wire. If the command is no longer running, running it again with the
  same commit finishes the switch.

* `dev` prints these lines when it returns: after the service is serving, or
  just before the error when the run fails, so they come at the end of its
  output. With `--detach`, it prints them when the box starts. `tesser wait`,
  and `tesser sync --restart` without `--detach`, print them when they return.
  Each check reads the box's current wiring, so a port wired in the meantime
  is not reported.

* On the box, every connection to that port is reset. While the service runs,
  the box notes the reset in the dev log (`tesser logs`), at most once a
  minute per port. On the laptop, the request gets a 503 naming the
  `tesser wire` command that fixes it.

### Waking dependencies

* `dev` starts only the requested service. It wakes the dependencies you own
  and the shared instances it depends on, and once its own server is ready it
  waits for them to serve, printing which ones it is waiting on.
* If a dependency is removed, its server exits, it is still asleep after 15
  seconds, or it does not come up in time, `dev` reports that it is not
  serving and finishes anyway. `tesser status <box_id>` shows where that box
  is.
* `dev` never creates missing dependencies. `make --ensure-running` creates
  shared instances.

### `make --ensure-running`

* It creates a box, checks out the commit, runs `setup` and `dev`, waits for
  the health check, and makes the box the org's shared instance.
* With the same commit again, it does nothing and prints the same box id.
  With a new commit, it starts a new box, waits until it is healthy, switches
  over, and removes the old box.
* From a worktree that already has its own box of the service, when no shared
  instance runs that commit yet, that box becomes the shared instance and
  keeps its warm build. This applies only when the box is the size the
  manifest sets, is not kept, and has no `--sleep-after` of its own; otherwise
  a new box is created. The CLI resets the box to the commit, deletes files
  git neither tracks nor ignores (installs and build output stay), runs
  `setup` and `dev` again with the org's env values, waits for the health
  check, and switches over. The box then belongs to the org, and the
  worktree's next `tesser dev` of the service starts a new box.
* A shared instance belongs to the org, so it cannot be synced to, and its env
  comes from `tesser env set`, not from files.

### During a switch

* Both boxes run `dev`: the new one from when `make` starts it, the old one,
  if awake, until it is removed 60 seconds after the switch.
* Both see `TESSER_SLOT=pinned`, so a queue keyed on `TESSER_SLOT` has workers
  on both commits, and the old commit can take jobs meant for the new one.
  Removal kills any job still running on the old box. Make jobs safe to retry
  and to run on either commit.
* If a job's producer and workers run on the same box, key the queue on
  `TESSER_BOX_ID` instead. Jobs still in the old box's queue when it is
  removed never run.

### `tesser service show`

Prints one service's repo and, for each shared instance, the full commit sha,
the box id, and a `tesser ssh` command that prints the manifest it runs. That
reads the file on the box, which can differ from the copy committed at that
sha. `--json` prints the same as JSON.

### `tesser wire`

* It refuses a target that is not a box in your org, has been removed, or runs
  a different service than the dependency names.

* After saving, it waits until this laptop's daemon routes the change. If the
  daemon has not loaded the wire after 70 seconds, the command fails, but the
  wire stays saved. `tesser daemon stop` then `tesser use` restarts the
  daemon.

* A wire changes where new connections go but restarts nothing. If the box's
  server ran while the dependency had nothing behind it, its client may have
  given up, and requests can hang even after the wire. Then `wire` prints:

  ```text theme={"theme":"css-variables"}
  tesser: <box_id> ran with api unbacked; restart it from its worktree so it reconnects: tesser sync <box_id> --restart
  ```

* A wire names a box and stays put. If that box is removed, the dependency
  fails loud instead of falling back to the shared instance. Re-wire it or
  return it to `--shared`.

* A wire connects boxes, whichever worktree or repository they came from.

* The panel marks a branch `custom wiring` when a laptop port reaches a
  different box than the box's own dependency on that port, and names any
  route that is failing. Laptop focus can override subtrees without changing
  wires.

* `tesser dev` reuses the box for a worktree and service, so each
  simultaneous frontend-plus-backend test needs its own frontend worktree. In
  a monorepo, run the frontend from the backend change's worktree.

* When both a dashboard and its backend depend on a third service, wire both
  to the copy meant for the test. Focusing the dashboard brings every
  reachable service to localhost, but does not fix inconsistent wiring on the
  boxes.

### `tesser exec --deps-of`

The workbench gets that service's dependency ports for as long as the command
runs. They resolve exactly as for that service's box in this worktree, wires
included. `--in <service>` on its own only changes the working directory to
the service's `root`.

## localhost routing

### The daemon

* `tesser dev`, `use`, and `review` start tesserd in the background when it
  is not running. `tesser daemon install` makes it a login service (see
  [`tesser daemon`](https://tesser.sh/docs/cli-reference#tesser-daemon)).
* It listens on port 3000 for its own panel and status page, and on every
  port any of your boxes declares or depends on. A port means that port
  everywhere: nothing is redirected to another port of another box.
* It answers only requests addressed to a name that reaches this machine:
  `localhost`, any `*.localhost` name, an IP address, the machine's hostname
  or `<hostname>.local`, and its Tailscale name when Tailscale is running.
  Any other `Host` gets a 403 that says to open `localhost:<port>` instead.
  A proxy of your own in front of tesserd must send `Host: localhost`.
* It answers your own machine and Tailscale peers, and refuses the rest of
  the LAN unless started with `--allow-lan`.

### Box addresses

* A box id such as `box_a1b2c3` appears in its address as `box-a1b2c3`,
  because a Content-Security-Policy does not accept a hostname with an
  underscore. `box_a1b2c3.localhost` still reaches the same box.
* Every port a box declares is served, such as `box-<id>.localhost:9229` for
  a debugger. Opening a page on a port the box does not serve, such as 3000,
  sends the browser to the same path on the box's own port. Any other request
  on that port answers 404.
* The laptop serves the boxes it started, boxes opened from a share link, and
  the org's shared instances. A shared instance needs no share link: the
  first request to its address adds it to the laptop, and opening it in a
  browser wakes it. `tesser service ls` shows each shared instance box.
* A box the laptop cannot add, such as a teammate's box nobody shared with
  you, answers 404 `NO_TARGET` with the reason, such as that the box does not
  exist or that you are not allowed to run on it. A browser shows the reason
  as the page heading. When the daemon cannot reach Tesser to ask, the
  request answers 503 and can be retried.
* A request from another site's page, such as an image on an unrelated site,
  never adds a box. Open the box's address from the address bar first.

### Dependency ports on a box address

* If a frontend's manifest says `5000 = "api"`, `box-<id>.localhost:5000` is
  the `api` that frontend talks to: its wire, or the org's shared instance,
  whichever its routing table says now.
* When that backend is one of your boxes, the request rides the tunnel the
  laptop already holds to it. Otherwise the daemon opens one through the
  frontend box's loopback port.
* A dependency with nothing behind it answers 503 and names the
  `tesser wire` command that fixes it.

### A box page calling bare localhost

* A page on a box's address stays with that box when it calls bare
  `localhost`. If `box-<id>.localhost:3000` fetches `http://localhost:8080`,
  loads a script or image from it, or opens a websocket to it, the request
  goes where `box-<id>.localhost:8080` would. So a URL your app has baked in
  stays in that box's worktree.
* tesserd recognizes these requests by their `Origin` header, or `Referer`
  when there is no `Origin`.
* A port outside that box's declared and dependency ports goes to the focused
  box.
* Page loads on bare `localhost` are never routed this way. Moving the focus
  does not reload these tabs or close their sockets.

### From a box

* Code on any of your boxes can open `http://box-<id>.localhost:<port>` for
  any instance box in the org and reach that box's declared port, wired or
  not. This includes the org's shared instances, so a shared box can call
  back into a dev box that handed it its address, such as for webhooks.
* Workbenches declare no ports, so they have no address. A box holds
  addresses for at most 254 others.
* A browser or curl on the box, which dials loopback with the name in
  `Host`, is routed by name on a port some other box declares.
* Nothing is bound on the box for this. Anything listening on the box wins
  its port, whether it started before or after: its own ports, its
  dependency ports, a database a test started. So from a browser on a box, a
  peer's copy of a port this box listens on reaches this box.
* A port another box declares accepts a connection on this box's loopback
  even when nothing local listens, so a raw TCP probe (`nc -z`,
  `wait-on tcp:`) cannot tell the port is free. Probe over HTTP. A port the
  target box does not declare is refused immediately.
* The run command gets the box's own address as `TESSER_BOX_URL` and its id
  as `TESSER_BOX_ID`.
* `<service>.localhost:<port>` names whichever box is the org's shared
  instance of that service now, so it survives a promote: the name moves to
  the new box and the old one drains. In-flight connections stay; a new
  connection after the promote lands on the new box. Use it when the other
  side should be the shared instance rather than one box, such as a webhook
  subscription that should survive deploys.

### Focus

* Focusing a box focuses its whole dependency graph. Each box brings its
  declared ports and dependency ports. Shared dependencies are included even
  without a local target or SSH key for them: the daemon tunnels through one
  of your registered boxes.
* The last explicit focus wins for every port in its subtree. Unrelated ports
  stay where they were. Removing a focus reveals the one underneath.
* A missing binding fails with an explanation. It never falls back to an
  older focus.
* Shared nodes and cycles are visited once per focused root.
* Focus changes only the laptop's routes. Dashboard A's server process still
  calls the backend wired to A. To browser-test a backend-only branch B, run
  a dashboard B and wire it to backend B (see
  [Wiring a dependency](https://tesser.sh/docs/wiring.md#wiring-a-dependency)).
* A new `tesser dev` joins the focus at the bottom, taking unclaimed ports.
  Re-running an existing box keeps its priority. Starting a service does not
  rewire its dependencies or launch other services.

For a dashboard wired to a server, where the server uses hp and a worker:

| Choice | Dashboard | Server | hp and worker |
| - | - | - | - |
| Use dashboard A | A | A's server | A's dependencies |
| Use server B | stays A | B | B's dependencies |
| Use hp C | stays A | stays B | hp C; worker stays unless hp C depends on another worker |
| Use dashboard A again | A | A's server | A's dependencies again |

### `tesser use`

* The focus is saved even when the daemon is unavailable. The command then
  fails and says routing was not verified.
* It also fails when the focus is applied but its routes are unavailable. A
  ready route means the local tunnel accepts connections and the last known
  box status allows serving. It is not an application health check.
* `tesser use --json` prints the same graph and status the panel reads.

### TCP

HTTP on a port is routed by name. Anything else is passed through as bytes,
so a database dependency is reachable from laptop tools on bare
`localhost:5432`, the same as from the box.

### Cookies and sockets

* `localhost` and `box-<id>.localhost` are different sites to the browser,
  so a `Lax` or default cookie is not sent between them. A box's address has
  its own cookies, so a tab there keeps a sign-in per box.
* A `SameSite=None` cookie set on `localhost` is sent with a box page's calls
  to bare `localhost`, and those calls reach the page's box, not the focused
  one.
* When the focus moves, the proxy closes the old box's connections on the
  ports that moved, so a hot-reload socket stops feeding the page from the
  old branch. Pages on ports that did not move are not touched.
* A frontend that calls its API at a hardcoded `http://localhost:<port>` and
  signs in with cookies should call its box's address instead and set
  `box_address = true`, or serve the API through the dev server's proxy, so
  the page only talks to its own origin.

### The panel

* `⌘⇧U` opens the panel. It lists the branches serving this port, each
  followed by its repository when the repository's name differs from the
  branch's. Hovering shows which branch you are on.
* "Hide until tesserd restarts" takes the square off every page. `⌘⇧U` still
  opens the panel, and expired AWS credentials still show.
* Choosing a branch focuses the service serving this page and its whole
  dependency graph. Choosing the current branch again restores its graph
  after overrides.
* Ports that fail to connect are listed under the branches.
* A tab keeps the kind of address it was opened on. On a box address,
  choosing a branch moves the tab to that branch's box address on the same
  path, and each box keeps its own cookies. On bare `localhost`, the address
  stays and the page reloads onto the chosen branch.
* A service with `box_address = true` never stays on bare `localhost`: a page
  opened there moves to the box that port shows, on the same path. Only page
  loads move.

## Members, sharing, and sign-in

### Orgs

* `tesser login` asks which org to use when it matters, and
  `tesser login --org <id>` picks one. `tesser whoami` prints the signed-in
  email and the current org.
* An admin renames the org on the dashboard's Organization tab, with the
  pencil button beside its name. A name is 1 to 64 characters. The org id
  never changes, so `tesser org`, `TESSER_ORG`, tokens, and grants keep
  working.

### Members and roles

* A member added with `tesser member add` is a member at once. The address
  gets an email from [noreply@tesser.sh](mailto:noreply@tesser.sh) naming the org, with a link to the
  dashboard and the install line. Signing in on the dashboard lands them in
  the org.
* `add` again with the same role resends the email. With a different role it
  fails; `tesser member role` changes a role.
* Admins manage members, service env values, and the warm pool, and can
  operate on any box in the org. Members create and use their own boxes, wake
  and `keep` the shared instances, and pin them with `--ensure-running`.
* The last admin cannot be demoted or removed.
* Service accounts, service account tokens, and grants are on the Team plan.
  Shared instances and the warm pool are on every plan.

### Box keys and share links

* `tesser status` shows the membership id that owns a box, and
  `tesser member ls` maps it to a person.
* A box carries the keys of its owner's laptops and the org admins'. A box an
  agent or CI job made for someone carries that person's keys too, and a
  shared instance carries every member's. Nobody else's key is on it until
  it is shared.
* Opening a share link (or `tesser review <link>`) puts the teammate's key on
  the box and on the owner's boxes it depends on, never on the org's shared
  instances. The box appears at `box-<id>.localhost:<port>` on their laptop,
  and `tesser ssh` works.
* While the link lives, the teammate counts as another owner: `sync`, `dev`,
  `wire`, `sleep`, and `rm` all work, so a `sync` from their worktree
  overwrites the author's.
* Links expire, die with the box, and can be revoked
  (`tesser share --revoke`). Each takes the key and the access back.
* On Tesser's cloud, `tesser share` also prints a
  `https://box-<id>.tesser.review/` address that the org's members open in a
  browser while the link lives. The first visit signs them in at tesser.sh,
  and only active members get in. To serve shared boxes on your own domain
  for people without a Tesser account, set up a
  [share gateway](https://tesser.sh/docs/share-gateway.md).

### Commands an agent is refused

Any member can change any box in the org, but when a coding agent such as
Claude Code or Codex names a box you did not make (a teammate's box, a shared
instance, an unclaimed pool box), these are refused:

`rm`, `sync`, `exec`, `dev`, `sleep`, `keep` (including `--off`),
`sleep-after --box`, `env push`, `wire <box_id> <service> <target_box_id>`,
`share --revoke`, and `boxd update`.

* The error says whose box it is and prints the same command with `--yes`,
  which the agent should run only when you asked it to use that box.
* The check runs before the box is woken, so a refused command never wakes or
  bills it.
* Commands that leave off the box id use the worktree's own box and are never
  refused. Read-only commands (`ls`, `status`, `logs`, `ssh`) are not
  checked, and neither are commands you type yourself in a terminal.

### Tokens

* A token comes from the dashboard (Your tokens on the Organization tab), or
  an admin mints one for a service account with `tesser member token create`.
* `tesser login --token tsr_api_… --org org_…` saves one.
* A token belongs to one org. Every token expires: 90 days from the
  dashboard, or whatever `--expires` said.
* `tesser logout` revokes the saved token and deletes it from disk.

### Grants

* A grant is a rule: OpenID Connect id tokens from one issuer whose claims
  match the grant's pins may be exchanged for a tesser token acting as one
  service account. Grants are org-level, made by admins, and always for a
  service account, never a person. The grant id is not a secret and can be
  committed.

  ```sh theme={"theme":"css-variables"}
  tesser grant create devin --pin org_id=<devin org id>   # service account "devin", made if missing
  tesser grant create replicas --pin organization_id=<replicas org id>
  tesser grant create github:acme/web --pin ref=refs/heads/main
  ```

* Each prints its id and the `tesser login oidc --org org_… --grant grt_…`
  line to run where the issuer is.

* The CLI gets an id token with the control URL as its audience. The control
  plane checks it against the issuer's keys and the pins, and returns a
  token that lasts as long as the id token, an hour at most. The CLI proves
  again when it is about to expire.

* In a GitHub Actions job with `permissions: id-token: write`, the CLI mints
  the id token itself. For Devin and Replicas, the printed line carries
  `--proof '<command>'`, a command that prints an id token for `{audience}`;
  the CLI fills in the audience and reruns it whenever it needs a token.

* An SSH key added with an exchanged token is dropped from every box once the
  last token of its session (a GitHub run, a Devin session) expires or is
  revoked.

* `tesser grant rm` stops new exchanges.

### Grants and your own AWS account

When the org runs boxes in its own AWS account, the agent also needs AWS
credentials. Give the grant a role:

```sh theme={"theme":"css-variables"}
tesser grant create devin --pin org_id=<devin org id> --aws-role arn:aws:iam::<account>:role/<role>
```

* The role trusts the same issuer for `sts:AssumeRoleWithWebIdentity`, with
  `sts.amazonaws.com` as the audience and conditions matching the pins, and
  allows Session Manager to the org's boxes.
* `tesser login oidc` assumes it with the same proof command for that
  audience, writes AWS profile `tesser-<org>` with a `credential_process` that
  proves and assumes again whenever AWS asks, and makes it the profile the
  CLI uses for the org's boxes.
* Assuming the role never asks Tesser, so `tesser grant rm` does not end AWS
  access. Changing the role's trust policy does.

### Who an agent's boxes belong to

* Boxes an agent session makes show as `devin · <session>`, recorded as made
  for the person who started the session: their email from the id token, or
  an alias an admin set with `tesser member alias set`. GitHub Actions carries
  no email, so its `actor_id` only attributes through an alias.
* They show in that person's dropdown.
* Usage is metered per box-second and shown on the dashboard's Usage tab.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.