Target leases¶
Terms:
- Target: one board under test, a
[targets.<name>]block in the lab file. - Lease: a time-limited reservation of a whole target by one holder.
- Holder: whoever acquired the lease and has its token: one person, one agent, or a group of agents the token was handed to.
- Token: the random secret
panio lease acquireprints. A command proves it is the holder by carrying the token in thePANIO_LEASEenvironment variable. - Default host: the target's
hostfield, which every channel of the target uses unless it names its own. - Control host: the machine cabled to the target. The lease record lives on the target's default host, which is the control host for a remote target.
See the glossary for the rest.
A lease keeps two people, or two agents, from driving one board at the same time. One holder has the board; everyone else can still watch it but cannot change it. Leases end on their own, and ending one cleans the board up for the next holder.
A lab that never runs panio lease behaves exactly as before.
When to use one¶
- A shared lab of scarce boards. Several people share a set of boards. Someone books a board for a limited time and nobody else drives it until the booking ends or is released.
- Parallel agents on one lab. Several agents run at once. Without a lease a second agent can power-cycle a board in the middle of the first one's flash or install. With a lease, each agent's workflow has exclusive control of its board, and a second agent is told who holds it and until when.
Shared-lab workflow¶
export PANIO_LEASE=$(panio lease acquire pi5 --duration 4h --owner alice)
panio netboot start pi5
panio power-cycle pi5
panio serial log pi5
panio lease release pi5
acquire prints only the token on stdout, so the export line captures it.
Every later command in that shell carries the token. If someone else holds the
board, acquire exits 23 and says who and until when. To wait for it:
--wait retries every 2 seconds until the board is free or the time runs
out. To see what is going on first, panio lease status (no target) lists
every target's lease.
Parallel-agents workflow¶
An orchestrating agent leases the board once and hands the token to the sub-agents that should share it:
Sub-agents started from that shell inherit PANIO_LEASE. For an agent that
does not inherit the environment, pass the token in its instructions and have
it export PANIO_LEASE=<token>. Everyone holding the token is the same
holder, so two sub-agents with the token can still step on each other: give
each its own board, or its own turn.
Two agents on the same machine and the same Unix user are different holders as long as they carry different tokens.
MCP¶
panio mcp has four lease tools: lease_acquire, lease_renew,
lease_release and lease_status. One MCP server is one holder. The server
keeps the token from lease_acquire in memory (the tool result never shows
it) and sets PANIO_LEASE on every command it runs for that target. A
PANIO_LEASE already in the server's own environment when it starts is used
as the token for every target, so a harness can hand a server an existing
lease. lease_acquire takes owner, duration, idle, wait and
preempt; lease_release takes preempt; lease_status with no target
lists them all. See MCP server.
Command reference¶
All lease commands take the target positionally or as -t/--target, and may
omit it when the lab has exactly one target. Durations are written 90s,
30m or 2h, and at most 24h.
| Command | What it does |
|---|---|
panio lease acquire [TARGET] |
Reserve the target and print its token alone on stdout. Exits 23 when someone else holds it. --owner LABEL sets the display name (default user@host of the machine you run it on). --duration DUR is the total length (default 2h). --idle DUR ends the lease after this long with no command and no live registered process (default 30m). --wait DUR polls every 2 s for that long. --preempt takes the target from its holder. --json prints {token, owner, expires_at, expires_in_secs}. If PANIO_LEASE already matches, acquire renews and prints the same token. |
panio lease renew [TARGET] |
Move the end to now plus --duration (default: the lease's original duration), capped at 24 h from now. Needs PANIO_LEASE. If the lease had already expired, renew retires it (cleaning up, as any expiry does) and exits 23 saying so. |
panio lease release [TARGET] |
End your lease and clean the target up. Needs PANIO_LEASE, or --preempt to end someone else's. Releasing a lease that is already over is not an error, so an end-of-job release does not retry: with a token that names an ended lease it exits 0 and says on stderr how it ended (and leaves a new holder alone); on a target nobody holds, with no token, it exits 0 and says '<target>' is not leased. A token that never named a lease on the target, or no token while someone else holds it, exits 23. |
panio lease cleanup [TARGET] --id ID |
Retry the cleanup that blocked the target; clears the block when every step passes. Exits 24 if it still fails. See Cleanup. --json prints the result. |
panio lease override-cleanup [TARGET] --id ID --reason TEXT |
Clear a cleanup block without verifying it; the reason and the outstanding failures go into the history first. --corrupt (instead of --id) moves an unreadable lease.json aside. |
panio lease status [TARGET] |
Who holds the target, until when, idle time left (or, while a registered process is alive, kept alive by N process(es); idle_left_secs is null then), and registered processes. Never prints the token. With no target in a lab of several, lists them all; an unreachable host is reported in its row. --json for scripts. Shows state (held, unleased, blocked) and, for a blocked target, the cleanup id and each step's result; exits 0 either way. Not read-only: it cleans up a lease that has expired, once. |
Preemption¶
Anyone can take a lease with acquire --preempt or release --preempt. The
holder's leftovers are cleaned up first, and the displaced holder learns what
happened on its next command (see ended tokens).
Preemption is for the case where the holder has gone away. It is not
access control.
Timing¶
A lease ends at the first of:
- its total duration (default 2 h, at most 24 h);
- being idle for the idle timeout (default 30 min): no command by the holder
and no live registered process. A mutating command by the holder registers
its own process, so an open
serial connectoradb shellkeeps the lease from going idle; the total duration still applies. The dashboard is different: see the dashboard and the idle timer; lease release, or a preemption.
Expiry is lazy: no background process watches the clock. The next
mutating command, acquire, release or status that touches the target
finds the expired lease, cleans up, and carries on. A read does not clean up,
so someone running video shot never stops the daemon it is reading from.
Cleanup¶
Ending a lease, by any route, tears down what the holder left:
- the holder's registered panio processes;
- the target's serialcap, hdmicap and hid daemons, including orphans whose discovery file is gone, waiting for each to exit;
- the target's netboot session, and the netboot link is taken down
(
netif mode off).
Power is never touched: a board that was on stays on.
Every step runs, whatever happens to the earlier ones, and each reports one of
ok, already_satisfied (nothing to do: the daemon was not running, the
process was already gone) or failed. The step ids are
registered_processes, serial_daemon, video_daemon, hid_daemon,
netboot_session and netif_mode_off; a step appears whenever the target has
that channel.
A failed step blocks the target. The lease still ends, but the record in
lease.json becomes a cleanup block and the target is refused to everyone
until a person recovers it: acquire (including --preempt, and --wait
fails at once), renew, release, every mutating command (even with the old
token, and whether or not lease_required is set), and daemons stop|restart
all exit 24 (target_cleanup_blocked). --all sweeps skip the target with a
line on stderr. Reads still run, with a warning when a PANIO_LEASE token is
set, and do not retry the cleanup. The block is written before cleanup
starts, so a panio that dies mid-cleanup leaves the target blocked too, and it
never expires. If recording any of this fails (the record, the history), panio
stops: it does not remove the record or hand out a token.
panio lease status shows the block and never the old token:
dut: blocked: cleanup failed (hid_daemon); cleanup id 3f2a...; run panio lease cleanup dut --id 3f2a...
Two commands recover the target. Both check, under the target's lock, that the record is blocked with exactly the id you give (a held or unleased target, or a newer block with another id, is refused), both leave the target unleased, and neither acquires it:
panio lease cleanup TARGET --id IDre-runs every step against the processes registered when the lease ended. Success clears the block and recordsresolution: "verified"in the history; failure updates the step results, keeps the block, and exits 24. If the target's serial, video, hid or netboot configuration changed since the lease ended, the retry would not clean up what was left, so the block stays and only an override clears it.panio lease override-cleanup TARGET --id ID --reason TEXTclears the block without verifying cleanup. The reason (required, not empty) and the outstanding failures are written to the history, withresolution: "overridden", before the block is cleared. Use it when cleanup cannot succeed (the device is gone, the config changed).
The history keeps the original end reason in how and the outcome in a
separate resolution field, so an ended token still gets its explanation.
An unreadable lease.json is treated the same way: it is no longer read as "no
lease". Mutations exit 24, status reports the target as blocked with an
error, and panio lease override-cleanup TARGET --corrupt --reason TEXT moves
the file aside as lease.json.corrupt-<time> and records it.
Reads and mutating commands¶
On a leased target, commands that only look are always allowed. Commands that change the target are allowed only with the holder's token. Anyone else gets exit 23 naming the holder and when the lease ends.
| Reads (always allowed) | Mutating (holder only on a leased target) |
|---|---|
serial log, evidence, wait, status, show |
serial connect, watch, stop, send, transact, dtr, reset |
video shot, read, show |
video watch, stop, preview |
netboot status, logs, tftp-root |
netboot start, stop |
netif status |
netif mode, down-hard |
power-state |
power on, off, power-cycle |
usb state |
usb attach-host, attach-target |
action list |
action run |
adb show, screencap |
adb shell, run, input |
doctor |
hid send, serve, stop; console |
serial wait and video shot stay reads even though they need a running
daemon: they never start one. console and video preview are mutating
because the dashboard they serve carries keyboard and mouse input.
Not checked at all: commands that edit the lab file (set, add, rm,
target, host, config, init), host-wide commands with no target
(discover, setup, configure, helper, skill, mcp, * devices,
netif vlan), and the lease commands themselves.
The dashboard and the idle timer¶
console and video preview hand you a web page. What you do in the page
(typing, mouse, watching) goes to the daemons directly and never runs a panio
command, so it does not count as activity.
consolekeeps the lease alive itself while it runs, as long as you carry the holder's token. It stays in the foreground (a local one prints "Press Ctrl-C to close", as a remote one does): every half idle timeout, at most every 5 minutes, it counts as holder activity (a remote console makes a read of the target on the control host). A console is not registered as a process under the lease, so ending the lease never kills it silently. If the lease is ever no longer yours (released, preempted, expired), the console prints why on stderr and exits with 23 (target_leased): a local one within about 5 seconds, a remote one at its next keep-alive tick. It returns normally, so its ssh tunnels are closed. For a released or preempted lease the cleanup has already stopped the target's daemons; for one that expired the message says the next lease check will stop them. Only the lease check's own answer ends a remote console: a network failure on a keep-alive tick is a warning and is retried (three in a row warn that the lease may idle out), and a local console that cannot read its lease record gives up after three tries with exit 109, not 23. If your token is not the holder's when the console starts, it says the lease will not be kept alive (a local console then returns as before). Without a token, a localconsolereturns as soon as the page is handed off, as always.video previewcannot do that: the command returns as soon as the page is handed off. With a token set it prints a one-line note on stderr. Runpanio lease renewnow and then, or keep any panio command going (serial connect, aserial logloop), while you use the page.
Daemons¶
panio daemons stop and restart look at the daemon's target: the capture
daemons, hid, and a target's netboot session (netbootd[pi5]). Naming one that
belongs to a target leased to someone else ('serialcap[pi5]') is refused with
exit 23 (exit 24 when the target is cleanup-blocked). stop --all
and restart --all|--stale skip those with one line on stderr for each, and
stop the rest. Every lease check runs before anything is
stopped, so a refusal leaves the host exactly as it was. The stop --all
sweep of untracked orphan daemons skips an orphan whose device belongs to a
leased target the same way; with no readable lab it cannot tell and says so.
Requiring a lease¶
By default an unleased target runs freely. To make a target refuse mutating commands unless the caller holds a lease:
--lease-required false removes the setting. With it on, a mutating command
on an unleased target exits 23 and says to run panio lease acquire; reads
still work. It needs every channel of the target on the target's default host
(see limits); the setting is refused otherwise.
A token whose lease has ended¶
If your lease is released, force-released, preempted, or found expired, your token no longer works. panio does not run quietly as if nothing happened:
- A mutating command refuses with exit 23. The message says how and when the lease ended (for example "preempted by bob"), and who holds the target now, if anyone.
- A read runs and prints the same message as a warning on stderr. That
includes a read on a target someone else holds now (for example after you
were preempted): it still runs, with a
warning:naming the new holder. - A token that matches nothing on that target (it never named a lease there) warns that the target is not leased and runs. A holder legitimately keeps its token while it touches a different, unleased target.
The record of why a token stopped working is kept in lease-history.jsonl
(the last 50 entries).
Where it lives¶
On the target's default host, under ~/.local/share/panio/<target>/:
lease.json (the record, owner-only), lease-history.jsonl, and lease.lock
(held briefly while the record is read or changed). In the lab file the only
setting is lease_required.
PANIO_LEASE is forwarded to the control host on stdin like AMT_PASSWORD,
so it never appears on a remote command line. For interactive commands
(serial connect, adb shell) stdin is your terminal, so the token
travels in a short-lived owner-only file named by a hidden --lease-file
option, which the remote panio reads and deletes. You do not use that option
yourself.
The check runs once, before the command, on the host where the target's default host is local: the control host after dispatch, or this machine when every channel and the default host are local.
Limits¶
- Single-host targets only. A target whose channels sit on more than one
host cannot be leased;
acquirenames the channels and hosts. Cross-host leasing is deferred (see distributed control). - Not a security boundary. A lease prevents accidents between cooperating users and agents. Anyone can preempt, and anyone who can read the control host's lease file can read the token. A process that talks to a daemon's HTTP API directly bypasses panio and the lease.
- Windows. Cleanup ends a registered process by pid. The Windows
serial connectandadb shellrun their tool as a child of panio inside a job object that kills it when panio ends, so the cleanup also frees the port. Processes the tool itself starts (adb's background server) leave the job and are not killed. - The block binds this version. An older panio that does not know about
cleanup blocks sees a lease held by
cleanup-blocked (was ...)with a token nobody has, so it refuses other callers, but its--preemptstill overrides it. Direct helper or daemon access bypasses it, as it does a lease. - Not a calendar. There are no future bookings and no queue; waiting is
--wait, which polls. - Whole target. There is no per-channel lease. Agents that want to share one board share one token.