Skip to content

Access

When another device reaches this one, or an agent here asks the service for something, Alto checks the request against rules: who may do what, where. There are two kinds:

  • Grants allow. Nothing from another device is allowed unless a grant allows it, and grants only add up, so adding one never takes anything away.
  • Forbids take away. Any forbid that matches wins over every grant, wherever each came from.

There’s no order between rules: a request is allowed when some grant allows it and no forbid matches.

These are always there, and are what Alto has always allowed:

Who May
You, on this device Anything. Never refused, by any rule, so a broken file can’t lock you out of fixing it.
Agents on this device What you may, unless a forbid says otherwise; see Agents.
Your own devices (your Tailscale account, or a trust entry) Anything another device can ask, including what happened here and your other devices.
A device paired from another account Read, type into, start, end and answer its sessions here, but not this device’s own information.

Some things only this device can do, whatever the grants say: pairing, settings, trust, and the like.

access.toml, beside settings.toml, holds your grants and forbids:

# Alex's devices may see what happens here, as your own do.
[[grant]]
src = ["alex@example.com"]
dst = ["device"]
verbs = ["read"]
# Agents here end only the sessions agents started.
[[forbid]]
src = ["agent:*"]
dst = ["*"]
verbs = ["kill"]
unless = { dst = ["by:agent"] }
because = "agents here end only what agents started"
# A group, for this file's rules.
[groups]
eng = ["alex@example.com", "*@eng.example.com"]
  • src, who: a login (sam@example.com), a domain (*@example.com), device:NAME (a paired device, by the name alto devices shows), tag:NAME (a tailnet tag), group:NAME (from this file’s [groups]), person, agent:NAME or agent:*, or *.
  • dst, where: *, sessions (any session, or a new one), own (the session the caller runs in, and those it started), device (this device’s own information: what happened here, your other devices), or selector words for sessions (by:agent, agent:claude, status:needs-you) and for this device (pool:NAME, tag:NAME, os:linux).
  • verbs: read, send, spawn, kill, approve. Typing is send, but typing at an agent while it’s asking something is answering it, so it’s approve (C-c or Escape alone stay send). A message to an agent (call … message) is send on its session; a method a program offers needs what its alto offer --needs said, send by default.
  • join: the modes a session is joined in, observer, peer and moderator (see Joining). A rule needs verbs or join.
  • A forbid’s unless is where it doesn’t hold: from one of its src, at one of its dst, or both (then both must match). Its because is told to whoever it refuses.
  • A group counts only in the file that defines it: your group:eng isn’t your organization’s.

A change takes effect at the next request, with no restart.

An agent is checked twice: as you (or whoever it acts for), and as itself. It’s allowed only if both are, so it never gets more than its person; a grant to agent:claude only widens what the agent itself may.

What agents may do as themselves by default is [agents] scope in settings.toml:

[agents]
# "open" (the default): whatever their person may.
# "own": read anything; type into, start, end and answer only their own
# sessions (the one they run in, and those they started).
scope = "own"

With "own", a grant gives an agent more:

[[grant]]
src = ["agent:claude"]
dst = ["pool:scratch"]
verbs = ["send", "kill"]

A session holds everyone attached to what they may do there. A device that may read a session but not send sees it, and anything it types is refused, saying so, once:

alto: session 4: refused: not allowed to send session 4 on this device: watching only (its log says why: access-91be0c)

Joining as an observer, a peer or a moderator

Section titled “Joining as an observer, a peer or a moderator”

Whoever attaches to a session joins it in a mode:

Mode May
observer Watch.
peer Watch, type, and answer what the session asks.
moderator All that, and pause agents there (alto pause) and end the session.

alto attach --mode observer 4 joins as one; without --mode, a client joins as the most it may, so you and your own devices are moderators, as before. Commands that attach for a moment (send, pause, capture) take the most they may.

Rules decide the modes, through the verbs each needs (a peer send, a moderator also kill) and join:

# Alex's laptop watches, and nothing more.
[[forbid]]
src = ["device:alex-laptop"]
dst = ["*"]
join = ["peer", "moderator"]
because = "watch only"
# The SRE group may type in prod, but not moderate there.
[[grant]]
src = ["group:sre"]
dst = ["pool:prod"]
join = ["observer", "peer"]
  • A forbid’s join refuses those modes (it needs no verbs). Asking for one is refused at once, saying why; joining without --mode takes the most left.
  • A grant’s join gives only those modes, each with what it may do, whatever the grant’s verbs. A grant without join gives the modes its verbs reach.
  • Ending a session with alto kill is a moderator’s too: a forbid of moderator stops it.

alto access check WHO MODE SESSION asks about a mode:

$ alto access check alex-laptop peer 4
refused: joining as a peer needs read: access.toml, forbid 1 forbids alex-laptop read session 4: watch only; the most it may join as is an observer

The log says the mode each client joined in, and a mode refused.

A forbid’s unless can ask for someone to be there:

[[forbid]]
src = ["agent:*"]
dst = ["pool:prod"]
verbs = ["send"]
unless = { present = { mode = "moderator", count = 1, from = "group:sre" } }
because = "agents in prod type only while SRE watches"

Unlike other forbids, it doesn’t refuse: an agent’s typing there waits until enough of the right people are attached, then goes through, in order. While it waits, alto ls says “agents wait for a moderator (agents in prod type only while SRE watches)” and the fleet view marks the session ⏸. A session started where the rule applies waits from the start.

  • mode: moderator (the default) or peer; someone attached in that mode or more counts.
  • count: how many (1 by default).
  • from: who counts, as src says it (a word or a list); anyone, if it’s left out. You on this device count when it names you (person, say).
  • It holds typing only, so verbs = ["send"]. A grant doesn’t get past it, and people typing aren’t held by a rule on agents.

alto access check agent:claude send 4 says when typing would wait, and for whom.

Who’s attached, and how, is in the listings: alto ls says “watched by 2 people (1 observing), 1 agent”, alto describe names each, the fleet view counts them (◉ 3), and alto ls --in watched:no lists the sessions nobody is attached to.

$ alto describe 4
session 4: agent, working
watched by 2 people (1 observing), 1 agent
you, moderator
alex@example.com on alex-laptop, observer
claude in session 2, moderator

A grant that can’t be read is access not given, which is safe; a forbid that can’t be read would be access given. So:

  • access.toml broken (it can’t be read, or says something Alto doesn’t know): the last rules it had that read cleanly stay in force, forbids and all, until it reads again. alto settings, alto access check and the service’s log say so. Taking the file away takes its rules with it.
  • Your organization’s managed.toml broken: nothing from another device or agent is allowed until it reads again. You, on this device, still may.

Managed settings take the same [[grant]], [[forbid]] and [groups], beside their settings. Their forbids hold as yours do, and you can’t change them.

A tailnet’s policy can grant Alto access in its grants, as an app capability; Tailscale has already decided who:

{
"src": ["group:sre"],
"dst": ["tag:alto"],
"app": { "altoterm.com/cap/alto": [{ "dst": ["sessions"], "verbs": ["read"] }] }
}

Each value takes dst, and verbs or join, as above. They’re read from Tailscale with each connection, so they last only while the policy says them. The tailnet carries grants only: a forbid there would be lost whenever Tailscale couldn’t be asked, so one is ignored, and the service’s log says so. Forbids go in managed.toml.

alto access check WHO VERB WHERE gives the decision and the rule behind it, without doing anything:

$ alto access check paired read device
refused: no grant lets a paired device read this device's own information; nearest: built in: devices paired with this one
$ alto access check alex@example.com read device
allowed by access.toml, grant 1
$ alto access check agent:claude kill 4
refused: access.toml, forbid 1 forbids claude, for you, on this device kill session 4: agents here end only what agents started

When the answer lets another login type here, it adds a warning: whoever may type into a shell may run anything as you.

WHO is me, agent:NAME, yours (one of your own devices), paired (a device paired from another account), a paired device’s name, or a login. WHERE is a session’s id, new, or device. It exits 1 when refused; --json gives allowed, reason, file and problem.

Whoever is refused is told what they asked, where, and a forbid’s because, not the rules, with a reference to this device’s log, which has the whole reason:

alto: refused: not allowed to kill session 4 on this device: agents here end only what agents started (its log says why: access-3fa2c1)

The reference is in the log too: alto log --json | grep access-3fa2c1.

  • Typing into a shell is everything. Whoever may send to your shell can run anything there as you, including editing access.toml. For other people, the line that holds is read against send.
  • Rules about agents are as strong as knowing an agent is one. Alto knows an agent by the program it runs under; one that leaves it (as a background service) is taken for you.
  • Sessions started before this version don’t hold agents or other devices to the rules. Restart them (alto reload --restart) to bring them in.