The protoagent command
protoagent is the terminal control plane for a protoAgent runtime — install, run, and manage an instance without touching the console. It's the discoverable front door that replaces the bare python -m server <subcommand> invocation (ADR 0075 — added in a follow-up).
Chatting with an agent is a separate job — that's what
proto(the A2A terminal client) is for.protoagentruns and manages the runtime;prototalks to it. They meet at the wire (A2A / ACP), not in one binary.
Install
uv tool install protolabs-agent # or: pipx install protolabs-agent
protoagent --help # the command is `protoagent` (install name differs)Upgrade with uv tool upgrade protolabs-agent (or pipx upgrade protolabs-agent).
A source checkout doesn't have the protoagent command: uv doesn't install the project itself into a checkout's environment ([tool.uv] package = false), so uv run protoagent fails with "Failed to spawn". Run the same subcommands through the server module instead:
uv run python -m server fleet --help # the same as `protoagent fleet --help`python -m server <subcommand> keeps working — both front doors route through the same dispatcher (server/cli.py::dispatch), so they can never drift.
Commands
protoagent --helpLifecycle
| Command | What it does |
|---|---|
protoagent serve [--port N] | Run the server in the foreground (identical to python -m server). |
protoagent up [--port N] [--host H] | Start the server detached (background), boot-watch the port, and record a pidfile at the instance root. |
protoagent down | Stop the server started by up (SIGTERM, then SIGKILL after ~8s). Refuses to kill a server it didn't launch. |
protoagent status | Report whether this instance's server is running — port, pid, version. Exit code 0 = running, 3 = stopped. |
protoagent setup | Complete headless setup for the live config (ADR 0010) — validates the model endpoint/key and marks setup complete. |
up / down / status act on this instance (scoped by PROTOAGENT_INSTANCE / PROTOAGENT_HOME). To manage the multi-agent fleet, use protoagent fleet.
Management
Each forwards to the same core the console REST API calls, and acts on disk/DBs then exits:
| Command | What it does | ADR |
|---|---|---|
protoagent plugin install <git-url> · list · update · uninstall · sync | Manage drop-in plugins (pinned in plugins.lock). | 0027 |
protoagent workspace new · ls · run · rm | Named, isolated agents on one host. | 0041 |
protoagent fleet ls · up · down · new · rm · rename · remote add|edit|rm · pair · order | Inspect, run and manage fleet member agents — live from the running hub when one answers, from this instance's fleet.json (through the ops/ layer) otherwise (see below). --json on each. | 0042 · 0075 |
protoagent fleet --all | The hub tree: every hub on this box (heartbeats, instance roots with a fleet, listeners by port) probed for its version and member counts, plus peers found on the network. --offline skips the scan and the probes. Not a member command: it never reads one hub's fleet. | 0042 |
protoagent pair | Print a one-time code another agent's hub claims to pair with the protoAgent running on this machine — the headless half of agent pairing (see below). --json for scripts. | 0113 |
protoagent skills ls · promote <name> | Inspect and curate the SKILL.md library. | 0041 |
protoagent config explain · get · set key=value … | Explain the config cascade; print config.yaml; write dotted keys (JSON-typed) to disk. | 0047 · 0075 |
protoagent knowledge ingest <url|file> | Fetch/extract a source and index it into this instance's knowledge base. | 0075 |
protoagent operations | List the operations on the shared ops layer — name, read/write, one-line summary. | 0075 |
protoagent agent export [-o PATH] [--dry-run] | Write this agent's secret-free snapshot zip — the declarative recipe (SOUL, stripped config, plugin SHA pins, skills). Works on a stopped agent. | 0091 |
protoagent agent import <zip> [--name N] [--dry-run] [--yes] | Stand up a fresh agent from a snapshot. Prints the plan (plugins it will install and run, capabilities it grants) and refuses to apply without --yes. | 0091 |
protoagent runtime use <rt> · list | Select the agent runtime. native (LangGraph) is the supported value; the acp:* runtimes are deprecated — hand coding jobs to an acp delegate instead. | 0033 |
protoagent hermes | Deprecated (#2633) — the Hermes preset still works for existing installs but is no longer offered. Hand work to an external agent with ACP delegates instead. | 0033 |
The fleet deck: protoagent fleet with no arguments
Bare protoagent fleet (or protoagent top) opens an interactive terminal over the running hub — the fleet deck (a task-oriented walkthrough: The fleet deck). The roster shows every member with the console's presence words (host, online, remote, stopped, unreachable), version skew, spend over the last 24 h, and the hub's runtime warnings as a banner. Keys: enter (or c) talk to the member, i member detail, w the work feed, n new member, R rename, d delete, a add a remote, e edit a remote, J/K move a row (with no filter active), H every hub on the box, F5 refresh, s start, x stop, r restart, l follow logs, o open the member in the browser console, / filter, ? help, q quit (members keep running). The footer lists only the keys that apply to the selected row. Member detail shows runtime status (model, identity, warnings), a following tail of the member's bounded, redacted log ring, the session inventory, and the telemetry rollup — each pane degrades on its own if that read fails.
Talking to a member. enter (or c) on an online member opens a conversation. The transcript streams your messages and the member's answers; the WORK pane lists every tool call of the current turn as it happens — a subagent's own calls nested under its task card — with args, result, and duration; enter on a card shows the full args and result. Thinking folds behind a one-line count (ctrl+z unfolds). esc cancels a running turn (then backs out); ctrl+n starts a new session; ctrl+s lists the member's console sessions and replays one, tool cards included. Sessions use the console's own id shape, so a conversation started here is waiting in the browser and vice versa. A stream that goes silent for 45 s is checked against the member's durable task and finalized from it only if the server already finished — never fabricated.
Acting on a turn. When the member parks on a question, a form, or an approval, the status line says so; enter on the empty composer (or ctrl+r) opens it — a plain question also takes whatever you type as the answer. Approvals are a / d; a form is a stepped wizard (ctrl+→ / ctrl+←, ctrl+s submits) with the console's own rules for required fields, choices, and conditional fields; ctrl+d dismisses a request the way the console does, so the turn never stays parked forever. Typing while the member is working STEERS the running turn: the message queues and folds in at the member's next model call; up on the empty composer pulls the newest queued message back to edit, and anything the turn ended without reading is re-sent as a fresh turn. ctrl+x cancels the selected running task card — that one delegation, not the turn. While a conversation is open the session is attended: a scheduled or inbox turn in it parks on a question instead of auto-answering, and the deck attaches to it as it runs (a turn already running when you open a session is attached too); when the member says the turn is operator-controllable, the composer interjects into it. esc on an attached turn detaches and backs out — it never cancels somebody else's turn. (ctrl+x is the composer's cut while the composer has focus; tab to the WORK pane first.)
Managing members. n creates a member: a name, an archetype from the hub's catalog (the built-in Basic and every installed archetype, with what each installs and needs), "inherit the hub's model connections and credentials" (on by default — the member boots ready to chat) and "start after create". R renames the selected member's display name only (letters, digits, - and _, like every member name) — its id, URL slug and data never change, so open windows survive. d deletes it: the member is stopped first, you type its name to confirm, and purging its workspace and data is a separate checkbox — both irreversible, and the deck says so. If the hub reports that the member stopped but its workspace survived (a 409), the deck says so and asks you to repeat the delete; that is a partial result, not a failure. a registers a remote protoAgent (name, URL, an optional bearer typed masked, sent once and never shown again); e edits one in place (blank bearer keeps the stored one, "clear" forgets it — one or the other, not both); d on a remote only unregisters it. An unreachable remote reads unreachable, never stopped. J/K move the selected row and persist the order on the hub as a complete permutation of member ids. The status line shows the hub's warm-agent cap (fleet.warm.max; read-only here — change it in the hub's settings).
Every hub on the box. H (or protoagent fleet --all) lists the hubs this machine runs — the desktop app's, ~/.protoagent, each scoped instance under it — and peers found on this box's ports and the tailnet (and the LAN when fleet.discovery.mdns is on): one row per hub with its state, how it was launched (desktop app, protoagent up, foreground), port, version and member counts, then its instance root. Running hubs come from the .instances/ heartbeats under every known box root; stopped ones from every instance root that carries a workspaces/fleet.json (a member's root is never a hub row). This shell's own instance is one input among these, never the only one — what a shell "sees" is not what it inherited. A running hub is probed with its own fleet token: one that answers but refuses every credential reads unauthorized (pass --token), one that does not answer unreachable. A peer found on the network is never sent a credential — not --token, not the env — its name and url are its own claim; to open one with a bearer, name it: --hub <url> --token. enter attaches the deck to that hub — the roster, feed and conversations then belong to its fleet; u on a stopped hub runs protoagent up for that instance root — on the port it last used when that is free and no member of any instance records it, else the first such port from 7870 to 7910 — and attaches once its port answers. Stopping a hub is not a deck action (protoagent down in that instance). Two hubs claiming one port both say so — ports are box-global.
The work feed. w lists what the fleet is doing — every member's server-fired turns, tool calls, room replies, spend, and parked questions, folded from the members' event buses into one time-ordered feed. f filters, p pauses, enter opens the member's conversation at that row's session. The roster's TURN column follows the same events, and the deck rings the bell when a member newly needs you.
Credentials. The deck opens a hub exactly as the verbs do (below): --token / PROTOAGENT_HUB_TOKEN first, then the hub's own fleet service token, then A2A_AUTH_TOKEN, then no credential — local tokens go to loopback hubs only, nothing goes off-box in cleartext without --insecure-http, and a peer the network reported gets no credential at all. Conversations reuse the credential that opened the hub; a remote member's bearer stays on the hub and is attached by its proxy. Nothing is ever printed.
Offline. The deck follows the same live/offline rule as the verbs below: with no hub answering it shows this instance's fleet.json badged offline, and only start/stop are available — H still lists every hub on the box, and u brings one up. --all opens the tree even when a hub answered but refused this shell's credentials; the roster beneath it is then the disk view, badged with the refusal, and start/stop are not offered there — attach to the hub (enter on its row) or pass --token. A fleet --all --json run never loads Textual.
From the desktop app. The desktop sidecar bundles the deck, so protoagent-server fleet opens it from the frozen binary (a desktop-only install has no other protoagent on the box, so that is how the deck is reached there). Textual is imported only when the deck opens, so --help and the non-interactive verbs stay fast; a build without it prints a one-line hint and exits 2.
Screens and keys, at a glance.
| Screen | Keys |
|---|---|
| Roster | enter/c talk · i detail · w work feed · H hubs · n new · R rename · d delete · a add remote · e edit remote · J/K move · s start · x stop · r restart · l logs · o open in console · / filter · F5 refresh · ? help · q quit |
| Conversation | type + enter send (steers a running turn) · enter on the empty composer / ctrl+r answer what the turn parked on · esc cancel or detach, then back · ctrl+n new session · ctrl+s sessions · ctrl+z unfold thinking · tab to the WORK pane · ctrl+x cancel the selected delegation · up edit the newest queued message |
| Question / form / approval | a approve · d deny · ctrl+→/ctrl+← form steps · ctrl+s submit · ctrl+d dismiss · esc back |
| Hubs | enter attach · u bring up · r rediscover · esc back |
| Work feed | f filter · p pause · enter open that session · esc back |
fleet talks to the running hub
fleet ls / up / down look for a running hub before they read anything from disk, because the hub is the only source of live truth about the fleet: its GET /api/fleet is what the console shows, and its control plane is what owns the member processes. A shell that read fleet.json from its own instance root used to report a fleet of one beside the desktop app's hub (which lives under a different PROTOAGENT_HOME) — and called the CLI's own pid a running server.
How a hub is found, in order: this instance's server.pid (from protoagent up), the .instances/<pid>.json heartbeats every server writes under its box root — scanned across every box root this machine uses, including the desktop app's — then :7870. How it is opened: --token / PROTOAGENT_HUB_TOKEN, then the hub's own fleet service token (<instance root>/workspaces/.fleet-token, ADR 0089), then A2A_AUTH_TOKEN, then no credential. Tokens are never printed.
protoagent fleet ls # live · http://127.0.0.1:7870 · protoagent v0.165.0 · via heartbeat
protoagent fleet ls --json | jq '.agents[] | select(.running) | .name'
protoagent fleet up protoEngineer # POST /api/fleet/protoEngineer/start — the hub owns the process
protoagent fleet down # POST /api/fleet/down
protoagent fleet new scout --archetype pm # from the hub's catalog (live); --bundle <git-url> works offline too
protoagent fleet new blank --no-start --no-inherit
protoagent fleet rename scout scout-prime # display name only (letters, digits, - and _); the id and slug stay
protoagent fleet rm scout --purge # asks you to type the name; --yes off a terminal
protoagent fleet remote add ava https://ava.tail:7870 --bearer-stdin < token.txt
protoagent fleet remote edit ava --url https://ava2.tail:7870 --clear-bearer
protoagent fleet pair http://100.64.0.5:7870 ABCDE-12345 # claim a code minted on the remote; the hub stores the token (ADR 0113)
protoagent fleet pair http://100.64.0.5:7870 # no code in argv: prompted (no echo), or --code-stdin from a pipe
protoagent fleet pair http://192.168.1.20:7870 --insecure-http # plain http off loopback/tailnet: refused unless you opt in (ADR 0113 D10)
protoagent fleet order protoagent scout-1a2b r-ava # every member id, in the order wanted
protoagent fleet --all # every hub on this box (and peers), probed; --json for scripts
protoagent fleet ls --hub https://ava.tail:7870 --token "$TOKEN" # a hub elsewhere (an explicit --hub that fails is an error, not a fallback)
protoagent fleet ls --hub http://100.119.239.8:7870 --token "$TOKEN" --insecure-http # a tailnet peer: http, but encrypted underneath
protoagent fleet ls --offline # this instance's fleet.json, no probeWhen nothing answers the output is badged offline · reading <fleet.json> and up / down act through the supervisor on disk — the right thing only when nothing is running. A hub that answered but could not be opened (rejected credential, timeout, 5xx, or only a fleet member answering) is an error, not a fallback: driving processes from disk beside a running hub is exactly the two-hubs bug. A member's 401 is reported as that member's credential problem, never as the hub's.
This box's fleet service tokens and A2A_AUTH_TOKEN are sent to loopback hubs only. A --hub on another host gets --token / PROTOAGENT_HUB_TOKEN and nothing else, so a stray URL can never harvest local credentials — and a credential is never sent in cleartext off-box: a non-loopback http:// hub is refused unless you pass --insecure-http for a link you know is encrypted underneath (a tailnet). Redirects are never followed. A fleet member is refused as a hub even when named explicitly: it is a fleet of itself, and lifecycle belongs to its hub. --json emits per-member result rows of one shape ({name, ok, …}) plus mode and hub.
Pairing another agent with this one: protoagent pair
A hub adds a remote protoAgent by pairing with it rather than being handed its token (ADR 0113). The remote shows a one-time code, and the hub claims it. The claim mints a token for that hub alone, which the remote lists under Settings ▸ Devices and can revoke without touching anything else.
On a machine with a console, the code comes from Settings ▸ Devices ▸ Pair an agent. On a headless box (docker, a server), run protoagent pair beside the running instance. It finds the instance the same way fleet finds a hub, and prints the code with a claim command for each address the instance is reachable on:
$ protoagent pair
Pairing code for ava: K7QM2-XPA4F (expires in 4:59)
On the hub, enter it under Settings ▸ Agents ▸ Pair…, or run:
protoagent fleet pair http://100.64.1.2:7870 K7QM2-XPA4F (tailnet)
protoagent fleet pair http://192.168.1.20:7870 K7QM2-XPA4F (lan)The code is ten characters, case-insensitive (dashes optional), works once, and expires after five minutes. Five wrong guesses at the instance cancel every pending code. An instance bound to loopback can't be paired, because nothing else can reach it: protoagent pair says so, lists the addresses it could use, and points at the fix. The fix is a reachable bind with an auth token (--host 0.0.0.0, or Allow devices on my network in Settings ▸ Devices), never an open instance. A fleet member listens on loopback behind its hub, so pair the hub instead.
Point at a local model
protoagent model points protoAgent at any OpenAI-compatible endpoint — the gateway is the default, not a lock-in, so a local Ollama / LM Studio / llama.cpp / vLLM server is one line:
protoagent model discover # probe :11434 / :1234 / :8080
protoagent model use --base-url http://127.0.0.1:8080/v1 --model qwen2.5
protoagent upmodel use writes the endpoint + model to your live config (a local endpoint ignores the key; a placeholder is set so the client constructs — use --key or secrets.yaml for a real gateway key). This one-liner is also the copy-paste target for HuggingFace's "Use this model" local-app snippet — a HF model card hands the model id straight to it.
Pick a tool-calling model. protoAgent drives tools on every turn, so the local model must support tool/function calling (e.g. llama3.2, qwen2.5) — point it at one that doesn't and the turn fails at the endpoint with does not support tools.
Examples
# Stand up an instance and check it
protoagent up --port 7870
protoagent status
protoagent config explain
# Point at a local LLM
protoagent model use --base-url http://127.0.0.1:11434/v1 --model llama3.2
# Install a plugin, then reload isn't needed for a fresh boot
protoagent plugin install https://github.com/protoLabsAI/careercoach-plugin
# Edit config headless, ingest a doc, list what operations exist
protoagent config set fleet.mdns.enabled=false
protoagent knowledge ingest https://example.com/post --domain research
protoagent operations
# Stop it
protoagent downExporting an agent
protoagent agent export --dry-run # review only: what is stripped, what the target must supply
protoagent agent export -o ~/snapshots/ # write the zipThe snapshot is a recipe, not a backup: SOUL, secret-stripped config, plugins.lock SHA pins, MCP server definitions and SKILL.md dirs. No runtime history, no credentials, no plugin code — importing yields a fresh agent, not a resumed one.
Credentials never travel. What the target must re-supply is listed by name in a required_secrets inventory, and every zip carries a REVIEW.md spelling out what was stripped and what still needs re-pointing. Two things it distinguishes, because the response differs:
- Credential-shaped text found in free text (a token pasted into
SOUL.mdor a config field) — scrubbed from the artifact, but still in the source agent. Treat it as exposed and rotate it. - Machine-local paths — scrubbed because they carry your username. Nothing to rotate; re-point them after import.
Redaction of free text is a safety net, not a guarantee — read the artifact before you publish it.
The same export is in the console at Settings ▸ Agent ▸ Snapshot, which shows the review first and downloads the zip on a second click.
Importing an agent
protoagent agent import vera-snapshot.zip --dry-run # the plan; changes nothing
protoagent agent import vera-snapshot.zip --name vera-2 --yes \
--secret providers.gateway=sk-…Importing runs code. A snapshot names plugin repos, and applying it clones them and enables them in-process — so import always prints its plan first (every URL, with unfamiliar sources flagged, plus the capabilities the config grants) and refuses to apply until you pass --yes. Read the plan; it is describing what is about to run on your machine.
The config applies verbatim, including capability settings like filesystem.allow_run and operator.allowed_dirs — those are part of the agent's definition, so they're shown in the plan rather than silently stripped. Its model connections travel in the provider-registry shape wherever the registry can express them, and a connection's key is named by the connection (providers.<id>); the retiring model.api_key keeps its name unless it is the gateway connection's key. --dry-run prints the names to supply (see Agent snapshots).
The new agent arrives incomplete until its credentials are supplied: none travel in a snapshot. Pass them with --secret NAME=VALUE (repeatable, written 0600 to the new agent only), or set them afterwards in that agent's Settings ▸ Secrets. Only credentials the source agent actually had are reported missing.
Roadmap
Later slices of ADR 0075 add a shared operation layer so every verb here has a matching MCP tool and REST endpoint. See the ADR for the plan.