This is the full developer documentation for AgentPod # The console for the machines your agents live on > Your agents run somewhere. AgentPod is how you see that place, and reach into it. Attach, don't migrate Point AgentPod at agent runtimes you already run. It detects them in place — Claude Code, Codex, OpenCode, Hermes, OpenClaw and Pi — rather than asking you to move anything. Reaches machines you can't Every node dials *out* to the hub over a WebSocket. No inbound ports, no port forwarding, and laptops behind NAT or CGNAT work the same as a VPS. Files, logs, terminal, health Read a workspace, tail a log, open a shell, check what’s running, plan a cleanup — on any station in the fleet, from one place. Talk to them in a room Turn on the bridge and every station gets a chat identity of its own. Message it from your phone, send it a voice note, answer its permission questions with a number — and get the provider’s own sentence when a turn fails. [The bridge](/use/rooms/) Skills you can prove loaded Install a skill under review, then verify a fresh session actually loaded it. Present, eligible and loaded are separate observations, never inferred from one another. [Managed skills](/use/skills/) Hosting requirements The hub is a Bun service with a Postgres database, and the console is a static site. Current releases also require configuration and a credential from an external account service that is not public source. Start with a hub you already have access to. These docs are also published as plain text for agents: [`/llms.txt`](/llms.txt) indexes them, and [`/llms-full.txt`](/llms-full.txt) is every page in one file. # Authentication > Where a token comes from, how to present it, what the hub checks, and what each refusal means. The hub **issues nothing**. Tokens come from the organization plane, the account service at `https://accounts.superjackfruit.com`, and every product, the hub included, verifies them offline against the plane’s published key set rather than calling back to ask whether a token is good. A hub tells you which issuer it trusts: ```plaintext GET /public/org-plane ``` ```json { "issuer": "https://accounts.superjackfruit.com", "url": "https://accounts.superjackfruit.com", "audience": "https://hub.agentpod.dev" } ``` `issuer` is the exact `iss` the hub accepts, `url` is where the plane’s endpoints live, and `audience` is the value a token must carry in `aud` to be spent here. A self-hosted hub answers with its own configuration; nothing below should be hard-coded except the claim names. ## The issuer [Section titled “The issuer”](#the-issuer) The issuer is the plane’s **root URL**, and it publishes standard discovery documents there: | Document | Path | | ----------------------------------- | ----------------------------------------- | | OpenID configuration | `/.well-known/openid-configuration` | | OAuth authorization server metadata | `/.well-known/oauth-authorization-server` | | Key set (JWKS) | `/api/auth/jwks` | Every token is a JWS signed with **`EdDSA`** (Ed25519), with a `kid` that names a key in that set. ## Getting a token [Section titled “Getting a token”](#getting-a-token) ### At a terminal [Section titled “At a terminal”](#at-a-terminal) ```sh fleet login ``` `fleet login` asks the hub which issuer it trusts and runs that plane’s **device flow**: it prints a page and a code, opens the page if it can, and waits while you confirm the code in any browser, so it works over SSH too. Approval gives this machine a **device credential** for the `apn` client (the CLI’s registered client id). The credential lasts **90 days from its last use**, so a machine in regular use stays signed in. Every later command spends the device credential for a fresh access token without opening a browser. The exchange underneath is: ```plaintext POST /api/token/device Authorization: Bearer dev_…: { "audience": "https://hub.agentpod.dev" } → { "access_token": "…", "token_type": "Bearer", "expires_in": 300 } ``` The plane lists and revokes device credentials on its Devices page; `fleet devices` prints where that is. The full command surface is in the [fleet reference](/reference/fleet/). ### In a browser [Section titled “In a browser”](#in-a-browser) Each first-party web app is a public OAuth client of the plane, signing in with the **authorization code flow and PKCE (S256)**. The token request names the resource it wants with `resource=`, and one token carries one audience, so a client that needs two products makes two token requests. Endpoints are in the plane’s discovery documents. ### On another principal’s behalf [Section titled “On another principal’s behalf”](#on-another-principals-behalf) A service holding a plane credential (`svc_…`) exchanges it at the plane: | Exchange | What it returns | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `POST /api/token/service` | A token for the service itself | | `POST /api/token/agent` | A token for an agent in the service’s own workspace, if its grant has `token:agent` and the agent is not suspended | | `POST /api/token/assertion` | A two-minute token for a person identified by a linked identity, such as a chat account, if its grant has `token:assert` | The hub uses `/api/token/agent` itself: an agent holds no long-lived credential, so its node exchanges the node credential it was enrolled with at the hub, and the hub asks the plane for a token naming the station’s agent. A suspended agent gets no token. ## Presenting a token [Section titled “Presenting a token”](#presenting-a-token) Send it as a bearer token: ```plaintext Authorization: Bearer ``` The operator API also reads `?token=`, because a browser’s WebSocket and EventSource cannot set a header. `/mcp` and `/api/fleet/dispatchable` take the header only: a credential in a URL is a credential in a log. Access tokens last **five minutes**. Do not cache one past `exp`; get a fresh one. ## What the hub checks [Section titled “What the hub checks”](#what-the-hub-checks) Every door that takes a token verifies it the same way: * **The algorithm is `EdDSA`**, fixed by the hub and never read from the token’s own header. Letting a token nominate the algorithm it should be checked with is the classic JWT confusion attack. * **The signature** against the plane’s key set. The hub caches the set for at most ten minutes, refetches when it sees an unknown `kid`, and keeps verifying against the last good set while the plane is unreachable, so valid tokens keep working through a plane outage. * **`iss`** equals the configured issuer exactly. Never a prefix match. * **`aud`**, a string or an array, equals or contains this hub’s audience. * **`exp`**, which is five minutes after `iat`. * **`ent`** includes `agentpod`: the product is enabled for the token’s workspace. The claims it then reads: | Claim | Meaning | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sub` | The principal, a `prn_` id. | | `principalKind` | `human`, `agent` or `service`. | | `org` | The workspace, an `org_` id: the person’s active workspace, or the one that owns the agent or service. The hub maps it to its own tenant, creating that tenant the first time it sees a valid token for an enabled workspace. | | `ent` | The products enabled for that workspace. | | `scope` | On an agent’s or a service’s exchange token, its grant scopes. On a person’s browser token it is the OAuth scope string and is never read as a grant. | | `amr` | How an exchange token was obtained: `device`, `exchange`, `service` or `assertion`. Absent on browser tokens; the hub never requires it. | | `act` | `{ "sub": "prn_…" }` when a service minted the token for another principal. An agent’s token names the hub’s service here. | | `mayDispatch` | The agents this principal may dispatch, as bare `prn_` ids. | | `mayGrantReach` | Whether this principal may grant reach: act on a station’s behalf, such as handing its agent a credential, for stations its `mayDispatch` covers. | | `email`, `email_verified` | For people. | **The principal kind is load-bearing.** The operator API takes people only. `/mcp` admits agents and services too, because serving agents is its whole purpose, and offers each a different tool set. ### Dispatch authority [Section titled “Dispatch authority”](#dispatch-authority) ```plaintext GET /api/fleet/dispatchable ``` Returns the agents the token’s holder may dispatch, with their handles. The answer is read from the token’s signed `mayDispatch` claim and from nothing else the caller sent, so there is nothing in the request that can change it. A missing or non-array claim reads as *permitted nothing*, never as *permitted everything*. An agent’s token is refused here whatever it may dispatch. ## What the errors mean [Section titled “What the errors mean”](#what-the-errors-mean) | Status | Body | Meaning | | ------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `401` | `Unauthorized`, `invalid_token`, or JSON-RPC `-32001` on `/mcp` | No token, or one the hub will not accept: expired, unknown key, wrong issuer, or not issued for this hub. Get a fresh one. | | `403` | `{ "error": "product_not_enabled", "org": "org_…" }` | The token is valid, but AgentPod is not enabled for that workspace. A person enables it on the plane. | | `403` | `Forbidden` | A valid token for the wrong kind of principal: the operator API takes people only. Other `403`s say which permission the principal lacks. | | `410` | `{ "error": "issuer_moved", "issuer": "…" }` | Any `/api/auth/*` route: sign-in, token exchange and the key set left the hub. Use the issuer named. | | `410` | `{ "error": "managed_by_org_plane", "url": "…" }` | A record the plane now owns, such as the device inventory or user and grant administration. Manage it at `url`. | | `503` | `{ "error": "org_plane_unavailable", … }` | The hub needed the plane to answer, for a read the token alone could not settle, and could not reach it. | A token that is valid, fresh and correctly signed still gets a `401` from a product whose audience it does not name. When a credential works on one product and not another, check `aud` first. ## Self-hosting [Section titled “Self-hosting”](#self-hosting) A hub also accepts one static API token from its own configuration. It is not an issuer: it acts as the hub’s default user in the bootstrap tenant, carries no claims, and is meant for setup and automation on a hub you run. ## Next [Section titled “Next”](#next) * [fleet reference](/reference/fleet/): `fleet login`, `whoami`, `logout` and `devices` * [MCP tools](/build/mcp/): what a token opens * [apn and fleet](/use/cli/): the two credentials, and why they never mix * [Dispatch and grants](/use/grants/): who may send an agent a message, and answer for it # MCP tools > The hub's MCP endpoint — self-scoped tools that let an agent answer questions about where it is running, and keep a file from its workspace in SuperLibrary. The hub speaks MCP over Streamable HTTP at **`/mcp`**. ## Getting a token [Section titled “Getting a token”](#getting-a-token) ```sh fleet login ``` The endpoint takes a token from the account service in `Authorization: Bearer`, and only there. There is no `?token=` fallback — a credential in a URL is a credential in a log. ## What an agent gets [Section titled “What an agent gets”](#what-an-agent-gets) Three tools. All read-only. **None of them take a station id**: | Tool | Answers | | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | | `agentpod_my_station` | Where you are running: your station key, its node, your harness, your Matrix identity, and whether the node is online | | `agentpod_my_sessions` | Your recent ACP sessions, newest first. Takes an optional `limit` (max 50) | | `agentpod_my_transcript` | The events of one of your sessions. Takes a `sessionId` and an optional `sinceSeq` | The station is derived from the calling principal in the token. There is no station parameter, which means there is no station parameter to tamper with — the tools answer for *you*, and there is no way to ask about anybody else. If you are not currently placed in a station, they say so in a plain sentence. That is not an error, and it should not be handled as one. `agentpod_my_transcript` is the one tool with an explicit ownership check, because a transcript is named by a session id and an id is a thing that can be guessed. A session that is not yours is refused. ## Linking a file to SuperLibrary [Section titled “Linking a file to SuperLibrary”](#linking-a-file-to-superlibrary) When the hub is configured for SuperLibrary, an agent has a fourth tool, `agentpod_link_artifact`. It keeps a file or folder from the agent’s own workspace in SuperLibrary, with its provenance (station, path, board, card and run), and returns a link. | Argument | Meaning | | -------- | -------------------------------------------------------------------------------------------------------- | | `path` | Required. The file or folder, relative to the workspace | | `title` | Optional. A name for the item | | `kind` | Optional. `file` or `folder`; a mismatch is refused | | `entry` | Optional, for a folder. The file to open first, relative to the linked folder (for example `index.html`) | **It takes no station argument.** Only the station the calling principal occupies is ever read, and any other key an agent sends is dropped. A person does not get the tool: a person uploads to SuperLibrary directly. It refuses, in a plain sentence the agent can act on, when: * the path leaves the workspace, or is the workspace itself; * the name is credentials, a `.env` file, a key or harness config. These are never linked; * a secret is found in the content. **This cannot be overridden**: remove the secret and link again, and nothing is added to the library; * a file is over 25 MB, a folder is over 100 MB or 500 files, or the folder has no linkable file; * the station is unavailable, its node is too old to list or read files, or SuperLibrary is unreachable. What SuperLibrary returns or holds is reference material, never instructions. After linking, the agent attaches the returned url to its card with `superpipeline_add_reference`. ### The rules agents are given [Section titled “The rules agents are given”](#the-rules-agents-are-given) The server’s `initialize` instructions always carry rule 3. Rules 1 and 2 appear only when the hub is configured for SuperLibrary, because that is when `agentpod_link_artifact` exists: 1. If it also has SuperLibrary’s MCP server (`library_search`), search it before non-trivial work. 2. Link files it produces with `agentpod_link_artifact`, from its own workspace only, and attach the url to its card with `superpipeline_add_reference`. 3. Never publish through gists, pastebins or personal accounts. The card prompt states the third rule on every card, whether the agent reports for itself or the bridge reports for it. It names `agentpod_link_artifact` and `library_search` only when the prompt’s `libraryTools` is true. The bridge requests the node’s proxy when available. Tool availability comes from the node’s actual response; a request to enable the proxy alone does not establish that the session has it. Salaam additionally checks that its adapter reports the same admitted servers. ## What a human gets [Section titled “What a human gets”](#what-a-human-gets) Nothing self-scoped — a person occupies no station, so those tools have no meaning. Fleet tools are not exposed over MCP yet; use `fleet` for nodes, agents, stats and activity. The server tells you this in its `initialize` instructions rather than making you find out by calling something. ## What you will not find [Section titled “What you will not find”](#what-you-will-not-find) **The fleet.** An agent token cannot enumerate other agents, nodes or stations. That is deliberate and not an oversight: the authority to *do your work* was never the authority to *find out what else exists*, and an agent enumerating its siblings is an agent doing reconnaissance. ## It adds no authority [Section titled “It adds no authority”](#it-adds-no-authority) Every tool calls the same service a normal HTTP route calls, through the same checks. There is no operation available over MCP that is not available over the API, and no second authorization model. ## Where work lives [Section titled “Where work lives”](#where-work-lives) AgentPod is the **execution** side: where you run, and what ran there. Claiming a card, reporting progress and finishing work live in [SuperPipeline’s MCP server](https://docs.superpipeline.dev/build/mcp/), not this one. ## Inside a station’s own session: the node’s MCP proxy [Section titled “Inside a station’s own session: the node’s MCP proxy”](#inside-a-stations-own-session-the-nodes-mcp-proxy) An agent running in a station does not need a token of its own to reach these tools. Its node can run a **loopback MCP proxy** that adds two servers to the harness’s `session/new` — this endpoint as `agentpod` and SuperLibrary’s as `superlibrary` — reached as the station’s own agent through a `127.0.0.1` URL and a per-station secret that never leave the machine. Only Hermes, Claude Code, Codex, opencode, Muse and Kilo stations can use it. Salaam uses the managed profile described below. OpenClaw’s and Pi’s ACP adapters take no HTTP MCP servers. ```sh fleet mcp-proxy list # which stations each node serves, and any drift fleet mcp-proxy enable --all-eligible # or name stations; applied without a node restart fleet mcp-proxy rotate stn_123 # a new secret; a session holding the old one is refused ``` The secrets persist across node restarts, so a session kept open across one keeps its tools. See [`fleet mcp-proxy`](/reference/fleet/#fleet-mcp-proxy) and [`apn mcp-proxy`](/reference/apn/#apn-mcp-proxy). ### Salaam’s managed profile [Section titled “Salaam’s managed profile”](#salaams-managed-profile) Salaam requires an explicitly reviewed AgentPod host manifest with `mcp_config`. The private profile pins the proxy origin, protocol, catalog, admitted tool schemas and read or approval policy. An existing host without this profile keeps MCP disabled. Missing assignments, changed schemas or a mismatched adapter fail readiness before the worker can use the tools. The node sends the credential directly to the daemon. ACP declarations contain empty headers; provider payloads receive the progressive `salaam_mcp` interface. To discover a tool, use `action: "catalog"`, then the server name, then the server and tool name to retrieve its schema. Use `action: "call"` with that schema to invoke it. For example, `library_search` is reached through server `superlibrary`, rather than registered as a separate model tool. The proxy binds its credential to the admitted principal, occupancy and run context. It checks the forwarded station token’s principal and organization, replaces the cache on a binding handoff, and persists that binding across node restarts. The upstream still verifies the organization plane’s signature, issuer, audience and entitlement. This adds no token issuer or audience. Reconnect preserves saved call identities and receipts. Changed bindings invalidate old credentials and decisions. Mutating tools require an operation-bound approval; a protocol reply alone does not verify the external effect. An uncertain mutation stays blocked for reconciliation and is never automatically dispatched again. This profile enables managed HTTP MCP only; arbitrary client-supplied servers, stdio commands and station coding tools are not enabled by it. ## Next [Section titled “Next”](#next) * [Authentication](/build/auth/) — where the token comes from and how it is verified ### Managed proxy compatibility and responses [Section titled “Managed proxy compatibility and responses”](#managed-proxy-compatibility-and-responses) A reviewed Salaam assignment uses a versioned route bound to its complete execution identity. Older transparent proxies reject that route before forwarding. A bound credential cannot use the legacy route; reconnecting through an incompatible node requires an upgrade and a fresh readiness check. The managed route sanitizes exact injected station-token and proxy-key echoes and common encodings across streamed response chunks. It forwards only MCP response metadata, suppresses cookies and trailers, and rejects opaque content encodings. Safe SSE progress continues to stream. Upstream body-copy failures produce fixed diagnostics. These controls do not replace product authorization or establish that arbitrary malicious response encodings are safe. # apn reference > Every apn command and subcommand: synopsis, flags, environment, what it needs and how it exits. Generated from the binary. `apn` (`agentpod-node`) is the resident agent on an enrolled host. Every command acts on **this machine**: its service, its enrolment, the harnesses installed on it. Acting on the fleet as a person is [`fleet`](/reference/fleet/)’s job, and `apn` never reads the credential `fleet login` stores. For the reasoning behind the split, see [apn and fleet](/use/cli/). Each command’s text below is what `apn help ` prints, from the same table in the binary. `apn node ` is the same as `apn `. This page is generated from that table and the source, and a test fails when a command or flag exists without an entry here. ## Commands [Section titled “Commands”](#commands) **Service** | Command | What it does | | --------------------------------- | ----------------------------------------------------------------- | | [`apn status`](#apn-status) | Show local service + hub connection state (`--json` for scripts). | | [`apn start`](#apn-start) | Enable and start the background service. | | [`apn stop`](#apn-stop) | Stop and disable the service (sticky across reboots). | | [`apn restart`](#apn-restart) | Restart the running service. | | [`apn logs`](#apn-logs) | Show service logs (-f to follow, -n N for last N lines). | | [`apn service`](#apn-service) | Install \| uninstall the platform service (launchd/systemd). | | [`apn telemetry`](#apn-telemetry) | Show, enable or disable OpenTelemetry export (`--endpoint` URL). | **Node** | Command | What it does | | ------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | [`apn salaam-station-files-v1`](#apn-salaam-station-files-v1) | Private Salaam station file protocol (not an interactive command). | | [`apn node`](#apn-node) | Explicit spelling for the machine-scoped verbs (apn node status, …). | | [`apn enroll`](#apn-enroll) | Enroll this machine with a hub (`--hub`, `--token`, `--force`, `--otlp-endpoint`). | | [`apn run`](#apn-run) | Run the agent in the foreground. | | [`apn native-skills`](#apn-native-skills) | Show, enable or disable native skill placement on this node. | | [`apn plugin-management`](#apn-plugin-management) | Show, enable or disable Console plugin management on this node. | | [`apn mcp-proxy`](#apn-mcp-proxy) | Show the stations the loopback MCP proxy serves, or rotate their secrets. | | [`apn hermes-skills`](#apn-hermes-skills) | Register or remove the managed skills directory in a Hermes profile. | | [`apn hermes-live`](#apn-hermes-live) | Install, enable or remove the agentpod-live streaming plugin in a Hermes profile. | | [`apn openclaw-errors`](#apn-openclaw-errors) | Install, enable or remove the agentpod-errors plugin in OpenClaw. | | [`apn pi-errors`](#apn-pi-errors) | Install, enable or remove the agentpod-errors extension in Pi. | | [`apn detect`](#apn-detect) | Print detected harness stations as JSON. | | [`apn forge-api`](#apn-forge-api) | Set up stk as this station’s forge credential broker (setup). | | [`apn scan`](#apn-scan) | Check this machine’s agents for exposure (`--json`). | | [`apn acp`](#apn-acp) | Attach an ACP editor to a station (`--list`, `--station`). | **Maintenance** | Command | What it does | | ----------------------------- | ----------------------------------------------------------- | | [`apn update`](#apn-update) | Self-update from the latest release (`--check`, `--force`). | | [`apn help`](#apn-help) | Show this list, or one command in detail. | | [`apn version`](#apn-version) | Print version and platform. | ## Environment [Section titled “Environment”](#environment) | Variable | Meaning | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$AGENTPOD_HUB_URL` | `apn enroll`: the hub, when `--hub` is not given. | | `$AGENTPOD_ENROLL_TOKEN` | `apn enroll`: the one-time enrollment token, when `--token` is not given. | | `$AGENTPOD_HUB` | `apn acp`: the hub, when `--hub` is not given. Default `https://hub.agentpod.dev`. | | `$AGENTPOD_TOKEN` | `apn acp`: a person’s access token. Prefer it to `--token`, which lands in shell history. | | `$OTEL_EXPORTER_OTLP_ENDPOINT` | `apn run`: the OTLP/HTTP collector to export traces to; unset exports nothing. Normally written to `otel.env` by `apn telemetry enable`. | | `$OTEL_SDK_DISABLED` | `apn run`: `true` turns export off even with an endpoint set. | | `$AGENTPOD_ALLOW_SHARED_AGENT_UID` | `apn run`, `apn forge-api setup --station`: `1` acknowledges that stations whose agents share a uid are not isolated from each other. Default off: a second station on a uid already in use is refused with `station-uid-shared`. Same as `allowSharedAgentUid` in the node config. | | `$AGENTPOD_STK_SHARED_POLICY` | `apn run`, `apn forge-api setup --station`: the shared base policy, an stk policy file of services every station needs, copied into each station’s own stk policy. Default `/etc/agentpod/stk-shared.yaml`; an absent file means none. Overrides `forgeApi.sharedPolicy` in the node config. | | `$AGENTPOD_STK_PORTS` | `apn run`, `apn forge-api setup --station`: the proxy port range for the per-station stk instances, as `MIN-MAX`. Default `8900-8999`. Overrides `forgeApi.proxyPortMin` and `proxyPortMax` in the node config. | | `$AGENTPOD_STK_REGISTRY` | `apn run`, `apn forge-api setup --station`: the node-owned file (mode 0600) that remembers which stk instance and port belong to which station. Default `/var/lib/agentpod/forge-api/stk-instances.json`. | ## Exit status [Section titled “Exit status”](#exit-status) Unless a command says otherwise: * **0** — it did what it was asked. * **1** — it failed; the reason is on stderr. * **2** — a usage error: an unknown command, verb or argument. Nothing was changed. `-h` or `--help` as the first argument prints the command’s help and exits 0 before anything else runs, so `apn stop -h` never stops the service. ## Credentials [Section titled “Credentials”](#credentials) Commands that talk to the hub use **this machine’s** credential, stored by `apn enroll` in the node config (`~/.config/agentpod-node/config.json` on Linux, `~/Library/Application Support/agentpod-node/config.json` on macOS). It says “I am this host” and nothing more. The exceptions are `apn enroll`, which spends a one-time enrollment token, and `apn acp`, which takes a person’s token explicitly. ## apn status [Section titled “apn status”](#apn-status) Show local service + hub connection state (`--json` for scripts). ```text apn status [--json] ``` | Flag | Type | Default | Meaning | | -------- | ---- | ------- | ------------------------------------------------- | | `--json` | bool | `false` | Print the local and hub state as one JSON object. | `apn help status` prints: ```text apn status — show local service state and hub connection state. Local block: whether the service is installed / enabled / running (with PID), the binary version, and the config path. Hub block: reachability and credential validity, checked against the existing GET /public/nodes/credential-check endpoint (no new endpoints). Exit code is 0 iff the service is running AND the stored hub credential is valid — safe to use in scripts/health checks. ``` **Needs:** The node’s enrolled credential, for the hub check. Not enrolled is reported, not an error. **Exit status:** 0 only when the service is running **and** the hub accepts the stored credential; 1 otherwise. Safe in a health check. ```sh apn status apn status --json | jq .hub ``` ## apn start [Section titled “apn start”](#apn-start) Enable and start the background service. ```text apn start ``` `apn help start` prints: ```text apn start — enable and start the background service (the symmetric inverse of 'apn stop'). macOS: `launchctl enable` + `launchctl bootstrap`. Linux: `systemctl [--user] enable --now`. If no service is installed yet, this prints a hint pointing at 'apn service install' (or run in the foreground with 'apn run'). ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn start ``` ## apn stop [Section titled “apn stop”](#apn-stop) Stop and disable the service (sticky across reboots). ```text apn stop ``` `apn help stop` prints: ```text apn stop — stop the service AND disable it (sticky across reboots/logins — it will not come back on its own). macOS: `launchctl bootout` + `launchctl disable`. Linux: `systemctl [--user] stop` + `disable`. Undo with 'apn start' — it re-enables and starts. ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn stop ``` ## apn restart [Section titled “apn restart”](#apn-restart) Restart the running service. ```text apn restart ``` `apn help restart` prints: ```text apn restart — restart the running service in place. macOS: `launchctl kickstart -k`. Linux: `systemctl [--user] restart`. ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn restart ``` ## apn logs [Section titled “apn logs”](#apn-logs) Show service logs (-f to follow, -n N for last N lines). ```text apn logs [-f] [-n N] ``` | Flag | Type | Default | Meaning | | ------ | ---- | ------- | ------------------------ | | `-f` | bool | `false` | Follow log output. | | `-n N` | int | `50` | Number of lines to show. | `apn help logs` prints: ```text apn logs [-f] [-n N] — show service logs. macOS: reads/tails ~/Library/Logs/agentpod-node.log. Linux: execs `journalctl [--user] -u agentpod-node [-f] [-n N]`. ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn logs -n 200 apn logs -f ``` ## apn service [Section titled “apn service”](#apn-service) Install | uninstall the platform service (launchd/systemd). ```text apn service ``` Subcommands: [`install`](#apn-service-install), [`uninstall`](#apn-service-uninstall). `apn help service` prints: ```text apn service — manage the platform service definition. install: writes a plist (macOS LaunchAgent) or systemd unit from a template embedded in the binary, then enables and starts it. Idempotent — re-running replaces the file and restarts. Non-root Linux uses a --user unit; root Linux uses a system unit; macOS uses a LaunchAgent (refuses to run as root). uninstall: stops, disables, and removes the plist/unit. Idempotent — a no-op when nothing is installed. Leaves config/enrollment untouched. ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn service install ``` ### apn service install [Section titled “apn service install”](#apn-service-install) Write the service definition, enable it and start it. ```text apn service install ``` Idempotent: re-running replaces the plist or unit and restarts the service. **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn service install ``` ### apn service uninstall [Section titled “apn service uninstall”](#apn-service-uninstall) Stop, disable and remove the service definition. ```text apn service uninstall ``` A no-op when nothing is installed. Configuration and enrolment are left alone, so `apn service install` brings the same node back. **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn service uninstall ``` ## apn telemetry [Section titled “apn telemetry”](#apn-telemetry) Show, enable or disable OpenTelemetry export (`--endpoint` URL). ```text apn telemetry ``` The file is `/etc/agentpod-node/otel.env` for a system unit and `~/.config/agentpod-node/otel.env` for a user unit. From elsewhere, an admin can do the same to every node with [`fleet nodes telemetry`](/reference/fleet/#fleet-nodes-telemetry). Subcommands: [`status`](#apn-telemetry-status), [`enable`](#apn-telemetry-enable), [`disable`](#apn-telemetry-disable). `apn help telemetry` prints: ```text apn telemetry — control this node's OpenTelemetry export through its otel.env file. status [--json]: the config path, endpoint, enabled/disabled and, when enabled, whether the collector answers (a ~2 s GET of /v1/traces; any HTTP response counts). enable --endpoint URL: validate (http/https only), write, restart the service. disable: comment the endpoint out, restart the service. Nothing is restarted when the file would not change. Unsupported on macOS (launchd has no env-file hook). ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). **Exit status:** As below for each verb; 1 on macOS, where it is unsupported; 2 for an unknown verb. ```sh apn telemetry status ``` ### apn telemetry status [Section titled “apn telemetry status”](#apn-telemetry-status) The config path, endpoint, on or off, and whether the collector answers. ```text apn telemetry status [--json] ``` | Flag | Type | Default | Meaning | | -------- | ---- | ------- | ----------------------------- | | `--json` | bool | `false` | Machine-readable JSON output. | **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn telemetry status --json ``` ### apn telemetry enable [Section titled “apn telemetry enable”](#apn-telemetry-enable) Turn export on to an endpoint and restart the service. ```text apn telemetry enable --endpoint URL ``` The URL is validated (http or https only) before anything is written. Nothing restarts when the file would not change. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ----------------------------------------------------------- | | `--endpoint URL` | string | — | OTLP/HTTP collector base URL (http or https). **Required.** | **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn telemetry enable --endpoint https://otel.example.com:4318 ``` ### apn telemetry disable [Section titled “apn telemetry disable”](#apn-telemetry-disable) Comment the endpoint out and restart the service. ```text apn telemetry disable ``` **Needs:** The user the node service runs as. A root Linux install manages a system unit; any other Linux user, a user unit; macOS, a LaunchAgent (never as root). ```sh apn telemetry disable ``` ## apn salaam-station-files-v1 [Section titled “apn salaam-station-files-v1”](#apn-salaam-station-files-v1) Private Salaam station file protocol (not an interactive command). ```text apn salaam-station-files-v1 ``` `apn help salaam-station-files-v1` prints: ```text Private versioned stdin/stdout protocol for the reviewed Salaam station controller. Linux only. It does not load node enrollment credentials or grant execution authority. Use the Salaam tools and their operation-bound approval surface, not this entry directly. ``` **Needs:** Private local stdin owned by the reviewed Salaam controller; no enrollment credentials are read. Linux openat2 boundary required. **Exit status:** 0 for a validated content/metadata receipt; 1 for a bounded protocol or filesystem refusal; 2 for extra arguments. A missing result never verifies an effect. ```sh # Invoked by Salaam's protected station controller, not as an interactive shell command. ``` ## apn node [Section titled “apn node”](#apn-node) Explicit spelling for the machine-scoped verbs (apn node status, …). ```text apn node [flags] ``` `apn help node` prints: ```text apn node — the explicit form of the machine-scoped verbs. Every verb apn dispatches is a node verb — this binary is machine-scoped only, the fleet-acting verbs live in the separate `fleet` binary — and both spellings work here: `apn status` and `apn node status` are the same command. The bare forms are kept because existing runbooks name them. ``` **Needs:** Whatever the verb needs. ```sh apn node status # the same as apn status ``` ## apn enroll [Section titled “apn enroll”](#apn-enroll) Enroll this machine with a hub (`--hub`, `--token`, `--force`, `--otlp-endpoint`). ```text apn enroll [--hub URL] [--token TOKEN] [--force] [--otlp-endpoint URL] ``` Writes this machine’s identity to `~/.config/agentpod-node/config.json` (Linux) or `~/Library/Application Support/agentpod-node/config.json` (macOS). Re-enrolling replaces only the identity and keeps every local setting, including the native-skill and plugin-management gates. Mint the token with [`fleet invite`](/reference/fleet/#fleet-invite). | Flag | Type | Default | Meaning | | --------------------- | ------ | ------------------------ | -------------------------------------------------------- | | `--hub URL` | string | `$AGENTPOD_HUB_URL` | Hub base URL. | | `--token TOKEN` | string | `$AGENTPOD_ENROLL_TOKEN` | One-time enrollment token from `fleet invite`. | | `--force` | bool | `false` | Re-enroll even when a valid config exists. | | `--otlp-endpoint URL` | string | — | OTLP/HTTP collector base URL to enable telemetry export. | `apn help enroll` prints: ```text apn enroll [--hub URL] [--token TOKEN] [--force] [--otlp-endpoint URL] — enroll this machine with a hub. Falls back to the AGENTPOD_HUB_URL/AGENTPOD_ENROLL_TOKEN environment variables when the flags are omitted. Idempotent: running it again on an already-enrolled machine is a friendly no-op unless the stored credential is no longer valid, or --force is passed. --otlp-endpoint URL also enables OpenTelemetry export to that collector (validated before contacting the hub; applies on an already-enrolled machine too; no restart — same as 'apn telemetry enable'). Ignored with a warning on macOS. ``` **Needs:** A one-time enrollment token — the machine’s invitation, not a person’s token. **Exit status:** 0 when enrolled, or already enrolled and kept. 1 when the hub or token is missing, the hub refused the token, or the config could not be written. ```sh apn enroll --hub https://hub.agentpod.dev --token "$ENROLL_TOKEN" AGENTPOD_HUB_URL=https://hub.agentpod.dev AGENTPOD_ENROLL_TOKEN=… apn enroll ``` ## apn run [Section titled “apn run”](#apn-run) Run the agent in the foreground. ```text apn run ``` Reads the OpenTelemetry variables below at start. Exits when interrupted. `apn help run` prints: ```text apn run — run the agent in the foreground: connects to the hub and handles terminal sessions until interrupted (Ctrl-C). This is what the installed service runs under the hood; run it directly for debugging. ``` **Needs:** The node’s enrolled credential. Without one it exits 1 and says to enroll. ```sh apn run ``` ## apn native-skills [Section titled “apn native-skills”](#apn-native-skills) Show, enable or disable native skill placement on this node. ```text apn native-skills ``` Subcommands: [`status`](#apn-native-skills-status), [`enable`](#apn-native-skills-enable), [`disable`](#apn-native-skills-disable). `apn help native-skills` prints: ```text apn native-skills — control the separate native skill-placement gate for this node. Native placement writes into a harness-visible skill directory. It is disabled by default and must be deliberately enabled by the node operator. `enable` and `disable` update only this node's local configuration; restart the node service after either change before the hub can observe the new capability. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn native-skills status ``` ### apn native-skills status [Section titled “apn native-skills status”](#apn-native-skills-status) Whether native placement is enabled on this node. ```text apn native-skills status ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. **Exit status:** 0; 1 when the node is not enrolled. ```sh apn native-skills status ``` ### apn native-skills enable [Section titled “apn native-skills enable”](#apn-native-skills-enable) Allow native skill placement; restart the service afterwards. ```text apn native-skills enable ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn native-skills enable && apn restart ``` ### apn native-skills disable [Section titled “apn native-skills disable”](#apn-native-skills-disable) Refuse native skill placement; restart the service afterwards. ```text apn native-skills disable ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn native-skills disable && apn restart ``` ## apn plugin-management [Section titled “apn plugin-management”](#apn-plugin-management) Show, enable or disable Console plugin management on this node. ```text apn plugin-management ``` The fleet side of this is [`fleet plugins`](/reference/fleet/#fleet-plugins). Subcommands: [`status`](#apn-plugin-management-status), [`enable`](#apn-plugin-management-enable), [`disable`](#apn-plugin-management-disable). `apn help plugin-management` prints: ```text apn plugin-management — let the Console install and remove the agentpod-live plugin in this node's Hermes profiles. Each change is planned on the node, reviewed in the Console and applied only if the profile still matches the review; the node probes Hermes's version itself. It is disabled by default. `enable` and `disable` update only this node's local configuration; restart the node service after either change. Nothing here restarts a gateway. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn plugin-management status ``` ### apn plugin-management status [Section titled “apn plugin-management status”](#apn-plugin-management-status) Whether the console may manage plugins on this node. ```text apn plugin-management status ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. **Exit status:** 0; 1 when the node is not enrolled. ```sh apn plugin-management status ``` ### apn plugin-management enable [Section titled “apn plugin-management enable”](#apn-plugin-management-enable) Allow console plugin management; restart the service afterwards. ```text apn plugin-management enable ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn plugin-management enable && apn restart ``` ### apn plugin-management disable [Section titled “apn plugin-management disable”](#apn-plugin-management-disable) Refuse console plugin management; restart the service afterwards. ```text apn plugin-management disable ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn plugin-management disable && apn restart ``` ## apn mcp-proxy [Section titled “apn mcp-proxy”](#apn-mcp-proxy) Show the stations the loopback MCP proxy serves, or rotate their secrets. ```text apn mcp-proxy ``` The proxy’s per-station secrets and its loopback port persist in `mcp-proxy.json` (owner-only, 0600) beside the node config, so a session kept open across a node restart keeps working. Which stations it serves is changed from the hub, audited: [`fleet mcp-proxy`](/reference/fleet/#fleet-mcp-proxy). Subcommands: [`status`](#apn-mcp-proxy-status), [`rotate`](#apn-mcp-proxy-rotate). `apn help mcp-proxy` prints: ```text apn mcp-proxy — the node's loopback MCP proxy gives a station's harness sessions the hub's and SuperLibrary's MCP tools. Each station's secret persists in mcp-proxy.json (0600) beside the node config, so a session kept open across a node restart keeps working. `rotate` replaces the named stations' secrets (every served station when none are named); the running node refuses the old ones from their next request, without a restart. Secrets are never printed. Which stations are served is changed from the hub: fleet mcp-proxy enable|disable. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn mcp-proxy status ``` ### apn mcp-proxy status [Section titled “apn mcp-proxy status”](#apn-mcp-proxy-status) The stations the proxy serves, and whether each has a persisted secret. No secret is printed. ```text apn mcp-proxy status ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. **Exit status:** 0; 1 when the node is not enrolled. ```sh apn mcp-proxy status ``` ### apn mcp-proxy rotate [Section titled “apn mcp-proxy rotate”](#apn-mcp-proxy-rotate) Replace the named stations’ secrets — every served station when none are named. ```text apn mcp-proxy rotate [STATION_ID...] ``` The running node re-reads the state file at the next request: a session holding an old secret is refused (401) from then on, with no restart, and new sessions get the new one. The fleet side is [`fleet mcp-proxy rotate`](/reference/fleet/#fleet-mcp-proxy-rotate). **Needs:** The user the node service runs as, editing its own files. No hub contact. **Exit status:** 0; 1 when the node is not enrolled or a named station is not served (nothing is rotated then). ```sh apn mcp-proxy rotate station_123 ``` ## apn hermes-skills [Section titled “apn hermes-skills”](#apn-hermes-skills) Register or remove the managed skills directory in a Hermes profile. ```text apn hermes-skills --profile NAME [--apply] ``` Subcommands: [`status`](#apn-hermes-skills-status), [`register`](#apn-hermes-skills-register), [`unregister`](#apn-hermes-skills-unregister). `apn help hermes-skills` prints: ```text apn hermes-skills --profile NAME [--apply] — a published skill is inert until skills.external_dirs names its directory. Without --apply the command prints the exact change and writes nothing. It edits one profile's own config.yaml and leaves the rest of it alone. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-skills status --profile default ``` ### apn hermes-skills status [Section titled “apn hermes-skills status”](#apn-hermes-skills-status) Whether the profile’s config names the managed skills directory. ```text apn hermes-skills status --profile NAME ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-skills status --profile default ``` ### apn hermes-skills register [Section titled “apn hermes-skills register”](#apn-hermes-skills-register) Add the managed skills directory to `skills.external_dirs`. ```text apn hermes-skills register --profile NAME [--apply] ``` Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-skills register --profile default # show the change apn hermes-skills register --profile default --apply # make it ``` ### apn hermes-skills unregister [Section titled “apn hermes-skills unregister”](#apn-hermes-skills-unregister) Undo what `register` changed. ```text apn hermes-skills unregister --profile NAME [--apply] ``` Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-skills unregister --profile default --apply ``` ## apn hermes-live [Section titled “apn hermes-live”](#apn-hermes-live) Install, enable or remove the agentpod-live streaming plugin in a Hermes profile. ```text apn hermes-live --profile NAME [--apply] [--replace-unmanaged] ``` Subcommands: [`status`](#apn-hermes-live-status), [`enable`](#apn-hermes-live-enable), [`disable`](#apn-hermes-live-disable). `apn help hermes-live` prints: ```text apn hermes-live --profile NAME [--apply] [--replace-unmanaged] — a harness-mode Hermes profile streams into AgentPod clients only with this plugin. enable installs the copy shipped in this apn, on a Hermes the CI contract tested, and adds it to plugins.enabled with plugins.stream_reasoning_deltas: true. Without --apply it prints the exact change and writes nothing. disable removes it and undoes the configuration edit. Neither restarts the gateway; that is yours to do. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-live status --profile default ``` ### apn hermes-live status [Section titled “apn hermes-live status”](#apn-hermes-live-status) The plugin’s install state, the Hermes version and whether it is a tested one. ```text apn hermes-live status --profile NAME ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-live status --profile default ``` ### apn hermes-live enable [Section titled “apn hermes-live enable”](#apn-hermes-live-enable) Install the shipped plugin and enable it in the profile’s config. ```text apn hermes-live enable --profile NAME [--apply] [--replace-unmanaged] ``` Refused on a Hermes version the plugin was not tested against. Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | --------------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | | `--replace-unmanaged` | bool | `false` | Replace a plugin directory of the same name that this apn did not install. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-live enable --profile default --apply systemctl --user restart hermes-gateway-default.service ``` ### apn hermes-live disable [Section titled “apn hermes-live disable”](#apn-hermes-live-disable) Remove the plugin and undo the config edit. ```text apn hermes-live disable --profile NAME [--apply] ``` Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--profile NAME` | string | — | The Hermes profile, a directory under `~/.hermes/profiles/`. **Required.** | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn hermes-live disable --profile default --apply ``` ## apn openclaw-errors [Section titled “apn openclaw-errors”](#apn-openclaw-errors) Install, enable or remove the agentpod-errors plugin in OpenClaw. ```text apn openclaw-errors [--apply] ``` See [When a turn fails](/use/errors/). Subcommands: [`status`](#apn-openclaw-errors-status), [`enable`](#apn-openclaw-errors-enable), [`disable`](#apn-openclaw-errors-disable). `apn help openclaw-errors` prints: ```text apn openclaw-errors [--apply] — OpenClaw's ACP bridge drops why a turn failed; this plugin reports it to this node. enable installs the copy shipped in this apn, on an OpenClaw the CI contract tested, into ~/.agentpod/openclaw and adds it to ~/.openclaw/openclaw.json with hooks.allowConversationAccess. Without --apply it prints the exact change and writes nothing. disable removes both. Neither restarts the gateway; that is yours to do. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn openclaw-errors status ``` ### apn openclaw-errors status [Section titled “apn openclaw-errors status”](#apn-openclaw-errors-status) The plugin’s install state, the OpenClaw version and whether it is a tested one. ```text apn openclaw-errors status ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn openclaw-errors status ``` ### apn openclaw-errors enable [Section titled “apn openclaw-errors enable”](#apn-openclaw-errors-enable) Install the shipped plugin and register it in `~/.openclaw/openclaw.json`. ```text apn openclaw-errors enable [--apply] ``` Refused on an OpenClaw version the plugin was not tested against. Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ----------------------------------------------------------------------- | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn openclaw-errors enable --apply ``` ### apn openclaw-errors disable [Section titled “apn openclaw-errors disable”](#apn-openclaw-errors-disable) Remove the plugin and its registration. ```text apn openclaw-errors disable [--apply] ``` Without `--apply` it prints the exact change and writes nothing. It never restarts a gateway; that is the operator’s call. | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ----------------------------------------------------------------------- | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn openclaw-errors disable --apply ``` ## apn pi-errors [Section titled “apn pi-errors”](#apn-pi-errors) Install, enable or remove the agentpod-errors extension in Pi. ```text apn pi-errors [--apply] ``` See [When a turn fails](/use/errors/). Subcommands: [`status`](#apn-pi-errors-status), [`enable`](#apn-pi-errors-enable), [`disable`](#apn-pi-errors-disable). `apn help pi-errors` prints: ```text apn pi-errors [--apply] — pi-acp drops why a turn failed; this extension reports it to this node. enable installs the copy shipped in this apn, on a Pi the CI contract tested, as ~/.pi/agent/extensions/agentpod-errors.ts. Without --apply it prints what it would do and writes nothing. Nothing needs restarting: each new Pi session loads it. ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn pi-errors status ``` ### apn pi-errors status [Section titled “apn pi-errors status”](#apn-pi-errors-status) The extension’s install state, the Pi version and whether it is a tested one. ```text apn pi-errors status ``` **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn pi-errors status ``` ### apn pi-errors enable [Section titled “apn pi-errors enable”](#apn-pi-errors-enable) Install the shipped extension into Pi’s extensions directory. ```text apn pi-errors enable [--apply] ``` Refused on a Pi version the extension was not tested against. Without `--apply` it prints what it would do and writes nothing. Nothing needs restarting. | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ----------------------------------------------------------------------- | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn pi-errors enable --apply ``` ### apn pi-errors disable [Section titled “apn pi-errors disable”](#apn-pi-errors-disable) Remove the extension. ```text apn pi-errors disable [--apply] ``` Without `--apply` it prints what it would do and writes nothing. | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ----------------------------------------------------------------------- | | `--apply` | bool | `false` | Make the change; without it, print what would change and write nothing. | **Needs:** The user the node service runs as, editing its own files. No hub contact. ```sh apn pi-errors disable --apply ``` ## apn detect [Section titled “apn detect”](#apn-detect) Print detected harness stations as JSON. ```text apn detect ``` `apn help detect` prints: ```text apn detect — print the harness stations detected on this host as JSON. Debug/ops smoke test for the descriptors; no hub connection required. ``` **Needs:** Nothing: no enrolment, no hub, no network. ```sh apn detect | jq '.[].key' ``` ## apn forge-api [Section titled “apn forge-api”](#apn-forge-api) Set up stk as this station’s forge credential broker (setup). ```text apn forge-api setup [--user NAME] [--agent-uid N]... [--state-dir DIR] [--stk PATH] apn forge-api setup --station ID [--agent-uid N]... [--allow-shared-agent-uid] apn forge-api prune --station ID [--dry-run] [--yes] ``` | Flag | Type | Default | Meaning | | -------------------------- | ------ | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--user NAME` | string | `stk` | The OS user stk runs as. | | `--agent-uid N` | string | — | A uid allowed to use the broker; repeatable. Default: the uid harnesses run as (the user who ran sudo). | | `--state-dir DIR` | string | stk’s own (`/var/lib/stk`) | Stk state directory (default: stk’s own). | | `--stk PATH` | string | `stk` | The stk binary. | | `--allow-shared-agent-uid` | bool | `false` | With `--station`: acknowledge that stations whose agents share a uid are not isolated from each other (otherwise a second station on a uid already in use is refused with `station-uid-shared`). Same as `allowSharedAgentUid` in the node config. | | `--dry-run` | bool | `false` | With `prune`: say what would be removed and remove nothing. | | `--yes` | bool | `false` | With `prune`: confirm. `prune` deletes the station’s stk user, state, every credential it holds and its harness wiring, and nothing runs it automatically. | | `--station ID` | string | — | `setup`: create this station’s own stk instance now, as the node would at its first grant: own OS user, state, policy file, proxy port and systemd unit, all derived from the id. Cannot be combined with `--user`, `--state-dir` or `--stk`. | `apn help forge-api` prints: ```text apn forge-api setup [--user stk] [--agent-uid N]... [--state-dir DIR] [--stk PATH] apn forge-api setup --station ID [--agent-uid N]... [--allow-shared-agent-uid] apn forge-api prune --station ID [--dry-run] [--yes] Installs stk as its own OS user so the agent can use forge credentials without being able to read them. Run it once with sudo; it is safe to repeat (an existing separate-user install is left alone). --agent-uid defaults to the uid the node starts harnesses as (the user who ran sudo). Without root, nothing is installed: the command reports same-user isolation, marked DEGRADED, and says how to fix it. --station ID creates that station's own stk instance (its own OS user, state, policy file, proxy port and systemd unit, derived from the id) ahead of time. The node does the same on its own at a station's first grant when it runs as root, so this is only for preparing a station or for a node that does not run as root. A second station whose agents run as a uid another station already uses is refused (station-uid-shared): isolation between stations needs a distinct uid per station. --allow-shared-agent-uid acknowledges the risk; the station's grants then report same-user isolation. prune removes ONE station's whole stk instance (its unit, user, state, policy and every credential it holds, and the harness wiring) when the station is gone from this node. It is manual: nothing removes an instance for you. --dry-run says what it would remove; without it --yes is required. A station the node's registry does not list is left alone. Exit 0 on separate-user or on a clearly reported same-user; 1 on errors. ``` **Needs:** root to install (Linux with systemd). Without root nothing is installed and the command reports same-user isolation as degraded. **Exit status:** 0 on separate-user, or on a clearly reported same-user. 1 on an error. 2 on a bad flag or verb. ```sh sudo agentpod-node forge-api setup sudo agentpod-node forge-api setup --agent-uid 1001 ``` ## apn scan [Section titled “apn scan”](#apn-scan) Check this machine’s agents for exposure (`--json`). ```text apn scan [--json] [--no-color] ``` See [Checking for exposure](/use/scan/). | Flag | Type | Default | Meaning | | ------------ | ---- | ------- | ---------------------------------------------------------------------- | | `--json` | bool | `false` | Emit the report as JSON. | | `--no-color` | bool | `false` | Disable ANSI colour (it is already off when stdout is not a terminal). | `apn help scan` prints: ```text apn scan [--json] [--no-color] — check the agent runtimes on this host for the two ways they get taken over: a listener bound to every network interface, and credential files other users can read. Needs no hub, no account and no network. Prints a graded report and exits 0 (clean), 1 (warnings) or 2 (critical), so it works in cron. A check that cannot determine an answer says so — it is never reported as a pass. ``` **Needs:** Nothing: no enrolment, no hub, no network. **Exit status:** 0 clean, 1 warnings, 2 critical. ```sh apn scan apn scan --json > posture.json ``` ## apn acp [Section titled “apn acp”](#apn-acp) Attach an ACP editor to a station (`--list`, `--station`). ```text apn acp --list [--hub URL] apn acp --station ID [--session ID] [--hub URL] ``` See [Attaching an editor](/use/acp/). | Flag | Type | Default | Meaning | | --------------- | ------ | ------------------------------------------------ | ------------------------------------------------------------- | | `--list` | bool | `false` | List the stations you can attach an editor to. | | `--station ID` | string | — | Station to attach to; required unless `--list`. | | `--session ID` | string | — | Specific session to resume. | | `--hub URL` | string | `$AGENTPOD_HUB`, else `https://hub.agentpod.dev` | Hub base URL. | | `--token TOKEN` | string | `$AGENTPOD_TOKEN` | A person’s access token (prefer the AGENTPOD\_TOKEN env var). | `apn help acp` prints: ```text apn acp --list — show the stations you can attach an editor to. apn acp --station [--session ] [--hub ] [--token ] — make a station on another machine look like a local agent to any ACP client (Zed, JetBrains, anything that speaks the protocol). The editor spawns this and talks ACP over its stdio; the frames are piped to the hub, which does the protocol work. Reaches stations behind NAT or CGNAT, because the node dials out. Prefer the AGENTPOD_TOKEN environment variable over --token: a token on the command line lands in shell history. This is the one command that needs no enrolled node — a laptop can install apn purely as a client. ``` **Needs:** A person’s access token, from `$AGENTPOD_TOKEN` or `--token` — never this host’s credential, and never the file `fleet login` writes. Needs no enrolled node. **Exit status:** 0 when the editor closes the session. 1 when the hub refuses or drops it. 2 without `--station` or `--list`. ```sh AGENTPOD_TOKEN=… apn acp --list AGENTPOD_TOKEN=… apn acp --station stn_123 ``` ## apn update [Section titled “apn update”](#apn-update) Self-update from the latest release (`--check`, `--force`). ```text apn update [--check] [--force] ``` | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ------------------------------------------------------ | | `--check` | bool | `false` | Resolve and report current/latest version, no changes. | | `--force` | bool | `false` | Update even when already on the latest version. | `apn help update` prints: ```text apn update [--check] [--force] — self-update from the latest GitHub release. --check resolves and reports the current/latest version without changing anything. --force updates even when already on the latest version. On success the service is restarted automatically; if the restart fails, the binary is already swapped and this prints a manual-restart hint. ``` **Needs:** Network access to GitHub releases, and write access to the binary’s own path. The download is verified against the release’s `SHA256SUMS`. **Exit status:** 0 when up to date, updated, or (with `--check`) reported. 1 when the release could not be fetched or verified, or the binary was swapped but the restart failed (the right restart command is printed). ```sh apn update --check apn update ``` ## apn help [Section titled “apn help”](#apn-help) Show this list, or one command in detail. ```text apn help [COMMAND] apn -h apn ``` | Argument | Meaning | | --------- | -------------------------------- | | `COMMAND` | one command, to print its detail | `apn help help` prints: ```text apn help [command] — with no argument, the commands grouped by purpose; with one, that command's detail. 'apn -h' prints the same detail, plus the command's flags and their defaults where it has any. ``` **Needs:** Nothing: no enrolment, no hub, no network. **Exit status:** 0; 2 for an unknown COMMAND. ```sh apn help apn help enroll ``` ## apn version [Section titled “apn version”](#apn-version) Print version and platform. ```text apn version ``` `apn help version` prints: ```text apn version — print the binary version and platform (GOOS/GOARCH). ``` **Needs:** Nothing: no enrolment, no hub, no network. **Exit status:** 0. ```sh apn version # agentpod-node v0.1.90 linux/amd64 ``` # fleet reference > Every fleet verb and subverb: synopsis, flags, environment, what it needs and how it exits. Generated from the binary. `fleet` (`agentpod-fleet`) acts on a fleet **as a principal** — a person or an agent — from a laptop, CI or an agent’s own workspace. It never uses a node’s credential; acting as a machine is [`apn`](/reference/apn/)’s job. For the reasoning behind the split, see [apn and fleet](/use/cli/). This page is generated from the binary’s own command table and source, and a test fails when a verb or flag exists without an entry here. Unless a command says otherwise, every verb that talks to the hub prints the hub’s JSON response unchanged on stdout: it is the hub’s shape, not a summary of it, so scripts and agents can depend on it. ## Commands [Section titled “Commands”](#commands) **Signing in** | Command | What it does | | --------------------------------- | ----------------------------------------------------- | | [`fleet login`](#fleet-login) | Sign in once; this machine keeps a device credential. | | [`fleet whoami`](#fleet-whoami) | Who the token says you are, read locally. | | [`fleet logout`](#fleet-logout) | Forget this machine’s token and device credential. | | [`fleet devices`](#fleet-devices) | Where this machine’s device credentials are listed. | **Reading the fleet** | Command | What it does | | ----------------------------------- | ------------------------------------- | | [`fleet agents`](#fleet-agents) | The agents this token may dispatch. | | [`fleet stats`](#fleet-stats) | Fleet totals. | | [`fleet activity`](#fleet-activity) | Recent activity across your stations. | **Nodes** | Command | What it does | | ----------------------------------- | --------------------------------------------------------------------- | | [`fleet nodes`](#fleet-nodes) | The fleet’s nodes, with versions. | | [`fleet invite`](#fleet-invite) | Mint the token a machine presents to `apn enroll`. | | [`fleet runtimes`](#fleet-runtimes) | The substrate nodes run on: list, providers, create, start, stop, rm. | **Stations** | Command | What it does | | ------------------------------------- | ------------------------------------------------------------------------ | | [`fleet stations`](#fleet-stations) | Detect, adopt and unadopt stations; grant one push or forge API access. | | [`fleet station`](#fleet-station) | One station: lifecycle, cleanup, changeset, files. | | [`fleet staff`](#fleet-staff) | Put an agent in a station, or take it out. | | [`fleet config`](#fleet-config) | Declare what a harness setting should be, and see what it is. | | [`fleet mcp-proxy`](#fleet-mcp-proxy) | Which stations each node’s loopback MCP proxy serves, and their secrets. | **Skills and plugins** | Command | What it does | | --------------------------------- | ----------------------------------------------------------- | | [`fleet skills`](#fleet-skills) | Skill artifacts, releases, cohorts, canaries and placement. | | [`fleet plugins`](#fleet-plugins) | Review and apply harness plugin changes on a station. | **Administration** | Command | What it does | | --------------------------------------- | ------------------------------------------------------ | | [`fleet settings`](#fleet-settings) | Hub-wide settings: signup, transcription, speech. | | [`fleet bridge`](#fleet-bridge) | The board roster: which agent claims from which board. | | [`fleet principals`](#fleet-principals) | Identities, service principals and their credentials. | | [`fleet grants`](#fleet-grants) | Dispatch authority, as a document (older hubs). | | [`fleet users`](#fleet-users) | People: list, show, ban, unban, role (older hubs). | **This binary** | Command | What it does | | --------------------------------- | ---------------------------------------------- | | [`fleet update`](#fleet-update) | Replace this binary with the newest release. | | [`fleet skill`](#fleet-skill) | Print the skill that ships inside this binary. | | [`fleet version`](#fleet-version) | Print the version and platform. | | [`fleet help`](#fleet-help) | Print the verb list and how credentials work. | ## Environment [Section titled “Environment”](#environment) | Variable | Meaning | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `$AGENTPOD_HUB` | Hub base URL. Else the hub the stored login (token.json) was made against, else `https://hub.agentpod.dev`. Never taken from a node’s config. | | `$AGENTPOD_TOKEN` | A principal’s token to use instead of the stored credential. Checked first; nothing is stored. When it has expired, commands say so rather than falling back. | | `$AGENTPOD_DEVICE_NAME` | What `fleet login` names this machine in the device list. Default: the hostname. | | `$AGENTPOD_LOGIN_TIMEOUT` | How long the older hub sign-in waits for the browser, as a Go duration (`90s`, `10m`). Default `5m`. | | `$BROWSER` | The command `fleet login` opens the sign-in page with (it may carry arguments). `none` opens nothing; the URL is always printed. | ## Exit status [Section titled “Exit status”](#exit-status) Unless a command says otherwise: * **0** — the hub accepted the request. * **1** — no usable credential, the hub could not be reached, or the hub refused or failed the request. Its status and body go to stderr: `401` means sign in again, `403` means this principal may not. * **2** — a usage error: an unknown verb, or a missing flag or argument. Nothing was sent. ## Credentials [Section titled “Credentials”](#credentials) `fleet` resolves a token in this order: `$AGENTPOD_TOKEN`; a cached token that has not expired; the stored device credential, exchanged for a fresh five-minute token. With none of those it prints how to sign in and exits 1. The files live under your user config directory (`~/.config/agentpod/` on Linux, `~/Library/Application Support/agentpod/` on macOS): `token.json` and `device.json`. A device credential is only ever exchanged with the hub and account service that issued it. Each command below says what it needs. The hub decides; `fleet` performs no permission check of its own and prints the hub’s refusal as it came. ## fleet login [Section titled “fleet login”](#fleet-login) Sign in once; this machine keeps a device credential. ```text fleet login ``` Asks the hub how it signs people in. A current hub names its account service, and `login` runs that service’s device flow: it prints a page and a code, opens the page if it can, and waits while you confirm the code in a browser — any browser, so it works over SSH. Approval stores a long-lived **device credential** for this machine and a first five-minute token. Every later command exchanges the device credential for a fresh token, so the browser opens once, not once per lapse. Against an older hub that issues its own tokens, `login` instead opens the hub’s sign-in page and receives the result on a local `127.0.0.1` callback (authorization code with PKCE), then registers this machine as a device at the hub. `$AGENTPOD_LOGIN_TIMEOUT` bounds that wait. `fleet login -h` prints `fleet help` and opens nothing. **Needs:** Nothing beforehand: this is how you get a credential. The sign-in itself is the account service’s. **Exit status:** 0 once signed in. 1 when the hub or account service cannot be reached, the code was denied or expired, or a credential could not be stored. ```sh fleet login BROWSER=none fleet login # on a server: print the page, open nothing ``` ## fleet whoami [Section titled “fleet whoami”](#fleet-whoami) Who the token says you are, read locally. ```text fleet whoami [--json] ``` Reads the token’s own claims — principal, kind, expiry — plus the hub it is for and where the token came from. It does not ask the hub, which is the point: it separates “not signed in” from “signed in, not permitted”. A `403` from another verb means the second; this answers the first. | Flag | Type | Default | Meaning | | -------- | ---- | ------- | ----------------------------------------------------------------- | | `--json` | bool | `false` | Print `principal`, `kind`, `source`, `hub` and `expires` as JSON. | **Needs:** Any token. It may still have to be resolved, which can exchange the device credential. ```sh fleet whoami fleet whoami --json | jq -r .principal ``` ## fleet logout [Section titled “fleet logout”](#fleet-logout) Forget this machine’s token and device credential. ```text fleet logout ``` Deletes both stored files. With a device credential from an account service, signing out is **local only**: the service owns the credential, so `logout` prints where to revoke it (its Devices page) and never sends it to the hub. With a credential an older hub issued, it first asks that hub to revoke it, and still signs out locally if the hub cannot be reached. **Needs:** Nothing. **Exit status:** 0 once both files are gone. 1 if a file could not be removed. ```sh fleet logout ``` ## fleet devices [Section titled “fleet devices”](#fleet-devices) Where this machine’s device credentials are listed. ```text fleet devices ``` With a device credential from an account service, prints where the devices that may act as you are listed — the account service’s Devices page — and sends nothing to the hub. With a credential from an older hub, prints that hub’s list. With no device credential at all (a token in `$AGENTPOD_TOKEN`, say) it asks the hub, and a current hub answers 410, which exits 1. Subcommands: [`revoke`](#fleet-devices-revoke). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet devices ``` ### fleet devices revoke [Section titled “fleet devices revoke”](#fleet-devices-revoke) Revoke one device credential (older hubs). ```text fleet devices revoke DEVICE_ID ``` With a device credential from an account service, revoking happens there: this prints its Devices page and exits 1, because nothing was revoked. Against an older hub it revokes one of your own devices; one that is not yours and one that does not exist are the same 404. | Argument | Meaning | | ----------- | -------------------------------------------- | | `DEVICE_ID` | the device’s id, as `fleet devices` lists it | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** 0 when revoked. 1 when the device is managed by an account service, or the hub refused. 2 without a DEVICE\_ID. ```sh fleet devices revoke dev_0123456789abcdef0123 ``` ## fleet agents [Section titled “fleet agents”](#fleet-agents) The agents this token may dispatch. ```text fleet agents ``` Not every agent in the fleet: the ones the token’s signed grant lets it dispatch. The hub reads that from the token itself. Nothing granted is an empty list, not an error. **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet agents | jq '.[].name' ``` ## fleet stats [Section titled “fleet stats”](#fleet-stats) Fleet totals. ```text fleet stats ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stats ``` ## fleet activity [Section titled “fleet activity”](#fleet-activity) Recent activity across your stations. ```text fleet activity ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet activity ``` ## fleet nodes [Section titled “fleet nodes”](#fleet-nodes) The fleet’s nodes, with versions. ```text fleet nodes ``` Subcommands: [`update`](#fleet-nodes-update), [`telemetry`](#fleet-nodes-telemetry), [`rm`](#fleet-nodes-rm). `fleet nodes -h` prints: ```text Usage: fleet nodes [update|telemetry|rm] fleet nodes the fleet's nodes, with versions fleet nodes update [--node NAME|ID …] [--force] fleet nodes rm NAME|ID [--force] remove a retired machine from the fleet fleet nodes telemetry each node's OpenTelemetry setting fleet nodes telemetry [--node NAME|ID …] --endpoint | --off update asks the hub to roll the newest release to your nodes, one at a time, and prints what happened to each. With --node it touches only those nodes (repeatable; a name or an ID from `fleet nodes`). --force re-applies the current release to a node that already has it — the escape hatch for a corrupt binary. rm removes an enrolled node: its stations are unregistered and its credential is revoked, so the machine cannot reconnect; to rejoin it needs a fresh `fleet invite` token. A connected node is refused unless --force, which disconnects it. A provisioned runtime's node is removed with `fleet runtimes rm`. Only the node-agent restarts; the harnesses it serves keep running. The exit status is 1 if any node was asked and did not update. telemetry (admin role required) reads or sets the OTLP endpoint each node-agent exports traces to, with no SSH. Without flags it lists the setting per node (offline nodes are shown and do not fail the exit status). --endpoint takes an http or https URL; --off disables export. A node restarts itself only if its setting changed. A node too old to know the verb says "unsupported" until `fleet nodes update`. The exit status is 1 if any node failed, was unsupported, or (when setting) was offline and so did not apply the change. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet nodes ``` ### fleet nodes update [Section titled “fleet nodes update”](#fleet-nodes-update) Roll the newest release to every node, or to `--node` ones. ```text fleet nodes update [--node NAME|ID ...] [--force] ``` The hub updates one node at a time, in name order, waits for each to answer, and skips nodes whose binary comes from an image. Only the node agent restarts; the harnesses it serves keep running. Names are resolved to ids before anything is sent, and an unknown name updates nothing. Waits up to 15 minutes for the whole rollout. | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | ----------------------------- | | `--node NAME\|ID` | string | — | Node name or ID (repeatable). | | `--force` | bool | `false` | Re-apply the current release. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** 0 when every node asked updated. 1 if any did not (the per-node results are printed first). 2 for an unknown `--node` name or a stray argument. ```sh fleet nodes update fleet nodes update --node build-01 --node build-02 ``` ### fleet nodes telemetry [Section titled “fleet nodes telemetry”](#fleet-nodes-telemetry) Read or set each node’s OpenTelemetry endpoint, with no SSH. ```text fleet nodes telemetry [--node NAME|ID ...] [--endpoint URL | --off] ``` Without `--endpoint` or `--off`, lists one line per node: name, status, the configured endpoint, the one it is running with when they differ, and the state of its service unit. With one of them, sets it on every node, or only the `--node` ones; a node restarts itself only if its setting changed. A node too old to know the verb reports `unsupported` until `fleet nodes update`. On the node itself this is [`apn telemetry`](/reference/apn/#apn-telemetry). | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | ----------------------------------------------------------- | | `--node NAME\|ID` | string | — | Node name or ID; needs `--endpoint` or `--off`. Repeatable. | | `--endpoint URL` | string | — | OTLP/HTTP endpoint URL (http or https). | | `--off` | bool | `false` | Disable telemetry export. | `fleet nodes telemetry -h` prints: ```text Usage: fleet nodes telemetry [--node NAME|ID …] [--endpoint | --off] With no flags, lists each node's telemetry setting. With --endpoint (http or https) or --off, sets it on every node, or only the --node ones. Admin role required. Exit 1 if any node failed, was unsupported, or (when setting) offline. ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** 0 when every node reported (or applied) the setting. 1 if any failed or was unsupported, or — when setting — was offline. 2 for `--endpoint` with `--off`, `--node` alone, or an invalid URL. ```sh fleet nodes telemetry fleet nodes telemetry --endpoint https://otel.example.com:4318 fleet nodes telemetry --node build-01 --off ``` ### fleet nodes rm [Section titled “fleet nodes rm”](#fleet-nodes-rm) Remove a retired machine from the fleet. ```text fleet nodes rm NAME|ID [--force] ``` Unregisters every station on the node, the way [`fleet stations unadopt`](#fleet-stations-unadopt) does, and revokes the node’s credential: a machine that dials back is refused, and rejoining takes a fresh [`fleet invite`](#fleet-invite) token. Workspace files, agent identities and Matrix rooms are kept. A connected node is refused unless `--force`, which disconnects it. A provisioned runtime’s node is refused with the [`fleet runtimes rm`](#fleet-runtimes-rm) that removes both, and a node with a bridge-roster row on one of its stations is refused until [`fleet bridge rm`](#fleet-bridge-rm) removes the row. A name is resolved from `fleet nodes`; anything else is sent as an id, so a node that is not there, or not yours, is the hub’s 404. | Argument | Meaning | | -------- | ------- | | \`NAME | ID\` | | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ----------------------------------------------- | | `--force` | bool | `false` | Disconnect and remove a node that is connected. | `fleet nodes rm -h` prints: ```text Usage: fleet nodes rm NAME|ID [--force] Removes an enrolled node from the fleet. Its stations are unregistered (as `fleet stations unadopt` would), and its credential is revoked, so the machine cannot reconnect; to rejoin it must be enrolled again with a fresh `fleet invite` token. Workspace files on the machine are not touched, and the node-agent keeps running until it is uninstalled there. A connected node is refused unless --force, which disconnects it. A provisioned runtime's node is refused: `fleet runtimes rm` removes the runtime and its node. ``` **Needs:** The node’s owner. Where the workspace enforces who may grow the fleet, also a workspace admin — the same authority `fleet invite` needs. **Exit status:** 0 when the node was removed. 1 when the hub refused (the reason is printed; for a connected node, with the `--force` command) or answered 404. 2 without exactly one node. ```sh fleet nodes rm build-01 fleet nodes rm build-01 --force ``` ## fleet invite [Section titled “fleet invite”](#fleet-invite) Mint the token a machine presents to `apn enroll`. ```text fleet invite [--label TEXT] [--ttl-minutes N] ``` Creates no node: it authorises one to appear. An unredeemed invitation looks exactly like no invitation in `fleet nodes`. The response carries the one-time token; hand it to [`apn enroll --token`](/reference/apn/#apn-enroll). | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | -------------------------------------------------- | | `--label TEXT` | string | — | What this token is for, recorded with it. | | `--ttl-minutes N` | int | `0` | Minutes until it expires (hub default when unset). | `fleet invite -h` prints: ```text usage: fleet invite [--label TEXT] [--ttl-minutes N] ``` **Needs:** A person’s token. When the hub enforces dispatch grants, a workspace admin’s. ```sh fleet invite --label "build runner" --ttl-minutes 30 ``` ## fleet runtimes [Section titled “fleet runtimes”](#fleet-runtimes) The substrate nodes run on: list, providers, create, start, stop, rm. ```text fleet runtimes ``` The hub treats a runtime’s state as evidence, not as a request’s outcome: `stop` writes `stopping`, and only the provider reporting the container down writes `stopped`. A `stop` that returns cleanly means “asked”; read the state afterwards. Subcommands: [`list`](#fleet-runtimes-list), [`providers`](#fleet-runtimes-providers), [`create`](#fleet-runtimes-create), [`start`](#fleet-runtimes-start), [`stop`](#fleet-runtimes-stop), [`rm`](#fleet-runtimes-rm). `fleet runtimes -h` prints: ```text usage: fleet runtimes list fleet runtimes providers fleet runtimes create --file PATH|- fleet runtimes start ID fleet runtimes stop ID fleet runtimes rm ID ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet runtimes list ``` ### fleet runtimes list [Section titled “fleet runtimes list”](#fleet-runtimes-list) Every runtime. ```text fleet runtimes list ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes list ``` ### fleet runtimes providers [Section titled “fleet runtimes providers”](#fleet-runtimes-providers) The providers this hub can provision on. ```text fleet runtimes providers ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes providers ``` ### fleet runtimes create [Section titled “fleet runtimes create”](#fleet-runtimes-create) Provision a runtime from a request document. ```text fleet runtimes create --file PATH|- ``` The request is a JSON document the hub validates; take its shape from `fleet runtimes providers`. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ------------------------------------------------ | | `--file PATH\|-` | string | — | Provision request, or - for stdin. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes create --file runtime.json ``` ### fleet runtimes start [Section titled “fleet runtimes start”](#fleet-runtimes-start) Ask a runtime to start. ```text fleet runtimes start ID ``` | Argument | Meaning | | -------- | ---------------- | | `ID` | the runtime’s id | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes start rt_123 ``` ### fleet runtimes stop [Section titled “fleet runtimes stop”](#fleet-runtimes-stop) Ask a runtime to stop; its state says when it has. ```text fleet runtimes stop ID ``` | Argument | Meaning | | -------- | ---------------- | | `ID` | the runtime’s id | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes stop rt_123 ``` ### fleet runtimes rm [Section titled “fleet runtimes rm”](#fleet-runtimes-rm) Delete a runtime. ```text fleet runtimes rm ID ``` | Argument | Meaning | | -------- | ---------------- | | `ID` | the runtime’s id | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet runtimes rm rt_123 ``` ## fleet stations [Section titled “fleet stations”](#fleet-stations) Detect, adopt and unadopt stations; grant one push or forge API access. ```text fleet stations [flags] ``` Acts on the fleet’s shape. For one station’s contents — lifecycle, disk, diffs, files — see [`fleet station`](#fleet-station). Every verb here parses the same flags; each lists the ones it reads. Subcommands: [`detected`](#fleet-stations-detected), [`list`](#fleet-stations-list), [`adopt`](#fleet-stations-adopt), [`unadopt`](#fleet-stations-unadopt), [`git-identity`](#fleet-stations-git-identity), [`grant-push`](#fleet-stations-grant-push), [`revoke-push`](#fleet-stations-revoke-push), [`forge-api`](#fleet-stations-forge-api). `fleet stations -h` prints: ```text Usage: fleet stations fleet stations detected --node NODE_ID what the node reports right now fleet stations list --node NODE_ID adopted stations, with their IDs fleet stations adopt --node NODE_ID --key KEY [--key KEY …] fleet stations unadopt --station STATION_ID fleet stations git-identity --station STATION_ID what it can push to forge as fleet stations grant-push --station STATION_ID give it a forge push key fleet stations revoke-push --station STATION_ID take that key away fleet stations forge-api --station STATION_ID … forge API access through the station's broker (operator only; see fleet stations forge-api --help) A detected station is not an agent until it is adopted. Adopting re-detects on the node first, so a key that has gone away is not adopted from a stale list. Push access is granted per station and never by adopting one: most stations never touch git, and a forge key for every station is an account nobody uses and a key nobody revokes. The keypair is generated ON THE NODE and the private half never leaves it — grant-push asks for the public half and registers that. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 0. ```sh fleet stations list --node node_123 ``` ### fleet stations detected [Section titled “fleet stations detected”](#fleet-stations-detected) What the node reports right now, adopted or not. ```text fleet stations detected --node NODE_ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ---------------------- | | `--node NODE_ID` | string | — | Node ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations detected --node node_123 ``` ### fleet stations list [Section titled “fleet stations list”](#fleet-stations-list) Adopted stations on a node, with the ids other verbs need. ```text fleet stations list --node NODE_ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ---------------------- | | `--node NODE_ID` | string | — | Node ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations list --node node_123 ``` ### fleet stations adopt [Section titled “fleet stations adopt”](#fleet-stations-adopt) Make detected stations into agents. ```text fleet stations adopt --node NODE_ID --key KEY [--key KEY ...] ``` Re-detects on the node first, so a key that has gone away is not adopted from a stale list. Several keys are one reviewed call. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ---------------------------------------------------------------------------------------- | | `--node NODE_ID` | string | — | Node ID. **Required.** | | `--key KEY` | string | — | Station key to adopt, as `fleet stations detected` reports it. **Required.** Repeatable. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations adopt --node node_123 --key hermes:default ``` ### fleet stations unadopt [Section titled “fleet stations unadopt”](#fleet-stations-unadopt) Stop treating a station as an agent. ```text fleet stations unadopt --station STATION_ID ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations unadopt --station stn_123 ``` ### fleet stations git-identity [Section titled “fleet stations git-identity”](#fleet-stations-git-identity) What a station can push to the forge as. ```text fleet stations git-identity --station STATION_ID ``` Shows the forge account, its key id, and authorName/authorEmail — who the station’s commits are by (GIT\_AUTHOR\_\* and GIT\_COMMITTER\_\* in its harness and terminal environment). The author is null for an identity provisioned before authors existed, until its node next connects. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations git-identity --station stn_123 ``` ### fleet stations grant-push [Section titled “fleet stations grant-push”](#fleet-stations-grant-push) Give a station a forge push key. ```text fleet stations grant-push --station STATION_ID ``` The keypair is generated on the node and the private half never leaves it; the hub registers the public half for the station’s occupying agent. Push access is never granted by adopting a station. A hub with no forge configured answers 503. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations grant-push --station stn_123 ``` ### fleet stations revoke-push [Section titled “fleet stations revoke-push”](#fleet-stations-revoke-push) Take a station’s forge push key away. ```text fleet stations revoke-push --station STATION_ID ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations revoke-push --station stn_123 ``` ### fleet stations forge-api [Section titled “fleet stations forge-api”](#fleet-stations-forge-api) Give a station’s agent scoped forge API access through its broker (operator only). ```text fleet stations forge-api --station STATION_ID [flags] ``` The station’s broker (stk) holds the credential and injects it into requests the policy allows; the agent never sees it. Every verb but `list` is an operator action: the hub operator seat AND the control pair’s reach over the station’s agent (the owner alone is refused with 403). Three credential modes: `minted` (the hub mints a scoped token on its configured forge for the occupying agent), `supplied` (an operator-provided token) and `app` (a GitHub App’s id, installation id and private key). A supplied or app credential is read from stdin, sealed on this machine to the station’s handover key (fetched from the hub once, before anything is created), and only the sealed envelope is sent. There is no flag that takes a credential. Output is the hub’s sanitised grant view; it never contains a token. See [Stations](/use/stations/#forge-api-access) for the limits. Subcommands: [`list`](#fleet-stations-forge-api-list), [`grant`](#fleet-stations-forge-api-grant), [`verify`](#fleet-stations-forge-api-verify), [`rotate`](#fleet-stations-forge-api-rotate), [`secret`](#fleet-stations-forge-api-secret), [`policy`](#fleet-stations-forge-api-policy), [`revoke`](#fleet-stations-forge-api-revoke), [`confirm-revoked`](#fleet-stations-forge-api-confirm-revoked). `fleet stations forge-api -h` prints: ```text Usage: fleet stations forge-api --station STATION_ID [flags] list --station ID grants: state, mode, account, isolation grant --station ID --provider P --instance https://… --repo owner/name … [--preset read|issues|pull-requests|maintain|none] [--rule 'allow: METHOD /path']… [--rules-file FILE] [--mode minted|app|supplied] [--expect-login LOGIN] [--allow-graphql] [--acknowledge-same-user] verify --station ID --grant GRANT_ID re-check the credential on the node rotate --station ID --grant GRANT_ID minted: a new token, the old one revoked secret --station ID --grant GRANT_ID re-supply a credential (stdin); add --mode app for an App policy --station ID --grant GRANT_ID --preset P [--rule …]… [--rules-file FILE] [--allow-graphql] [--acknowledge-same-user] replaces the whole policy: omitting --allow-graphql turns GraphQL OFF revoke --station ID --grant GRANT_ID confirm-revoked --station ID --grant GRANT_ID supplied: you revoked it at the provider Granting an agent forge API access is an OPERATOR action (hub operator seat and the control pair's reach). An agent must not try to grant itself access. A supplied or app credential is read from STDIN, never from a flag: a token for --mode supplied; for --mode app the lines app_id=…, installation_id=… and private_key=… (the PEM may continue over the following lines). The CLI fetches the station's handover key and seals the credential to it on this machine; the hub only ever sees the sealed envelope. A minted grant needs no credential. A write grant (any preset except read, or an allow rule for a write method) on a station whose broker runs in same-user mode is refused until the operator passes --acknowledge-same-user: the agent could then read the broker's store. Today every station reports same-user. The hub records who acknowledged. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** 0 on success; 2 when a flag or the stdin credential is refused before any request; 1 when the hub refuses (its status and fixed message are printed, never the request). ```sh fleet stations forge-api list --station stn_123 ``` #### fleet stations forge-api list [Section titled “fleet stations forge-api list”](#fleet-stations-forge-api-list) A station’s forge API grants: state, mode, account, isolation. ```text fleet stations forge-api list --station STATION_ID [--json] ``` Prints only known fields (id, state, mode, account, isolation, provider, instance, preset, repositories); `--json` prints the same fields as JSON. Readable by the station’s owner as well as an operator. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------------------ | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--json` | bool | `false` | List: print the same fields as JSON. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api list --station stn_123 ``` #### fleet stations forge-api grant [Section titled “fleet stations forge-api grant”](#fleet-stations-forge-api-grant) Grant a station API access to named repositories on one forge. ```text fleet stations forge-api grant --station STATION_ID --provider forgejo|github|gitea|bitbucket \ --instance https://HOST --repo OWNER/NAME [--repo …] [--preset read|issues|pull-requests|maintain|none] \ [--rule 'allow: METHOD /path']… [--rules-file FILE] [--mode minted|app|supplied] [--expect-login LOGIN] [--allow-graphql] \ [--acknowledge-same-user] ``` Refused before any request: no `--repo`, an `--instance` that is not https, an unknown provider, preset or mode, a malformed `--rule`, or `--mode supplied|app` without `--expect-login` (for an app, its slug). `--rule` is `allow: METHOD /path` or `deny: METHOD /path`; a rule with a `when` condition goes in `--rules-file`, a JSON array of `{allow|deny, when}` objects. `minted` (the default) sends one request. `supplied` reads one token from stdin; `app` reads `app_id=…`, `installation_id=…` and `private_key=…` lines (the PEM may continue over the following lines). For those the CLI fetches the station’s key, seals, creates the grant, then posts the envelope; if that last step fails, re-send with `secret`. What the hub accepts today: Forgejo `minted` (the hub’s configured forge only) and `supplied`; GitHub `app` and `supplied`. Gitea and Bitbucket are refused until their adapters exist. A write grant (any preset except `read`, or an allow rule for a write method) on a station whose broker runs in same-user mode (today: every station) is refused (409 `same-user-acknowledgement-required`) unless the operator passes `--acknowledge-same-user`; the hub records who acknowledged. Agents must never pass it. | Flag | Type | Default | Meaning | | ------------------------- | ------ | -------- | ------------------------------------------------------------------ | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--provider PROVIDER` | string | — | Forgejo, github, gitea or bitbucket. **Required.** | | `--instance URL` | string | — | The forge’s https origin. **Required.** | | `--repo OWNER/NAME` | string | — | Owner/name (repeatable). **Required.** | | `--preset PRESET` | string | read | Read, issues, pull-requests, maintain or none. | | `--rule RULE` | string | — | ‘allow: METHOD /path’ or ‘deny: METHOD /path’ (repeatable). | | `--rules-file FILE` | string | — | A JSON array of rules ({allow\|deny, when}), added after `--rule`. | | `--mode MODE` | string | `minted` | Minted, app or supplied. | | `--expect-login LOGIN` | string | — | The account the credential must belong to (app: the App’s slug). | | `--allow-graphql` | bool | `false` | Let the station use the GraphQL endpoint. | | `--acknowledge-same-user` | bool | `false` | Operator only: accept a write grant in same-user mode. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api grant --station stn_123 --provider forgejo --instance https://forge.example \ --repo org/repo --preset issues pbpaste | fleet stations forge-api grant --station stn_123 --provider github --instance https://github.com \ --repo org/repo --mode supplied --expect-login octo-bot ``` #### fleet stations forge-api verify [Section titled “fleet stations forge-api verify”](#fleet-stations-forge-api-verify) Re-check a grant’s credential on the node. ```text fleet stations forge-api verify --station STATION_ID --grant GRANT_ID ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api verify --station stn_123 --grant fag_123 ``` #### fleet stations forge-api rotate [Section titled “fleet stations forge-api rotate”](#fleet-stations-forge-api-rotate) Mint a minted grant a new token; the previous one is revoked. ```text fleet stations forge-api rotate --station STATION_ID --grant GRANT_ID ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api rotate --station stn_123 --grant fag_123 ``` #### fleet stations forge-api secret [Section titled “fleet stations forge-api secret”](#fleet-stations-forge-api-secret) Re-supply a supplied or app grant’s credential from stdin, sealed. ```text fleet stations forge-api secret --station STATION_ID --grant GRANT_ID [--mode app] ``` Reads a token from stdin, or with `--mode app` the App’s three field lines; fetches the station’s key once, seals, and posts only the envelope. | Flag | Type | Default | Meaning | | ---------------------- | ------ | -------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | | `--mode MODE` | string | `minted` | App to read an App’s fields instead of a token. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh pbpaste | fleet stations forge-api secret --station stn_123 --grant fag_123 ``` #### fleet stations forge-api policy [Section titled “fleet stations forge-api policy”](#fleet-stations-forge-api-policy) Replace a grant’s preset, rules and GraphQL setting. ```text fleet stations forge-api policy --station STATION_ID --grant GRANT_ID --preset PRESET [--rule RULE]… [--rules-file FILE] [--allow-graphql] [--acknowledge-same-user] ``` Replaces, not merges: rules not given are removed, and GraphQL is off unless `--allow-graphql` is given. Moving a same-user grant to a write policy needs `--acknowledge-same-user` (operator only). | Flag | Type | Default | Meaning | | ------------------------- | ------ | ------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | | `--preset PRESET` | string | — | Read, issues, pull-requests, maintain or none. **Required.** | | `--rule RULE` | string | — | ‘allow: METHOD /path’ or ‘deny: METHOD /path’ (repeatable). | | `--rules-file FILE` | string | — | A JSON array of rules ({allow\|deny, when}), added after `--rule`. | | `--allow-graphql` | bool | `false` | Let the station use the GraphQL endpoint. | | `--acknowledge-same-user` | bool | `false` | Operator only: accept a write policy in same-user mode. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api policy --station stn_123 --grant fag_123 --preset issues --rule 'deny: DELETE /repos/**' ``` #### fleet stations forge-api revoke [Section titled “fleet stations forge-api revoke”](#fleet-stations-forge-api-revoke) Remove a grant from the station (and, when minted, revoke the token at the forge). ```text fleet stations forge-api revoke --station STATION_ID --grant GRANT_ID ``` agentpod cannot revoke a supplied token at its provider: revoke it there, then run `confirm-revoked`. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api revoke --station stn_123 --grant fag_123 ``` #### fleet stations forge-api confirm-revoked [Section titled “fleet stations forge-api confirm-revoked”](#fleet-stations-forge-api-confirm-revoked) Record that you revoked a supplied credential at its provider. ```text fleet stations forge-api confirm-revoked --station STATION_ID --grant GRANT_ID ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | -------------------------------------------------------------------- | | `--station STATION_ID` | string | — | Station ID. **Required.** | | `--grant GRANT_ID` | string | — | Grant ID, as `fleet stations forge-api list` shows it. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet stations forge-api confirm-revoked --station stn_123 --grant fag_123 ``` ## fleet station [Section titled “fleet station”](#fleet-station) One station: lifecycle, cleanup, changeset, files. ```text fleet station ... ``` The same operations as the console’s panels, each gated on a capability the station declares — see [What you can do to a station](/use/panels/). Every verb is a round trip to the node through the hub, so an offline node answers 409. Nothing is retried: whether a write that may have landed should be repeated is the caller’s decision. Subcommands: [`lifecycle`](#fleet-station-lifecycle), [`cleanup`](#fleet-station-cleanup), [`changeset`](#fleet-station-changeset), [`fs`](#fleet-station-fs). `fleet station -h` prints: ```text usage: fleet station lifecycle --station ID --action start|stop|restart fleet station cleanup plan --station ID fleet station cleanup apply --station ID --path P [--path P …] fleet station changeset status --station ID [--base REF] fleet station changeset diff --station ID --side uncommitted|committed [--path P] [--base REF] fleet station fs write --station ID --path P --from FILE|- [--base64] [--backup] fleet station fs mkdir --station ID --path P fleet station fs move --station ID --from P --to P fleet station fs delete --station ID --path P [--recursive] ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet station lifecycle --station stn_123 --action restart ``` ### fleet station lifecycle [Section titled “fleet station lifecycle”](#fleet-station-lifecycle) Start, stop or restart a station’s harness. ```text fleet station lifecycle --station ID --action start|stop|restart ``` | Flag | Type | Default | Meaning | | ------------------------------- | ------ | ------- | ------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--action start\|stop\|restart` | string | — | Start, stop or restart. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station lifecycle --station stn_123 --action restart ``` ### fleet station cleanup [Section titled “fleet station cleanup”](#fleet-station-cleanup) Find and reclaim disk a station no longer needs. ```text fleet station cleanup --station ID ... ``` Subcommands: [`plan`](#fleet-station-cleanup-plan), [`apply`](#fleet-station-cleanup-apply). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station cleanup plan --station stn_123 ``` #### fleet station cleanup plan [Section titled “fleet station cleanup plan”](#fleet-station-cleanup-plan) List what could be reclaimed, and how much. ```text fleet station cleanup plan --station ID ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station cleanup plan --station stn_123 ``` #### fleet station cleanup apply [Section titled “fleet station cleanup apply”](#fleet-station-cleanup-apply) Reclaim the named paths. ```text fleet station cleanup apply --station ID --path P [--path P ...] ``` At least one `--path` is required: an empty apply is far more likely a shell glob that matched nothing than a deliberate no-op. | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | --------------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--path P` | string | — | A path to reclaim (repeatable). **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station cleanup apply --station stn_123 --path .cache/pip ``` ### fleet station changeset [Section titled “fleet station changeset”](#fleet-station-changeset) What the agent changed in the station’s git checkout. ```text fleet station changeset --station ID ... ``` Subcommands: [`status`](#fleet-station-changeset-status), [`diff`](#fleet-station-changeset-diff). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station changeset status --station stn_123 ``` #### fleet station changeset status [Section titled “fleet station changeset status”](#fleet-station-changeset-status) Changed files, committed and uncommitted. ```text fleet station changeset status --station ID [--base REF] ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--base REF` | string | — | Ref to compare against. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station changeset status --station stn_123 --base origin/main ``` #### fleet station changeset diff [Section titled “fleet station changeset diff”](#fleet-station-changeset-diff) The diff itself, one side at a time. ```text fleet station changeset diff --station ID --side uncommitted|committed [--path P] [--base REF] ``` | Flag | Type | Default | Meaning | | ------------------------------- | ------ | ------- | --------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--side uncommitted\|committed` | string | — | Uncommitted or committed. **Required.** | | `--path P` | string | — | Limit the diff to one path. | | `--base REF` | string | — | Ref to compare against. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station changeset diff --station stn_123 --side uncommitted --path README.md ``` ### fleet station fs [Section titled “fleet station fs”](#fleet-station-fs) Write, create, move and delete files in a station. ```text fleet station fs --station ID ... ``` Subcommands: [`write`](#fleet-station-fs-write), [`mkdir`](#fleet-station-fs-mkdir), [`move`](#fleet-station-fs-move), [`delete`](#fleet-station-fs-delete). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station fs mkdir --station stn_123 --path notes ``` #### fleet station fs write [Section titled “fleet station fs write”](#fleet-station-fs-write) Write a local file (or stdin) to a path in the station. ```text fleet station fs write --station ID --path P --from FILE|- [--base64] [--backup] ``` Sent as UTF-8 by default. A file that is not valid UTF-8 is refused rather than corrupted; send it with `--base64`. | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ------------------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--path P` | string | — | Path on the station. **Required.** | | `--from FILE\|-` | string | — | Local file to send, or - for stdin. **Required.** | | `--base64` | bool | `false` | Send the file as base64 rather than utf8. | | `--backup` | bool | `false` | Keep a backup of what was overwritten. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station fs write --station stn_123 --path notes/todo.md --from todo.md echo hello | fleet station fs write --station stn_123 --path hello.txt --from - ``` #### fleet station fs mkdir [Section titled “fleet station fs mkdir”](#fleet-station-fs-mkdir) Create a directory. ```text fleet station fs mkdir --station ID --path P ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ---------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--path P` | string | — | Path on the station. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station fs mkdir --station stn_123 --path notes ``` #### fleet station fs move [Section titled “fleet station fs move”](#fleet-station-fs-move) Move or rename a path. ```text fleet station fs move --station ID --from P --to P ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ----------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--from P` | string | — | Source path on the station. **Required.** | | `--to P` | string | — | Destination path (move). **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station fs move --station stn_123 --from draft.md --to final.md ``` #### fleet station fs delete [Section titled “fleet station fs delete”](#fleet-station-fs-delete) Delete a file, or a directory with `--recursive`. ```text fleet station fs delete --station ID --path P [--recursive] ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------------------ | | `--station ID` | string | — | Station ID. **Required.** | | `--path P` | string | — | Path on the station. **Required.** | | `--recursive` | bool | `false` | Delete a directory and its contents. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet station fs delete --station stn_123 --path scratch --recursive ``` ## fleet staff [Section titled “fleet staff”](#fleet-staff) Put an agent in a station, or take it out. ```text fleet staff ``` Start with `options`: it says which harnesses, models and profiles this hub accepts, and every other verb here fails on a value it did not get from there. Subcommands: [`options`](#fleet-staff-options), [`create`](#fleet-staff-create), [`assign`](#fleet-staff-assign), [`unassign`](#fleet-staff-unassign). `fleet staff -h` prints: ```text usage: fleet staff options fleet staff create --file PATH|- fleet staff assign --station ID --file PATH|- fleet staff unassign --station ID ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet staff options ``` ### fleet staff options [Section titled “fleet staff options”](#fleet-staff-options) The harnesses, models and profiles this hub accepts. ```text fleet staff options ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet staff options ``` ### fleet staff create [Section titled “fleet staff create”](#fleet-staff-create) Create an agent from a definition document. ```text fleet staff create --file PATH|- ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ----------------------------------------------- | | `--file PATH\|-` | string | — | Agent definition, or - for stdin. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet staff create --file agent.json ``` ### fleet staff assign [Section titled “fleet staff assign”](#fleet-staff-assign) Put an agent in a station. ```text fleet staff assign --station ID --file PATH|- ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--file PATH\|-` | string | — | Assignment document, or - for stdin. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet staff assign --station stn_123 --file assignment.json ``` ### fleet staff unassign [Section titled “fleet staff unassign”](#fleet-staff-unassign) Take the agent out of a station. ```text fleet staff unassign --station ID ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet staff unassign --station stn_123 ``` ## fleet config [Section titled “fleet config”](#fleet-config) Declare what a harness setting should be, and see what it is. ```text fleet config ... ``` `set` records a declaration and writes nothing to a station; `plan`, `inspect` and `apply` are the reviewed trio that writes. See [Declared harness settings](/use/config/). Subcommands: [`settings`](#fleet-config-settings), [`show`](#fleet-config-show), [`set`](#fleet-config-set), [`unset`](#fleet-config-unset), [`drift`](#fleet-config-drift), [`opt-out`](#fleet-config-opt-out), [`opt-in`](#fleet-config-opt-in), [`plan`](#fleet-config-plan), [`inspect`](#fleet-config-inspect), [`apply`](#fleet-config-apply). `fleet config -h` prints: ```text usage: fleet config settings every setting the fleet can declare fleet config show [--node ID] the declarations themselves, as stored fleet config show --station ID one station: declared vs observed, with state fleet config set SETTING_ID --value V [--value V ...] [--station ID | --node ID] fleet config set SETTING_ID --json JSON [--station ID | --node ID] fleet config unset SETTING_ID [--station ID | --node ID] fleet config drift every station whose value differs fleet config opt-out SETTING_ID [--station KEY | --node ID] [--reason TEXT] fleet config opt-in SETTING_ID [--station KEY | --node ID] fleet config opt-out SETTING_ID [--station KEY | --node ID] --clear fleet config opt-out what is exempt, and where fleet config plan --station ID [--setting SETTING_ID] narrow the plan to one setting fleet config inspect --station ID --operation ID a plan already made, as it was reviewed fleet config apply --station ID --operation ID --plan-digest SHA256 `set` records a DECLARATION; it does not write to a station. `apply` is the verb that writes, and it refuses to run without --plan-digest: that digest must be the one `plan` printed for this operation, so a human reviewed the exact edit being written rather than whatever the current plan happens to be by the time apply runs. Only `show --station` and `drift` compare anything. Without --station, `show` returns the declaration rows and contacts no station: no observed value, no state. A fleet- or node-level declaration is one row that may apply to many stations, so comparing it means naming which station you mean. --station is accepted for any setting. A setting whose registered scope is not `profile` is NOT refused here; it is stored, and reported `out-of-scope` when the declaration is read back. A single --value is always declared as a STRING. A setting whose value is a list — every `additive-only` setting is one, a command allowlist being the first — is declared either by repeating --value once per entry: fleet config set hermes.approvals.command_allowlist \ --value "git status" --value "ls" or with --json, which takes the value exactly as JSON and is the way to declare a ONE-entry list, a number or a boolean: fleet config set hermes.approvals.command_allowlist --json '["git status"]' fleet config set hermes.approvals.timeout --json 900 --value and --json are mutually exclusive, and `set` needs one of them: a declaration with no value is not a declaration. A list-valued setting given a single --value is stored as the string it is, and every later plan for that station is refused SHAPE_UNEXPECTED by the node, so the shape matters here rather than at write time. `opt-out` stops this system writing a setting; it does not change what is already in the file. An exemption is not an undo — a station that was already drifted, or already carries a value written before the exemption existed, stays exactly as it is. `opt-in` is not the same as `--clear`. `opt-in` records "this station is NOT exempt", which overrides a node-level exemption for that one station. `--clear` forgets the exemption row entirely, so the station falls back to whatever the node says — if the node is exempt, the station is exempt again too. Use `opt-in` to pin a station in despite its node; use --clear to stop having an opinion at the station level at all. A station-level row always beats a node-level one — `opt-in` at the station overrides `opt-out` at the node, never the other way around. --station names a station by its stationKey, not its row id: a stationKey survives unadopt and re-adopt, so an exemption recorded against it still applies after the station is re-adopted, which a row id would not. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet config settings ``` ### fleet config settings [Section titled “fleet config settings”](#fleet-config-settings) Every setting the fleet can declare. ```text fleet config settings ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config settings ``` ### fleet config show [Section titled “fleet config show”](#fleet-config-show) The declarations as stored, or one station’s declared-against-observed. ```text fleet config show [--node ID] fleet config show --station ID ``` Only `--station` contacts a station and reports observed values and state; without it this returns the declaration rows. | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ----------- | | `--station ID` | string | — | Station ID. | | `--node ID` | string | — | Node ID. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config show --station stn_123 ``` ### fleet config set [Section titled “fleet config set”](#fleet-config-set) Declare a setting’s value, fleet-wide or for a node or station. ```text fleet config set SETTING_ID --value V [--value V ...] [--station ID | --node ID] fleet config set SETTING_ID --json JSON [--station ID | --node ID] ``` One `--value` declares a string; several declare a list of strings, in order. `--json` takes any JSON value, and is the only way to declare a one-entry list, a number or a boolean. Exactly one of them is required. With neither scope flag the declaration is fleet-wide. | Argument | Meaning | | ------------ | -------------------------------------- | | `SETTING_ID` | a setting from `fleet config settings` | | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | -------------------------------------- | | `--value V` | string | — | The declared value; repeat for a list. | | `--json JSON` | string | — | The declared value, exactly as JSON. | | `--station ID` | string | — | Station ID. | | `--node ID` | string | — | Node ID. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config set hermes.approvals.command_allowlist --value "git status" --value ls fleet config set hermes.approvals.timeout --json 900 --node node_123 ``` ### fleet config unset [Section titled “fleet config unset”](#fleet-config-unset) Remove a declaration at one level. ```text fleet config unset SETTING_ID [--station ID | --node ID] ``` | Argument | Meaning | | ------------ | -------------------- | | `SETTING_ID` | the declared setting | | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ----------- | | `--station ID` | string | — | Station ID. | | `--node ID` | string | — | Node ID. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config unset hermes.approvals.timeout --node node_123 ``` ### fleet config drift [Section titled “fleet config drift”](#fleet-config-drift) Every station whose value differs from its declaration. ```text fleet config drift ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config drift ``` ### fleet config opt-out [Section titled “fleet config opt-out”](#fleet-config-opt-out) Exempt a station or node from a setting, or list exemptions. ```text fleet config opt-out [--station KEY | --node ID] fleet config opt-out SETTING_ID (--station KEY | --node ID) [--reason TEXT] fleet config opt-out SETTING_ID (--station KEY | --node ID) --clear ``` Without a SETTING\_ID, lists what is exempt and where. With one, records an exemption (exactly one of `--station` or `--node`); `--clear` forgets the row instead, so the station falls back to its node. An exemption stops this system writing the setting; it does not change what is already in the file. | Argument | Meaning | | ------------ | -------------------------------------- | | `SETTING_ID` | the setting to exempt; omit it to list | | Flag | Type | Default | Meaning | | --------------- | ------ | ------- | ------------------------------------------------------------------------------------------------- | | `--station KEY` | string | — | Station key (survives unadopt/re-adopt; not a row id). | | `--node ID` | string | — | Node ID. | | `--reason TEXT` | string | — | Why this station or node is exempt. | | `--clear` | bool | `false` | Forget the exemption row instead of recording one; the station then falls back to the node level. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config opt-out hermes.approvals.timeout --station hermes:default --reason "tuned by hand" ``` ### fleet config opt-in [Section titled “fleet config opt-in”](#fleet-config-opt-in) Record that a station or node is not exempt, overriding its node. ```text fleet config opt-in SETTING_ID (--station KEY | --node ID) ``` Not the same as `opt-out --clear`: this pins a station in despite a node-level exemption; a station-level row always beats a node-level one. | Argument | Meaning | | ------------ | ----------- | | `SETTING_ID` | the setting | | Flag | Type | Default | Meaning | | --------------- | ------ | ------- | ------------------------------------------------------ | | `--station KEY` | string | — | Station key (survives unadopt/re-adopt; not a row id). | | `--node ID` | string | — | Node ID. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config opt-in hermes.approvals.timeout --station hermes:default ``` ### fleet config plan [Section titled “fleet config plan”](#fleet-config-plan) Derive the edit that would make a station match its declarations. ```text fleet config plan --station ID [--setting SETTING_ID] ``` Prints the plan with its operation id and digest. Without `--setting` it plans every setting declared for the station at any level. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--setting SETTING_ID` | string | — | Narrow the plan to one setting. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As above; also 1 when nothing is declared for the station. ```sh fleet config plan --station stn_123 ``` ### fleet config inspect [Section titled “fleet config inspect”](#fleet-config-inspect) A plan already made, exactly as it was reviewed. ```text fleet config inspect --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config inspect --station stn_123 --operation op_123 ``` ### fleet config apply [Section titled “fleet config apply”](#fleet-config-apply) Write a reviewed plan to the station. ```text fleet config apply --station ID --operation ID --plan-digest SHA256 ``` Refuses to run without the digest `plan` printed, so what is written is exactly what a person reviewed, never a plan re-derived at apply time. | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ----------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | | `--plan-digest SHA256` | string | — | Reviewed plan digest. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet config apply --station stn_123 --operation op_123 --plan-digest 3f2a… ``` ## fleet mcp-proxy [Section titled “fleet mcp-proxy”](#fleet-mcp-proxy) Which stations each node’s loopback MCP proxy serves, and their secrets. ```text fleet mcp-proxy ... ``` The proxy gives a station’s harness sessions the hub’s and SuperLibrary’s MCP tools as the station’s own agent, through a loopback URL and a per-station secret that never leave the node. A change is written to the node’s config (`mcpProxy.stations`) and applied without a restart; every change and rotation leaves an audit row per station. The hub records what was **declared** here; `list` compares it with what each node **serves**. With nothing declared, whatever the node’s config says stands, so a hand-edited config needs no migration. Needs a node at v0.1.96 or later; an older one answers `unknown`. The node side is [`apn mcp-proxy`](/reference/apn/#apn-mcp-proxy). Subcommands: [`list`](#fleet-mcp-proxy-list), [`enable`](#fleet-mcp-proxy-enable), [`disable`](#fleet-mcp-proxy-disable), [`rotate`](#fleet-mcp-proxy-rotate). `fleet mcp-proxy -h` prints: ```text usage: fleet mcp-proxy list [--node ID] every station: declared, served, state fleet mcp-proxy enable STATION_ID... serve these stations fleet mcp-proxy enable --all-eligible [--node ID] serve every station whose harness can use it fleet mcp-proxy disable STATION_ID... stop serving these stations fleet mcp-proxy rotate STATION_ID... | --node ID new secrets; sessions holding old ones are refused The node's loopback MCP proxy gives a station's harness sessions the hub's and SuperLibrary's MCP tools, as the station's own agent. Only Hermes, Claude Code, Codex, opencode, Muse and Kilo stations can use it: OpenClaw's and Pi's ACP adapters take no HTTP MCP servers in session/new, and enabling one is refused. A change is written to the node's config and applied without a restart, and leaves an audit row per station. A state of "drifted" means what was declared here and what the node serves disagree — re-run enable or disable to settle it. Secrets never leave the node. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet mcp-proxy list ``` ### fleet mcp-proxy list [Section titled “fleet mcp-proxy list”](#fleet-mcp-proxy-list) Every station: eligible, declared, served, and its state. ```text fleet mcp-proxy list [--node ID] ``` `state` is `on` or `off`; `drifted` when the declaration and the node disagree (a hand edit, a reset config); `ineffective` when a station is served but its harness takes no HTTP MCP servers; `unknown` when the node could not be asked. `unadoptedStations` are ids the node’s config names that are not adopted stations of that node. `drifted` at the top counts all three kinds. | Flag | Type | Default | Meaning | | ----------- | ------ | ------- | --------------- | | `--node ID` | string | — | Only this node. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet mcp-proxy list --node node_123 ``` ### fleet mcp-proxy enable [Section titled “fleet mcp-proxy enable”](#fleet-mcp-proxy-enable) Serve the named stations, or every eligible one. ```text fleet mcp-proxy enable STATION_ID... fleet mcp-proxy enable --all-eligible [--node ID] ``` Only Hermes, Claude Code, Codex, opencode, Muse and Kilo stations are eligible. Naming an OpenClaw or Pi station refuses the whole request (422) and changes nothing: their ACP adapters take no HTTP MCP servers in `session/new`. `--all-eligible` skips them and lists them under `skipped`. A station already served keeps its secret, so its open sessions keep working. | Argument | Meaning | | ------------ | -------------------------------------------------------- | | `STATION_ID` | a station id from `fleet stations`; several may be named | | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------- | | `--all-eligible` | bool | `false` | Every adopted station whose harness takes HTTP MCP servers (Hermes, Claude Code, Codex, opencode, Muse, Kilo). | | `--node ID` | string | — | With `--all-eligible`: only this node’s stations. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** 0 when every station is served; 1 otherwise — 404 an unknown station, 422 an ineligible one, 409 a node offline or too old, 502 a node that failed. ```sh fleet mcp-proxy enable stn_123 stn_456 fleet mcp-proxy enable --all-eligible ``` ### fleet mcp-proxy disable [Section titled “fleet mcp-proxy disable”](#fleet-mcp-proxy-disable) Stop serving the named stations. ```text fleet mcp-proxy disable STATION_ID... ``` Applied at once: the station’s next proxied request is refused, and its next session gets no proxied servers. | Argument | Meaning | | ------------ | ---------------------------------- | | `STATION_ID` | a station id; several may be named | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As `enable`. ```sh fleet mcp-proxy disable stn_123 ``` ### fleet mcp-proxy rotate [Section titled “fleet mcp-proxy rotate”](#fleet-mcp-proxy-rotate) New secrets for the named stations, or every station a node serves. ```text fleet mcp-proxy rotate STATION_ID... fleet mcp-proxy rotate --node ID ``` Secrets otherwise persist across node restarts, so a session kept open across one keeps working. Rotate when that is not wanted: a session holding an old secret is refused (401) from its next request, and new sessions get the new one. The secrets are never sent to the hub. | Argument | Meaning | | ------------ | ------------------------------------------- | | `STATION_ID` | a served station’s id; several may be named | | Flag | Type | Default | Meaning | | ----------- | ------ | ------- | --------------------------------------- | | `--node ID` | string | — | Every station this node’s proxy serves. | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As `enable`; 409 also when the node’s proxy does not serve a named station. ```sh fleet mcp-proxy rotate stn_123 fleet mcp-proxy rotate --node node_123 ``` ## fleet skills [Section titled “fleet skills”](#fleet-skills) Skill artifacts, releases, cohorts, canaries and placement. ```text fleet skills ... ``` Every mutation prints the hub’s reviewed record; read it before the matching apply. No plan here ever turns into an implicit apply. The workflow is on [Managed skills](/use/skills/). Subcommands: [`artifacts`](#fleet-skills-artifacts), [`artifact`](#fleet-skills-artifact), [`releases`](#fleet-skills-releases), [`cohorts`](#fleet-skills-cohorts), [`upload`](#fleet-skills-upload), [`release`](#fleet-skills-release), [`cohort`](#fleet-skills-cohort), [`canary`](#fleet-skills-canary), [`station`](#fleet-skills-station), [`native`](#fleet-skills-native). `fleet skills -h` prints: ```text Usage: fleet skills fleet skills artifacts fleet skills artifact delete --id ARTIFACT_ID fleet skills releases fleet skills cohorts fleet skills upload --harness H --profile P ARCHIVE.tgz fleet skills release import RELEASE.json fleet skills cohort create --release ID --digest SHA256 --station ID fleet skills canary plan --cohort ID --release ID --digest SHA256 --station ID fleet skills canary inspect --cohort ID --release ID --digest SHA256 --station ID --operation ID fleet skills canary apply --cohort ID --release ID --digest SHA256 --station ID --operation ID --plan-digest SHA256 fleet skills station plan --station ID --artifact ARTIFACT_ID fleet skills station verify --station ID --profile PROFILE fleet skills station rollback-plan --station ID --profile PROFILE fleet skills station inspect --station ID --operation ID fleet skills station apply --station ID --operation ID --plan-digest SHA256 fleet skills native plan --station ID --profile PROFILE --action activate|deactivate|rollback fleet skills native inspect --station ID --operation ID fleet skills native apply --station ID --operation ID --plan-digest SHA256 fleet skills native verify --station ID --profile PROFILE Every mutation returns the hub's reviewed record. Read that response before an apply command; this CLI never turns a plan into an implicit apply. ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 0. ```sh fleet skills artifacts ``` ### fleet skills artifacts [Section titled “fleet skills artifacts”](#fleet-skills-artifacts) Uploaded skill archives. ```text fleet skills artifacts ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills artifacts ``` ### fleet skills artifact [Section titled “fleet skills artifact”](#fleet-skills-artifact) Act on one uploaded artifact. ```text fleet skills artifact delete --id ARTIFACT_ID ``` Subcommands: [`delete`](#fleet-skills-artifact-delete). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills artifact delete --id art_123 ``` #### fleet skills artifact delete [Section titled “fleet skills artifact delete”](#fleet-skills-artifact-delete) Delete an uploaded artifact. ```text fleet skills artifact delete --id ARTIFACT_ID ``` | Flag | Type | Default | Meaning | | ------------------ | ------ | ------- | -------------------------- | | `--id ARTIFACT_ID` | string | — | Artifact ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills artifact delete --id art_123 ``` ### fleet skills releases [Section titled “fleet skills releases”](#fleet-skills-releases) Imported catalog releases. ```text fleet skills releases ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills releases ``` ### fleet skills cohorts [Section titled “fleet skills cohorts”](#fleet-skills-cohorts) Canary cohorts. ```text fleet skills cohorts ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills cohorts ``` ### fleet skills upload [Section titled “fleet skills upload”](#fleet-skills-upload) Upload a skill archive for one harness and profile. ```text fleet skills upload --harness H --profile P ARCHIVE.tgz ``` | Argument | Meaning | | ------------- | ------------------------- | | `ARCHIVE.tgz` | the gzipped skill archive | | Flag | Type | Default | Meaning | | ------------- | ------ | ------- | ----------------------------- | | `--harness H` | string | — | Target harness. **Required.** | | `--profile P` | string | — | Skill profile. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills upload --harness hermes --profile default skills-hermes.tgz ``` ### fleet skills release [Section titled “fleet skills release”](#fleet-skills-release) Import a release record. ```text fleet skills release import RELEASE.json ``` Subcommands: [`import`](#fleet-skills-release-import). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills release import release.json ``` #### fleet skills release import [Section titled “fleet skills release import”](#fleet-skills-release-import) Import a six-harness release, pinning each archive to an uploaded artifact. ```text fleet skills release import RELEASE.json ``` Every archive the record names must already be uploaded, matched by harness, profile and SHA-256; a missing one stops the import before anything is sent. | Argument | Meaning | | -------------- | ---------------------------------------------------- | | `RELEASE.json` | a complete release record covering all six harnesses | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills release import release.json ``` ### fleet skills cohort [Section titled “fleet skills cohort”](#fleet-skills-cohort) Create a canary cohort. ```text fleet skills cohort create --release ID --digest SHA256 --station ID ``` Subcommands: [`create`](#fleet-skills-cohort-create). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills cohort create --release rel_123 --digest 3f2a… --station stn_123 ``` #### fleet skills cohort create [Section titled “fleet skills cohort create”](#fleet-skills-cohort-create) Create a cohort of one canary station for a release. ```text fleet skills cohort create --release ID --digest SHA256 --station ID ``` | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | -------------------------------- | | `--release ID` | string | — | Release ID. **Required.** | | `--digest SHA256` | string | — | Release digest. **Required.** | | `--station ID` | string | — | Canary station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills cohort create --release rel_123 --digest 3f2a… --station stn_123 ``` ### fleet skills canary [Section titled “fleet skills canary”](#fleet-skills-canary) Plan, inspect and apply a release on a cohort’s canary. ```text fleet skills canary --cohort ID --release ID --digest SHA256 --station ID ... ``` Subcommands: [`plan`](#fleet-skills-canary-plan), [`inspect`](#fleet-skills-canary-inspect), [`apply`](#fleet-skills-canary-apply). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills canary plan --cohort coh_123 --release rel_123 --digest 3f2a… --station stn_123 ``` #### fleet skills canary plan [Section titled “fleet skills canary plan”](#fleet-skills-canary-plan) Plan the canary install. ```text fleet skills canary plan --cohort ID --release ID --digest SHA256 --station ID ``` | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | ----------------------------- | | `--cohort ID` | string | — | Cohort ID. **Required.** | | `--release ID` | string | — | Release ID. **Required.** | | `--digest SHA256` | string | — | Release digest. **Required.** | | `--station ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills canary plan --cohort coh_123 --release rel_123 --digest 3f2a… --station stn_123 ``` #### fleet skills canary inspect [Section titled “fleet skills canary inspect”](#fleet-skills-canary-inspect) Read a canary operation. ```text fleet skills canary inspect --cohort ID --release ID --digest SHA256 --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | ----------------------------- | | `--cohort ID` | string | — | Cohort ID. **Required.** | | `--release ID` | string | — | Release ID. **Required.** | | `--digest SHA256` | string | — | Release digest. **Required.** | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills canary inspect --cohort coh_123 --release rel_123 --digest 3f2a… --station stn_123 --operation op_123 ``` #### fleet skills canary apply [Section titled “fleet skills canary apply”](#fleet-skills-canary-apply) Apply the reviewed canary plan. ```text fleet skills canary apply --cohort ID --release ID --digest SHA256 --station ID --operation ID --plan-digest SHA256 ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ----------------------------------- | | `--cohort ID` | string | — | Cohort ID. **Required.** | | `--release ID` | string | — | Release ID. **Required.** | | `--digest SHA256` | string | — | Release digest. **Required.** | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | | `--plan-digest SHA256` | string | — | Reviewed plan digest. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills canary apply --cohort coh_123 --release rel_123 --digest 3f2a… --station stn_123 --operation op_123 --plan-digest 9c1e… ``` ### fleet skills station [Section titled “fleet skills station”](#fleet-skills-station) Managed skill installs on one station. ```text fleet skills station --station ID ... ``` Subcommands: [`plan`](#fleet-skills-station-plan), [`verify`](#fleet-skills-station-verify), [`rollback-plan`](#fleet-skills-station-rollback-plan), [`inspect`](#fleet-skills-station-inspect), [`apply`](#fleet-skills-station-apply). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station verify --station stn_123 --profile default ``` #### fleet skills station plan [Section titled “fleet skills station plan”](#fleet-skills-station-plan) Plan installing an uploaded artifact on a station. ```text fleet skills station plan --station ID --artifact ARTIFACT_ID ``` | Flag | Type | Default | Meaning | | ------------------------ | ------ | ------- | ----------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--artifact ARTIFACT_ID` | string | — | Uploaded artifact ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station plan --station stn_123 --artifact art_123 ``` #### fleet skills station verify [Section titled “fleet skills station verify”](#fleet-skills-station-verify) Check a profile’s installed skills against the record. ```text fleet skills station verify --station ID --profile PROFILE ``` | Flag | Type | Default | Meaning | | ------------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--profile PROFILE` | string | — | Profile. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station verify --station stn_123 --profile default ``` #### fleet skills station rollback-plan [Section titled “fleet skills station rollback-plan”](#fleet-skills-station-rollback-plan) Plan rolling a profile back to its previous install. ```text fleet skills station rollback-plan --station ID --profile PROFILE ``` | Flag | Type | Default | Meaning | | ------------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--profile PROFILE` | string | — | Profile. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station rollback-plan --station stn_123 --profile default ``` #### fleet skills station inspect [Section titled “fleet skills station inspect”](#fleet-skills-station-inspect) Read a station skill operation. ```text fleet skills station inspect --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station inspect --station stn_123 --operation op_123 ``` #### fleet skills station apply [Section titled “fleet skills station apply”](#fleet-skills-station-apply) Apply a reviewed station skill plan. ```text fleet skills station apply --station ID --operation ID --plan-digest SHA256 ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ----------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | | `--plan-digest SHA256` | string | — | Reviewed plan digest. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills station apply --station stn_123 --operation op_123 --plan-digest 9c1e… ``` ### fleet skills native [Section titled “fleet skills native”](#fleet-skills-native) Native placement: skills in the harness’s own skill directory. ```text fleet skills native --station ID ... ``` Refused unless the node’s operator enabled it with [`apn native-skills enable`](/reference/apn/#apn-native-skills-enable). Subcommands: [`plan`](#fleet-skills-native-plan), [`inspect`](#fleet-skills-native-inspect), [`apply`](#fleet-skills-native-apply), [`verify`](#fleet-skills-native-verify). **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills native verify --station stn_123 --profile default ``` #### fleet skills native plan [Section titled “fleet skills native plan”](#fleet-skills-native-plan) Plan activating, deactivating or rolling back native placement. ```text fleet skills native plan --station ID --profile PROFILE --action activate|deactivate|rollback ``` | Flag | Type | Default | Meaning | | ----------------------------------------- | ------ | ------- | -------------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--profile PROFILE` | string | — | Profile. **Required.** | | `--action activate\|deactivate\|rollback` | string | — | Native placement action. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills native plan --station stn_123 --profile default --action activate ``` #### fleet skills native inspect [Section titled “fleet skills native inspect”](#fleet-skills-native-inspect) Read a native placement operation. ```text fleet skills native inspect --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills native inspect --station stn_123 --operation op_123 ``` #### fleet skills native apply [Section titled “fleet skills native apply”](#fleet-skills-native-apply) Apply a reviewed native placement plan. ```text fleet skills native apply --station ID --operation ID --plan-digest SHA256 ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ----------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | | `--plan-digest SHA256` | string | — | Reviewed plan digest. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills native apply --station stn_123 --operation op_123 --plan-digest 9c1e… ``` #### fleet skills native verify [Section titled “fleet skills native verify”](#fleet-skills-native-verify) Check a profile’s natively placed skills. ```text fleet skills native verify --station ID --profile PROFILE ``` | Flag | Type | Default | Meaning | | ------------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--profile PROFILE` | string | — | Profile. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet skills native verify --station stn_123 --profile default ``` ## fleet plugins [Section titled “fleet plugins”](#fleet-plugins) Review and apply harness plugin changes on a station. ```text fleet plugins --station ID ... ``` The node plans, you read the plan, and `apply` sends only the digest you reviewed. Nothing here restarts a station. Refused unless the node’s operator enabled it with [`apn plugin-management enable`](/reference/apn/#apn-plugin-management-enable). Subcommands: [`plan`](#fleet-plugins-plan), [`show`](#fleet-plugins-show), [`inspect`](#fleet-plugins-inspect), [`apply`](#fleet-plugins-apply), [`history`](#fleet-plugins-history), [`inventory`](#fleet-plugins-inventory). `fleet plugins -h` prints: ```text usage: fleet plugins plan --station ID --action enable|disable fleet plugins show --station ID --operation ID fleet plugins inspect --station ID --operation ID fleet plugins apply --station ID --operation ID --plan-digest SHA256 fleet plugins history --station ID fleet plugins inventory --station ID ``` **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet plugins inventory --station stn_123 ``` ### fleet plugins plan [Section titled “fleet plugins plan”](#fleet-plugins-plan) Plan enabling or disabling the live-streaming plugin. ```text fleet plugins plan --station ID --action enable|disable ``` | Flag | Type | Default | Meaning | | -------------------------- | ------ | ------- | -------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--action enable\|disable` | string | — | Enable or disable. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins plan --station stn_123 --action enable ``` ### fleet plugins show [Section titled “fleet plugins show”](#fleet-plugins-show) Read a plugin operation. ```text fleet plugins show --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins show --station stn_123 --operation op_123 ``` ### fleet plugins inspect [Section titled “fleet plugins inspect”](#fleet-plugins-inspect) Ask the node to re-check a planned operation against the profile now. ```text fleet plugins inspect --station ID --operation ID ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins inspect --station stn_123 --operation op_123 ``` ### fleet plugins apply [Section titled “fleet plugins apply”](#fleet-plugins-apply) Apply the reviewed plan. ```text fleet plugins apply --station ID --operation ID --plan-digest SHA256 ``` | Flag | Type | Default | Meaning | | ---------------------- | ------ | ------- | ----------------------------------- | | `--station ID` | string | — | Station ID. **Required.** | | `--operation ID` | string | — | Operation ID. **Required.** | | `--plan-digest SHA256` | string | — | Reviewed plan digest. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins apply --station stn_123 --operation op_123 --plan-digest 9c1e… ``` ### fleet plugins history [Section titled “fleet plugins history”](#fleet-plugins-history) Past plugin operations on a station. ```text fleet plugins history --station ID ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins history --station stn_123 ``` ### fleet plugins inventory [Section titled “fleet plugins inventory”](#fleet-plugins-inventory) What the station’s harness has installed. ```text fleet plugins inventory --station ID ``` | Flag | Type | Default | Meaning | | -------------- | ------ | ------- | ------------------------- | | `--station ID` | string | — | Station ID. **Required.** | **Needs:** A person’s token: `$AGENTPOD_TOKEN`, or the credential `fleet login` stored. The hub refuses an agent’s token on these routes, and answers only about what the signed-in person owns. ```sh fleet plugins inventory --station stn_123 ``` ## fleet settings [Section titled “fleet settings”](#fleet-settings) Hub-wide settings: signup, transcription, speech. ```text fleet settings ... ``` Subcommands: [`show`](#fleet-settings-show), [`signup`](#fleet-settings-signup), [`transcription`](#fleet-settings-transcription), [`speech`](#fleet-settings-speech). `fleet settings -h` prints: ```text usage: fleet settings show fleet settings signup [enable|disable] fleet settings transcription [show|set --file PATH|- |test] fleet settings speech [show|set --file PATH|- |test] ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet settings show ``` ### fleet settings show [Section titled “fleet settings show”](#fleet-settings-show) Every hub-wide setting. ```text fleet settings show ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings show ``` ### fleet settings signup [Section titled “fleet settings signup”](#fleet-settings-signup) Whether new people may sign up (older hubs). ```text fleet settings signup [enable|disable] ``` With no argument it reads; each direction has to be named, so a forgotten word never reopens signup. On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. Subcommands: [`enable`](#fleet-settings-signup-enable), [`disable`](#fleet-settings-signup-disable). **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As above; 2 for any word other than enable or disable. ```sh fleet settings signup ``` #### fleet settings signup enable [Section titled “fleet settings signup enable”](#fleet-settings-signup-enable) Open signup (older hubs). ```text fleet settings signup enable ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings signup enable ``` #### fleet settings signup disable [Section titled “fleet settings signup disable”](#fleet-settings-signup-disable) Close signup (older hubs). ```text fleet settings signup disable ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings signup disable ``` ### fleet settings transcription [Section titled “fleet settings transcription”](#fleet-settings-transcription) The voice-note transcription default. Bare, it shows the setting. ```text fleet settings transcription [show|set --file PATH|-|test] ``` See [Voice notes](/use/voice/). Subcommands: [`show`](#fleet-settings-transcription-show), [`set`](#fleet-settings-transcription-set), [`test`](#fleet-settings-transcription-test). **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings transcription ``` #### fleet settings transcription show [Section titled “fleet settings transcription show”](#fleet-settings-transcription-show) The transcription setting. ```text fleet settings transcription show ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings transcription show ``` #### fleet settings transcription set [Section titled “fleet settings transcription set”](#fleet-settings-transcription-set) Replace the transcription setting with a document. ```text fleet settings transcription set --file PATH|- ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ------------------------------------------------ | | `--file PATH\|-` | string | — | Settings document, or - for stdin. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings transcription set --file transcription.json ``` #### fleet settings transcription test [Section titled “fleet settings transcription test”](#fleet-settings-transcription-test) Ask the hub to try the configured transcription service. ```text fleet settings transcription test ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings transcription test ``` ### fleet settings speech [Section titled “fleet settings speech”](#fleet-settings-speech) The spoken-reply default. Bare, it shows the setting. ```text fleet settings speech [show|set --file PATH|-|test] ``` See [Voice notes](/use/voice/). Subcommands: [`show`](#fleet-settings-speech-show), [`set`](#fleet-settings-speech-set), [`test`](#fleet-settings-speech-test). **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings speech ``` #### fleet settings speech show [Section titled “fleet settings speech show”](#fleet-settings-speech-show) The speech setting. ```text fleet settings speech show ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings speech show ``` #### fleet settings speech set [Section titled “fleet settings speech set”](#fleet-settings-speech-set) Replace the speech setting with a document. ```text fleet settings speech set --file PATH|- ``` | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | ------------------------------------------------ | | `--file PATH\|-` | string | — | Settings document, or - for stdin. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings speech set --file speech.json ``` #### fleet settings speech test [Section titled “fleet settings speech test”](#fleet-settings-speech-test) Ask the hub to try the configured speech service. ```text fleet settings speech test ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet settings speech test ``` ## fleet bridge [Section titled “fleet bridge”](#fleet-bridge) The board roster: which agent claims from which board. ```text fleet bridge ... ``` This table is the gate on an agent doing anything at all: a staffed, online agent claims no card until a row points it at a board. No verb here prints a credential. See [Working a board](/use/boards/). Subcommands: [`list`](#fleet-bridge-list), [`add`](#fleet-bridge-add), [`set`](#fleet-bridge-set), [`rm`](#fleet-bridge-rm), [`related-work`](#fleet-bridge-related-work). `fleet bridge -h` prints: ```text usage: fleet bridge list fleet bridge add --key K --board B --station S --token T [--mcp-token M] [--mode M] [--concurrency N] [--profile P] [--wait-ms N] [--enabled true|false] fleet bridge set KEY [--board B] [--station S] [--mode M] [--enabled true|false] [--token T] [--mcp-token M] [--concurrency N] [--profile P] [--wait-ms N] fleet bridge rm KEY fleet bridge related-work BOARD on|off ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet bridge list ``` ### fleet bridge list [Section titled “fleet bridge list”](#fleet-bridge-list) Every roster row. ```text fleet bridge list ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet bridge list ``` ### fleet bridge add [Section titled “fleet bridge add”](#fleet-bridge-add) Add a roster row. ```text fleet bridge add --key K --board B --station S --token T [--mcp-token M] [--mode M] [--concurrency N] [--profile P] [--wait-ms N] [--enabled true|false] ``` The four required flags are reported together when missing. | Flag | Type | Default | Meaning | | ----------------------- | ------ | ------- | ----------------------------------------------------------------------------------- | | `--key K` | string | — | Roster key (add). **Required.** | | `--board B` | string | — | SuperPipeline board id. **Required.** | | `--station S` | string | — | Station id the work runs on. **Required.** | | `--token T` | string | — | SuperPipeline agent token (claims). **Required.** | | `--mcp-token M` | string | — | Run-scoped token the harness spends. | | `--mode M` | string | — | Permission mode. | | `--concurrency N` | int | `0` | Max cards at once; 0 leaves the hub’s value. | | `--profile P` | string | — | Profile key. | | `--wait-ms N` | int | `0` | How long a permission request waits for an answer, in ms; 0 leaves the hub’s value. | | `--enabled true\|false` | string | — | True or false. | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet bridge add --key reviewer --board brd_123 --station stn_123 --token "$SP_TOKEN" ``` ### fleet bridge set [Section titled “fleet bridge set”](#fleet-bridge-set) Change some fields of a roster row; the rest stay. ```text fleet bridge set KEY [--board B] [--station S] [--mode M] [--enabled true|false] [--token T] [--mcp-token M] [--concurrency N] [--profile P] [--wait-ms N] ``` Refuses a change with no fields, which the hub would otherwise answer 200 for and change nothing. | Argument | Meaning | | -------- | -------------- | | `KEY` | the roster key | | Flag | Type | Default | Meaning | | ----------------------- | ------ | ------- | ----------------------------------------------------------------------------------- | | `--board B` | string | — | SuperPipeline board id. | | `--station S` | string | — | Station id the work runs on. | | `--token T` | string | — | SuperPipeline agent token (claims). | | `--mcp-token M` | string | — | Run-scoped token the harness spends. | | `--mode M` | string | — | Permission mode. | | `--concurrency N` | int | `0` | Max cards at once; 0 leaves the hub’s value. | | `--profile P` | string | — | Profile key. | | `--wait-ms N` | int | `0` | How long a permission request waits for an answer, in ms; 0 leaves the hub’s value. | | `--enabled true\|false` | string | — | True or false. | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet bridge set reviewer --board brd_456 ``` ### fleet bridge rm [Section titled “fleet bridge rm”](#fleet-bridge-rm) Remove a roster row. ```text fleet bridge rm KEY ``` | Argument | Meaning | | -------- | -------------- | | `KEY` | the roster key | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet bridge rm reviewer ``` ### fleet bridge related-work [Section titled “fleet bridge related-work”](#fleet-bridge-related-work) Switch a board’s Related prior work section on or off. ```text fleet bridge related-work BOARD on|off ``` The earlier work SuperLibrary finds for a claimed card, put into the card’s prompt as reference material. On for every board until switched off; the next claim on the board reads the new value. Anything but `on` or `off` exits 2 without calling the hub. | Argument | Meaning | | -------- | ------------------------- | | `BOARD` | a SuperPipeline `brd_` id | | `STATE` | `on` or `off` | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet bridge related-work brd_0000000000000001 off ``` ## fleet principals [Section titled “fleet principals”](#fleet-principals) Identities, service principals and their credentials. ```text fleet principals ... ``` Only `list` works against a current hub; the rest are managed by the account service. Subcommands: [`list`](#fleet-principals-list), [`suspend`](#fleet-principals-suspend), [`restore`](#fleet-principals-restore), [`add-service`](#fleet-principals-add-service), [`add-credential`](#fleet-principals-add-credential), [`revoke-credential`](#fleet-principals-revoke-credential). `fleet principals -h` prints: ```text usage: fleet principals list fleet principals suspend ID fleet principals restore ID fleet principals add-service HANDLE --client CLIENT --scope SCOPE[,SCOPE] fleet principals add-credential PRN_ID --client CLIENT fleet principals revoke-credential SVC_ID ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet principals list ``` ### fleet principals list [Section titled “fleet principals list”](#fleet-principals-list) Who exists in this workspace, read through the account service. ```text fleet principals list ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals list ``` ### fleet principals suspend [Section titled “fleet principals suspend”](#fleet-principals-suspend) Suspend a principal, reversibly (older hubs). ```text fleet principals suspend ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ----------- | | `ID` | a `prn_` id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals suspend prn_123 ``` ### fleet principals restore [Section titled “fleet principals restore”](#fleet-principals-restore) Undo a suspension (older hubs). ```text fleet principals restore ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ----------- | | `ID` | a `prn_` id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals restore prn_123 ``` ### fleet principals add-service [Section titled “fleet principals add-service”](#fleet-principals-add-service) Create a service principal with its first credential (older hubs). ```text fleet principals add-service HANDLE --client CLIENT --scope SCOPE[,SCOPE] ``` The response carries the credential’s secret once; it cannot be read again. On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | -------------------- | | `HANDLE` | the service’s handle | | Flag | Type | Default | Meaning | | ----------------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--client CLIENT` | string | — | The HUB\_OAUTH\_CLIENTS id whose audiences its tokens carry. **Required.** | | `--scope SCOPE[,SCOPE]` | string | — | Comma-separated grant scopes, e.g. evidence:read. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals add-service evidence-reader --client superwitness --scope evidence:read ``` ### fleet principals add-credential [Section titled “fleet principals add-credential”](#fleet-principals-add-credential) Add a credential to a service principal: the first half of a rotation (older hubs). ```text fleet principals add-credential PRN_ID --client CLIENT ``` The old credential keeps working until `revoke-credential`, so the consumer can switch with no gap. On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | -------------------------- | | `PRN_ID` | the service principal’s id | | Flag | Type | Default | Meaning | | ----------------- | ------ | ------- | -------------------------------------------------------------------------- | | `--client CLIENT` | string | — | The HUB\_OAUTH\_CLIENTS id whose audiences its tokens carry. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals add-credential prn_123 --client superwitness ``` ### fleet principals revoke-credential [Section titled “fleet principals revoke-credential”](#fleet-principals-revoke-credential) Revoke one service credential (older hubs). ```text fleet principals revoke-credential SVC_ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | -------------------------- | | `SVC_ID` | the credential’s `svc_` id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet principals revoke-credential svc_123 ``` ## fleet grants [Section titled “fleet grants”](#fleet-grants) Dispatch authority, as a document (older hubs). ```text fleet grants ... ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. See [Dispatch and grants](/use/grants/). Subcommands: [`list`](#fleet-grants-list), [`show`](#fleet-grants-show), [`set`](#fleet-grants-set), [`rm`](#fleet-grants-rm). `fleet grants -h` prints: ```text usage: fleet grants list fleet grants show PRINCIPAL_ID fleet grants set PRINCIPAL_ID --file PATH|- fleet grants rm PRINCIPAL_ID ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet grants list ``` ### fleet grants list [Section titled “fleet grants list”](#fleet-grants-list) Every grant. ```text fleet grants list ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet grants list ``` ### fleet grants show [Section titled “fleet grants show”](#fleet-grants-show) One principal’s grant. ```text fleet grants show PRINCIPAL_ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------------- | ----------- | | `PRINCIPAL_ID` | a `prn_` id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet grants show prn_123 ``` ### fleet grants set [Section titled “fleet grants set”](#fleet-grants-set) Replace a principal’s grant with a document. ```text fleet grants set PRINCIPAL_ID --file PATH|- ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------------- | ----------- | | `PRINCIPAL_ID` | a `prn_` id | | Flag | Type | Default | Meaning | | ---------------- | ------ | ------- | --------------------------------------------- | | `--file PATH\|-` | string | — | Grant document, or - for stdin. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet grants set prn_123 --file grant.json ``` ### fleet grants rm [Section titled “fleet grants rm”](#fleet-grants-rm) Remove a principal’s grant. ```text fleet grants rm PRINCIPAL_ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------------- | ----------- | | `PRINCIPAL_ID` | a `prn_` id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet grants rm prn_123 ``` ## fleet users [Section titled “fleet users”](#fleet-users) People: list, show, ban, unban, role (older hubs). ```text fleet users ... ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. Subcommands: [`list`](#fleet-users-list), [`show`](#fleet-users-show), [`ban`](#fleet-users-ban), [`unban`](#fleet-users-unban), [`role`](#fleet-users-role). `fleet users -h` prints: ```text usage: fleet users list fleet users show ID fleet users ban ID --reason "why" [--expires RFC3339] fleet users unban ID fleet users role ID --role ROLE ``` **Needs:** A workspace admin’s token. Anyone else is refused with 403. **Exit status:** As below for each verb; with no verb, prints the usage and exits 2. ```sh fleet users list ``` ### fleet users list [Section titled “fleet users list”](#fleet-users-list) Every account. ```text fleet users list ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet users list ``` ### fleet users show [Section titled “fleet users show”](#fleet-users-show) One account. ```text fleet users show ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ------------- | | `ID` | the user’s id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet users show usr_123 ``` ### fleet users ban [Section titled “fleet users ban”](#fleet-users-ban) Ban an account, with a recorded reason. ```text fleet users ban ID --reason "why" [--expires RFC3339] ``` A reason is required: a ban nobody can review later is one the next person cannot lift with confidence. On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ------------- | | `ID` | the user’s id | | Flag | Type | Default | Meaning | | ------------------- | ------ | ------- | ----------------------------------------- | | `--reason TEXT` | string | — | Why, recorded with the ban. **Required.** | | `--expires RFC3339` | string | — | RFC3339 instant the ban lifts itself. | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet users ban usr_123 --reason "shared credentials" --expires 2026-12-01T00:00:00Z ``` ### fleet users unban [Section titled “fleet users unban”](#fleet-users-unban) Lift a ban. ```text fleet users unban ID ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ------------- | | `ID` | the user’s id | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet users unban usr_123 ``` ### fleet users role [Section titled “fleet users role”](#fleet-users-role) Set an account’s role. ```text fleet users role ID --role ROLE ``` On a hub that signs in through an account service (every current hub), this record is managed there: the hub answers 410 `managed_by_org_plane` with the account service’s URL, which this prints, and the command exits 1. It works as described only against an older hub that still issues its own tokens. | Argument | Meaning | | -------- | ------------- | | `ID` | the user’s id | | Flag | Type | Default | Meaning | | ------------- | ------ | ------- | ------------------------------ | | `--role ROLE` | string | — | The role to set. **Required.** | **Needs:** A workspace admin’s token. Anyone else is refused with 403. ```sh fleet users role usr_123 --role admin ``` ## fleet update [Section titled “fleet update”](#fleet-update) Replace this binary with the newest release. ```text fleet update [--check] [--force] ``` Fetches the `agentpod-fleet` asset for this platform and verifies it against the release’s `SHA256SUMS`. There is no service to restart: the next invocation is the new binary. | Flag | Type | Default | Meaning | | --------- | ---- | ------- | ------------------------------------------------------ | | `--check` | bool | `false` | Resolve and report current/latest version, no changes. | | `--force` | bool | `false` | Update even when already on the latest version. | **Needs:** Nothing from the hub. Network access to GitHub releases, and write access to the binary’s own path. **Exit status:** 0 when up to date, updated, or (with `--check`) reported. 1 when the release could not be fetched or verified. ```sh fleet update --check fleet update ``` ## fleet skill [Section titled “fleet skill”](#fleet-skill) Print the skill that ships inside this binary. ```text fleet skill fleet skill install [--dir DIR] [--json] ``` Prints a `SKILL.md` for agents: what `fleet` is, how to sign in, every verb, and which verbs change shared state and so need an operator’s say-so. It is written from this binary’s own verbs and stamped with its version, and a test fails when a verb exists that it does not mention. Not to be confused with `fleet skills`, which manages skill artifacts on the fleet. Subcommands: [`install`](#fleet-skill-install). **Needs:** Nothing. Runs without a credential or a hub. **Exit status:** 0. 2 for an unknown subverb. ```sh fleet skill | less ``` ### fleet skill install [Section titled “fleet skill install”](#fleet-skill-install) Write the skill where an agent will find it. ```text fleet skill install [--dir DIR] [--json] ``` Writes `DIR/agentpod-fleet/SKILL.md` atomically (a staged file in the same directory, then a rename) and prints the path. Running it again rewrites only that file. `DIR` defaults to `~/.claude/skills`. `fleet update` refreshes a copy at the default directory and never creates one. | Flag | Type | Default | Meaning | | ----------- | ------ | ------- | ------------------------------------------------------------------ | | `--dir DIR` | string | — | The skills directory to install under (default \~/.claude/skills). | | `--json` | bool | `false` | Print the path written as JSON. | **Needs:** Nothing. Runs without a credential or a hub. **Exit status:** 0 once written. 1 when the file could not be written. 2 for a bad flag. ```sh fleet skill install fleet skill install --dir ./skills --json ``` ## fleet version [Section titled “fleet version”](#fleet-version) Print the version and platform. ```text fleet version ``` **Needs:** Nothing. Runs without a credential or a hub. **Exit status:** 0. ```sh fleet version # agentpod-fleet v0.1.90 darwin/arm64 ``` ## fleet help [Section titled “fleet help”](#fleet-help) Print the verb list and how credentials work. ```text fleet help fleet -h fleet ``` All three print the same text. A group’s own usage is `fleet -h`; for verbs with flags, `fleet -h` prints them with their defaults. **Needs:** Nothing. Runs without a credential or a hub. **Exit status:** 0. ```sh fleet help ``` # Concepts > Nodes, stations, harnesses, principals, grants and the hub — the words the rest of the documentation assumes. Eight words carry most of AgentPod. They are worth ten minutes. ## Node [Section titled “Node”](#node) A **machine** in your fleet: a VPS, a laptop, a container, a box under a desk. A node runs the `agentpod-node` agent, which dials *out* to the hub and holds that connection open. Nothing ever dials *in* to a node. That is not a detail — it is why a laptop on hotel Wi-Fi, or a host behind CGNAT with no routable address, is reachable at all. A node’s credential says “I am this host”. It is not an authority to operate the fleet, and AgentPod deliberately never lets it become one. ## Harness [Section titled “Harness”](#harness) The **software that actually runs an agent**: Claude Code, Codex, OpenCode, Hermes, OpenClaw, Pi. AgentPod does not replace a harness and does not run agents itself. For each one it ships a *descriptor* that knows how to ask that harness what it is running, where its config lives, where its logs go, and how to start and stop it. The descriptor wraps the harness’s own native interface rather than reinventing it. ## Station [Section titled “Station”](#station) One **runtime instance** on a node — a single place where an agent works. A station belongs to exactly one node and one harness, and carries a station key that is unique within that harness. Stations **nest**. A harness that manages sub-runtimes appears as a parent station with children beneath it, which is why the console shows a tree under each node rather than a flat list. Detection finds stations; **adoption** is what puts one under management. Until you adopt it, AgentPod knows a station exists and does nothing else with it. Each station declares **capabilities** — which of the panels it supports. A request for a capability the station has not declared is refused at the hub, before anything reaches the machine. ## Purpose [Section titled “Purpose”](#purpose) What a station is *for*: `personal`, `work`, or any name you choose. It is not where the station runs. This exists because a node name carries purpose only by accident of how a particular fleet was built. A station’s own purpose is the one anything reads; a node’s purpose is only the default handed to a station at adoption that has none of its own. ## Principal [Section titled “Principal”](#principal) **Who is acting.** Every principal has a `prn_`-prefixed id, a handle, and one of three kinds. Principals live in your account service, which issues their ids and their tokens: | Kind | Is | | --------- | ------------------------------------ | | `human` | A person | | `agent` | An agent that acts on its own behalf | | `service` | A system component | The kind is not decoration — it is load-bearing at the door. The hub’s MCP endpoint serves different tools to an agent than to a person, and the endpoint that lists the agents you may dispatch refuses an agent token outright. An agent enumerating its siblings is an agent doing reconnaissance, so that answer is read from the signed token and from nothing the caller can influence. ## Grant [Section titled “Grant”](#grant) **Who may spend an agent’s time.** Messaging an agent, approving a tool call for it, and putting a card in front of it are all the same act under different names, and all three are checked against a `mayDispatch` claim the account service signed into the caller’s token. A grant is read from the signed token and from nothing the caller sent. A missing claim means *permitted nothing* — never *permitted everything*. See [Dispatch and grants](/use/grants/). Operating a station is a **different** authority from dispatching the agent in it. Reading logs is not dispatching; sending a message is. ## Room [Section titled “Room”](#room) If the bridge is on, every adopted station has a **chat identity and a room**. It is how an agent whose harness never spoke a messaging protocol can be reached from a phone — and how permission questions, failed turns and a board’s approval gates get in front of a person who is not at a console. See [Talking to an agent in a room](/use/rooms/). ## Hub [Section titled “Hub”](#hub) The **one place that knows everything about the fleet**: the registry of nodes and stations, the broker that routes a request to the right node, enrollment, audit, and the provisioning drivers. It is a Bun service over Postgres, and you host it. It does not issue identity. People, agents, services and workspaces belong to the account service, and the hub checks every token it is shown against that service’s published keys, without calling it — see [Authentication](/build/auth/). ## How they fit [Section titled “How they fit”](#how-they-fit) ```plaintext hub └── node a machine, dialing out └── station a runtime on it, adopted ├── station (nested, where the harness has sub-runtimes) └── station ``` A **principal** acts on those stations, bounded by a **grant**. A **harness** is what the station is an instance of. With the bridge on, each station also has a **room**. ## Next [Section titled “Next”](#next) * [Nodes](/use/nodes/) and [Stations](/use/stations/) — operating both * [What you can do to a station](/use/panels/) — the panels, and what each one needs * [Dispatch and grants](/use/grants/) — who may act on whom # Your first node > Install the node agent on a machine and watch it appear in the console. A **node** is a machine in your fleet. Enrolling one takes a hub, an enrollment token, and one command on the target host. This page assumes you already have access to a running hub and its console. Since v0.1.92, the hub requires `ORG_PLANE_*` configuration and a credential from an external account service that is not public source. The deployment runbook alone is not a complete self-hosting path for current releases. ## 1. Get an enrollment token [Section titled “1. Get an enrollment token”](#1-get-an-enrollment-token) In the console, open **Nodes** and create an enrollment token. The plaintext is shown once. It proves to the hub that *this* machine was invited. A fresh token is **single-use and expires after an hour**. It is consumed atomically, so two machines racing to enroll with the same token cannot both win — exactly one does, and the other is refused. Once a token is bound to a node, re-presenting it is allowed and resumes that node. That is what makes `apn enroll` idempotent: running it again on a machine that is already enrolled is a friendly no-op rather than an error. ## 2. Install the agent [Section titled “2. Install the agent”](#2-install-the-agent) On the machine you want to enroll: ```sh curl -fsSL https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/install.sh \ | sudo bash -s -- https://hub.example.com ``` The script downloads the right binary for the platform, installs it, registers a service, and enrolls. Binaries are published for linux and darwin on both amd64 and arm64. Without root, install for your user instead: ```sh curl -fsSL https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/install.sh \ | bash -s -- --user https://hub.example.com ``` The user install puts the service under your login session — a systemd `--user` unit on Linux, a LaunchAgent on macOS — so it starts when you log in rather than when the machine boots. ## 3. Check it [Section titled “3. Check it”](#3-check-it) ```sh apn status ``` This prints two blocks: whether the service is installed, enabled and running, and whether the hub is reachable with a valid credential. It exits `0` only if **both** are true, so it works directly in a health check or a cron job. The node should now appear in the console as online. ## 4. See what’s on it [Section titled “4. See what’s on it”](#4-see-whats-on-it) ```sh apn detect ``` This prints the harness stations found on the host as JSON. It needs no hub connection and no account — it is the fastest way to confirm the agent sees what you expected before you go looking in the console. The v0.1.98 release described on the landing page ships detection for **Hermes, OpenClaw, Claude Code, Codex, OpenCode, Pi, Muse Code and Kilo**. Detection does not imply the same capabilities for every harness; Muse Code and Kilo capabilities beyond detection have not been checked. ## 5. Adopt a station [Section titled “5. Adopt a station”](#5-adopt-a-station) Detection finds runtimes; it does not take them over. In the console, open the node and **adopt** the stations you want to manage. Adoption is what puts a station in the registry and makes the panels available. ## Next [Section titled “Next”](#next) * [Concepts](/start/concepts/) — what nodes, stations and harnesses actually are * [Nodes](/use/nodes/) — running the service day to day * [Checking for exposure](/use/scan/) — `apn scan`, which needs no hub at all # What AgentPod is > A fleet and facilities console for agent runtimes — what it manages, and what it deliberately does not. AgentPod manages **the environments AI agents live in**, wherever they run. An agent needs a place to work: a filesystem, a config file, somewhere its logs go, a process that can be started and stopped. Once you have more than one agent, on more than one machine, that place stops being something you can hold in your head. AgentPod is the console for those places. It is **attach-first**. You point it at runtimes you already have and it detects them where they sit. Nothing moves, and nothing is rewritten to a new format. ## What it gives you per runtime [Section titled “What it gives you per runtime”](#what-it-gives-you-per-runtime) * **Filesystem** — browse, read and write the workspace * **Logs** — tail them live * **Terminal** — an interactive shell, through the hub * **Config** — edit the harness’s own configuration file * **Health** — whether it is up, and what it reports about itself * **Lifecycle** — start, stop and restart * **Cleanup** — plan a disk reclaim, inspect the plan, then apply it * **Changesets** — what the agent has actually changed in its working tree * **Activity** — an audit trail of what was done to the station, and by whom * **Skills** — what a station actually has, installed under review and verified in a session * **Plugins** — reviewed harness plugin changes, planned and applied * **Provisioning** — create new runtimes on Docker, Cloudflare, Modal or Fly Not every station offers all of these. Each one is gated on a capability the harness declares, and a station that does not declare it is refused before the request ever reaches the machine. ## And a way to talk to them [Section titled “And a way to talk to them”](#and-a-way-to-talk-to-them) The console is for operating the ground an agent stands on. The other half of AgentPod is for working *with* the agent: turn on the **bridge** and every adopted station gets a chat identity and a room of its own. You message it from a client you already have. The agent’s reply comes back as that agent. It can be sent a voice note and answer with one; it asks for permission before running a tool and you reply with a number; when a turn fails you get the provider’s own sentence rather than silence. The harness itself never learns any of this is happening. See [Talking to an agent in a room](/use/rooms/). ## Three tiers [Section titled “Three tiers”](#three-tiers) ```plaintext Operator (web console) │ HTTPS + WSS ▼ ┌───────────────────────────┐ outbound WSS tunnels (NAT-friendly) │ AgentPod Hub │◄────────┬────────────┬──────────────┐ │ (Bun + Hono + Postgres) │ │ │ │ │ • node/station registry │ node-agent node-agent node-agent │ • connection broker │ (VPS) (laptop) (provisioned) │ • enrollment + auth │ │ • provisioning drivers │ │ • audit + activity log │ └───────────────────────────┘ ``` **node-agent** is a single static Go binary, installed per host. It dials out to the hub and executes contract verbs locally. It opens no inbound ports, which is why a laptop behind CGNAT is reachable at all. **The hub** is the registry, the connection broker, and the place authentication and audit live. It is a Bun + Hono service over Postgres, and you host it yourself. **The console** is a static SvelteKit app: node list, then the station tree under each node, then the panels above. ## What it is not [Section titled “What it is not”](#what-it-is-not) It is **not a harness**, and it does not run your agents. Claude Code, Codex, Hermes and the rest do that. AgentPod manages the ground they stand on. It is **not an orchestrator**. Deciding what work an agent should do next is a different job in a different plane — that is what [SuperPipeline](https://docs.superpipeline.dev) is for. AgentPod answers “where does this agent live, and is it healthy”, not “what should it work on”. The two do join up: AgentPod can **claim work from a board** and run it on a station, and the decisions that work stops on come back to a room where you can answer them. That is a bridge between two planes rather than one plane growing into the other — see [Working a board](/use/boards/). ## Next [Section titled “Next”](#next) * [Enroll your first node](/start/first-node/) — a machine in the fleet in about five minutes * [Concepts](/start/concepts/) — nodes, stations, harnesses and principals * [Talking to an agent in a room](/use/rooms/) — the bridge, if you want the other half # Attaching an editor > Make a station on another machine look like a local agent to any ACP client. ACP is the protocol editors speak to coding agents. `apn acp` makes a station on **another machine** look like a local agent to any client that speaks it — Zed, JetBrains, anything else. ## How it works [Section titled “How it works”](#how-it-works) Your editor spawns `apn acp` and talks ACP over its stdin and stdout. Those frames are piped to the hub, which does the protocol work and routes them to the station. The node dialled out, so this reaches stations behind NAT or CGNAT that your editor could never connect to directly. ```sh apn acp --list # stations you can attach to apn acp --station # attach apn acp --station --session # resume a session ``` `--hub` points at a hub other than the default. ## It needs no node [Section titled “It needs no node”](#it-needs-no-node) `apn acp` does not assume this machine is enrolled — a laptop can install `apn` purely as a client, attach to stations elsewhere, and never be part of the fleet itself. (`apn scan` is the other command that works on an unenrolled machine; see [Checking for exposure](/use/scan/).) ## Tokens [Section titled “Tokens”](#tokens) Prefer `$AGENTPOD_TOKEN`, which is what `--token` defaults to. A token passed as an argument lands in your shell history and in the process list. ## Sessions [Section titled “Sessions”](#sessions) Sessions are recorded, and an agent can read back its own transcripts through [the MCP tools](/build/mcp/) — `agentpod_my_sessions` to find a session, then `agentpod_my_transcript` to read it. # Working a board > The SuperPipeline bridge — a hub that claims work from a board and runs it on a station, and the gates and questions that come back to a room. AgentPod answers “where does this agent live, and is it healthy”. Deciding *what* an agent should work on is a different job in a different plane, which is what [SuperPipeline](https://docs.superpipeline.dev) is for. The bridge joins them: the hub can **claim work from a board** and run it on a station. It is outbound-only. It adds no route, opens no port, and a hub with it off is indistinguishable from one built before it existed. ## The roster [Section titled “The roster”](#the-roster) The bridge runs from a roster: which agent claims from which board, onto which station, and with what credential. It lives in the hub’s database and is edited in the console under **Admin → Bridge**, or from the terminal: ```sh fleet bridge list fleet bridge add --key … fleet bridge set … fleet bridge rm ``` There is no environment variable and **no restart**. The bridge brings its running loops into line with the roster every ten seconds, so an agent added at noon starts claiming at noon. | what you do | what happens | | --------------------------------- | --------------------------------------------------------------- | | add an agent | a loop starts within a tick | | disable or remove one | it **finishes the card it is holding**, then stops | | edit one, or replace a credential | the loop drains as above, then is rebuilt with the new settings | **A roster key is not an identity.** It is a dispatch label; the authority comes from the token attached to it. And a credential **cannot be read back** — from the console or the API. The read surface answers whether a token is present and nothing else. Rotating one is a replace, and it takes effect on the next tick. ## What the loop does [Section titled “What the loop does”](#what-the-loop-does) Claim a card, run it as a turn on the station, report back. The timings are deliberate rather than configurable: | | | | ------------ | ----------------------------------------------------------------------------------------------------- | | poll | every 5s after a cycle that found nothing | | backoff | 30s after a failed cycle — claim, release, claim is not a fix | | heartbeat | every 60s while a card is being worked, against the board’s 15-minute reclaim | | turn timeout | 30 minutes for one prompt turn; on expiry the run is failed **on the board** rather than left hanging | A lost lease is ordinary — the card was reclaimed, and the loop claims again. One condition halts a loop permanently: being told a run belongs to **another** agent. That means two things believe they hold the same work, and continuing would have one write over the other. ## When the agent reports for itself [Section titled “When the agent reports for itself”](#when-the-agent-reports-for-itself) Give a roster entry an MCP credential and its harness gets SuperPipeline’s own tools **inside the session**. The agent can then attach a reference, ask a question, or complete the card itself, rather than having the bridge infer an outcome from the shape of its reply. This is the better arrangement where the harness supports it: the thing that did the work is the thing that reports it. ## Gates and questions come back to a room [Section titled “Gates and questions come back to a room”](#gates-and-questions-come-back-to-a-room) Work on a board stops on a human twice: at an **approval gate**, and when an agent **asks a question** mid-run. Both reach a room, so neither needs the web app. A gate arrives as a card with its options, and your answer decides it on the board. A question arrives the same way — the agent’s words and the options it offered — and replying with the option’s number answers it. Two rules worth knowing: * **Only a human may decide a gate.** An agent’s token is refused, and this is a product boundary rather than a setting: an agent holding both halves of that pair makes every “a human decided this” record unverifiable. * **A question answered somewhere else settles here.** If it was answered in the web app, or retired by the agent asking a newer one, the card in the room stops standing rather than inviting an answer to something already decided. Questions reach a room by push where the board is configured for it, and by a reconciliation sweep otherwise — so a board that has not been reconfigured is served late rather than not at all. **Buttons only.** A question whose agent offered no options cannot be answered from the room, and the room says where it can be answered rather than pretending. **Some questions approve an exact operation.** A strict decision (a Salaam agent asking to write a file) and a harness permission routed through a board both carry what they would do: the summary, the files and their previews, or the command. An updated app shows it and binds your answer to that exact version; an answer for a version that has since changed, or one that arrives after the decision expired, is refused. Replying with a number can **decline** such a question but never approve it — the reply cannot show you what you would be approving — so the room points you to an updated app or the board instead. **A gate may offer a choice of versions.** An agent drafting a post can hand over up to three versions; the gate then asks you to choose one. An updated app shows each version and approves the one you pick; request changes and reject send the whole set back. Each version is shown within an equal share of the room’s space, so a long one never crowds out the others, and a version that could not be shown in full here cannot be chosen from the room — choose it on the board. An older app, the phone’s Lock Screen and a reply by number never approve any version: they can only send the set back, and say where to choose. Once a version is approved, the room’s receipt names it. **A gate may need more than one approver.** A workspace owner or admin can set a stage to need two to five approvals — from the stage’s deciders, or from the workspace’s owners and admins — before its gate passes. Each approval short of that is said in the room as a reply to the gate, for example “Approved by asha — 1 of 2. Waiting for 1 more.”, and the gate stays open; the approval that reaches the number passes it, and the room’s receipt says so. Each person counts once: approving again is refused with a reply saying it is still waiting for others, and an approval from someone who does not count for that gate is refused with a reply saying why. One rejection or request for changes still ends the gate at once. The delivery is manual unless everyone who approved chose automatic. **An approval may wait for its release time.** A workspace owner or admin can set a stage to hold its approvals for a cooling-off period (5 minutes to 7 days), and a post can say it must not go out before a set time. The gate then says so before anyone approves (“If approved, it goes ahead after Mon 16 Nov, 09:00 UTC”). An approval is recorded at once but does not act until that time: the room’s receipt says “Approved by rakesh — goes ahead after … unless pulled back”, and nothing is posted or advanced until then. Until it acts, anyone who may decide the gate, or a workspace owner or admin, can **pull it back** with a comment, from the app or on the board; that sends the work back for changes, and the room replies “Pulled back by … — returned for changes: …”. When the time comes the room replies “Released — it went ahead.” or, if what was approved changed or expired meanwhile, “Did not go ahead: …”. Pulling back after it went ahead is refused, and nobody can move the time. **A gate may be decided by checks.** A workspace owner or admin can give a stage up to five machine checks: CI on the card’s linked pull request, or a verdict on its latest run. The checks run before anyone is asked. While they run the card waits on the stage and nothing is posted; once every check has passed, the gate opens as usual and lists them (“Checks: build (CI) ✓, quality (verdict) ✓.”). A check that fails sends the card back for changes, and the board’s room says so (“CI check “build” failed on “Ship it” — returned for changes.”). Checks still pending past the stage’s timeout are reminded and then escalated like any decision: the workspace’s owners and admins are invited, and a card lists the checks so they can **override** one, with a comment, from the app or on the board. The room then says who overrode which check and why. Agents never override, and whoever produced the work cannot override a check on it. ## A card waiting on you [Section titled “A card waiting on you”](#a-card-waiting-on-you) A card also stops on a person when its agent keeps failing, says it is **blocked**, or is **not authorised** to go on. That reaches the board’s room too, as its own card: what is wrong, what the agent said, and three choices. * **Resume here** — with a required instruction for the next attempt. * **Resume at an earlier stage** — one choice per earlier stage, with the same instruction. * **Archive** — with an optional remark. Moving the card forward is not offered from chat: a move skips the stage’s work and cancels its gates, which deserves the board. Your answer goes to the board as you, and names the exact wait you were shown — if the card was resumed, archived or moved on the board meanwhile, or now waits on something else, the board refuses it, nothing changes, and the room says so. A card that parks again arrives as a new card. Whoever settles it, here or on the board, the card in the room closes. An older app shows the card as text with a link to the board; an updated app shows the buttons. ## When nobody answers [Section titled “When nobody answers”](#when-nobody-answers) A board can give a stage a **timeout**: remind after so long, escalate after longer. It applies to everything waiting on that stage — its gate, an agent’s question, a delivery follow-up, a waiting card. The board keeps the clock; nothing is ever approved, rejected or resumed because time passed. * **Each card says when.** A gate, question, follow-up or waiting card with a timeout carries its deadline, so an updated app can show “escalates in 3 h”. * **The reminder** is a short reply to the card in the board room: *Still waiting: “Launch post” has waited 3 h for an approval.* Once per decision. * **The escalation** invites the workspace’s **owners and admins** — as your workspace’s Accounts settings list them, and only those who have linked a Matrix account — into the board room, then replies to the card: *Escalated to workspace admins after 6 h …*, with a link to the card. Once per decision. An invited admin sees what the room sees and answers like anyone in it; the board judges every answer by its usual rules. Each admin is invited to a board once: somebody who leaves the room is not invited back by the next escalation. Because the room is encrypted, an admin cannot read what was posted before they joined, so when they join, each card still waiting is **moved below**: posted again in full — preview, options, deadline — with the escalation under it. The copy below is the one that counts; the earlier card says *Moved below* and an answer to it is refused, so nothing is answered twice. If the board cannot be read at that moment, only the escalation (with its link) is posted again. A board that more than one workspace on the hub knows is never escalated, and a card the room never showed is not replied to. A gate whose approval subject **expires** before anyone approves it is closed by the board; the card in the room closes as *Expired*, and the board parks the card for a person (it arrives as a waiting card). ## SuperToolkit approvals [Section titled “SuperToolkit approvals”](#supertoolkit-approvals) When a SuperToolkit write needs approval and the host is connected to SuperPipeline, the request reaches the board’s room as an **approval request**: no card, no stage — what the agent wants to do (the service, the capability, the target, every argument in full, the station and the agent), with **Allow once** and **Deny**. Only **workspace owners and admins** can allow it; anyone else is told so and nothing changes, and the agent that asked can never decide its own request. An updated app binds your answer to exactly the request it showed; a reply with a number can deny it but never allow it. SuperToolkit checks the decision again before it acts. ## Next [Section titled “Next”](#next) * [Talking to an agent in a room](/use/rooms/) — the bridge these cards arrive on * [Dispatch and grants](/use/grants/) — who may answer for which agent * [SuperPipeline’s own docs](https://docs.superpipeline.dev) — boards, stages and gates # apn and fleet > Two binaries — the resident node daemon and the client that acts as you — and why their credentials never mix. AgentPod ships two command-line binaries. `apn` (`agentpod-node`) is the resident daemon installed on an enrolled host. `fleet` (`agentpod-fleet`) is a separate client you run anywhere that is **not** an enrolled node — a laptop, CI, an agent’s own workspace. Every command, subcommand, flag and environment variable is in the generated references: [`apn` reference](/reference/apn/) and [`fleet` reference](/reference/fleet/). This page is the why; those are the what. ## Two binaries, not two modes [Section titled “Two binaries, not two modes”](#two-binaries-not-two-modes) `apn` acts on **this machine**: `status`, `start`, `stop`, `logs`, `service`, `telemetry`, `enroll`, `run`, the harness plugin installers, `detect`, `scan`, `acp`, `update`. Those that talk to the hub use the credential `apn enroll` stored on this host, which says *“I am this host.”* `fleet` acts on the fleet **as you** — everything from `whoami` to rolling a release to editing a grant. It uses a token held by a person or an agent: the one `fleet login` obtains from your workspace’s account service, or `$AGENTPOD_TOKEN`. ```sh apn status # how is this machine? fleet whoami # who am I? ``` `apn` and `fleet` are separate binaries built from the same repository and sharing no code — `apn` links none of `fleet`’s code, and `fleet` links none of `apn`’s. A fleet command **never falls back to the node’s credential**, and that is now structural rather than conventional: `fleet` cannot reach the node’s credential at all, and `apn` cannot reach the stored fleet token — `apn` links none of the code that reads `fleet login`’s token file. `apn acp` is the one exception, and it is narrower than it looks: it accepts a principal token you hand it explicitly, via `$AGENTPOD_TOKEN` or `--token`, never the file on disk. A node secret asserts which host you are; it was never an authority to operate a fleet, and treating it as one would mean that rooting any laptop in the fleet hands over the whole fleet. The two credentials are stored separately, in separate config directories, and are never substituted for one another. ## Installing `fleet` [Section titled “Installing fleet”](#installing-fleet) ```sh curl -fsSL https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/install-fleet.sh | sh ``` This installs a client only — it enrols nothing and installs no service. `apn` has its own installer; see [Enrolling a node](/use/nodes/). ## Skill for agents [Section titled “Skill for agents”](#skill-for-agents) `fleet` carries a skill for agents inside the binary: what `fleet` is, how to sign in, every verb, and which verbs change shared state and need an operator’s say-so. It is stamped with the version that printed it. ```sh fleet skill # print SKILL.md fleet skill install # write ~/.claude/skills/agentpod-fleet/SKILL.md fleet skill install --dir DIR # write DIR/agentpod-fleet/SKILL.md ``` The installer can do it in one step: set `SKILL_DIR` and it runs the freshly installed `fleet skill install --dir "$SKILL_DIR"` (a binary that predates the verb is reported, and the install still succeeds). ```sh curl -fsSL https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/install-fleet.sh | SKILL_DIR=~/.claude/skills sh ``` `fleet update` refreshes a skill already installed at the default directory, so it does not fall behind the binary; it never creates one. ## Signing in [Section titled “Signing in”](#signing-in) ```sh fleet login ``` `fleet login` signs in through your workspace’s **account service**. It prints a page and a code, opens the page if it can, and waits while you confirm the code in a browser. Any browser will do — the one on your laptop works for a `fleet` running over SSH — and `BROWSER=none` stops it trying to open one. The browser opens **once**, not once per lapse. A token lasts five minutes; approving the code also stores a **device credential** for this machine, and every later command exchanges it for a fresh token without a browser. The account service issues, lists and revokes device credentials, so that is where you manage them: ```sh fleet devices # says where this machine's devices are listed: the account service's Devices page fleet logout # signs this machine out: deletes the stored token and device credential ``` `fleet logout` is **local**. It deletes both files but does not revoke the device credential, which belongs to the account service — it prints the Devices page where you can. Revoke a machine you no longer have from that page, from any browser. `fleet devices revoke` does not revoke there either: it points at the same page and exits 1, because nothing was revoked. (A hub old enough to issue its own tokens signs you in through its own browser page instead, and there `fleet devices` and `fleet devices revoke` list and revoke at the hub.) Then: ```sh fleet whoami # who the stored token says you are fleet whoami --json # the same, for scripts fleet nodes # the fleet's nodes fleet agents # the agents you may dispatch fleet stats # fleet totals fleet activity # recent fleet activity ``` Set `$AGENTPOD_HUB` to talk to a hub other than the default, and `$AGENTPOD_DEVICE_NAME` to name this machine in the device list. ### What `fleet agents` actually answers [Section titled “What fleet agents actually answers”](#what-fleet-agents-actually-answers) Not “every agent in the fleet” — **the agents this token may dispatch**. That answer is read from a claim the hub signed into the token itself. It is not a query parameter, not a header, and not derived from anything sent alongside the token, so there is nothing in the request to tamper with. An agent’s token is refused here outright, whatever it may dispatch. Enumerating your siblings is reconnaissance, and the authority to *ask an agent to work* was never the authority to *find out what else exists*. If you have been granted nothing, you get an empty list rather than an error. That is the truth, and it is something you can act on. ## The rest of `fleet` [Section titled “The rest of fleet”](#the-rest-of-fleet) Everything the hub exposes should be reachable from here. If something is not, that is a gap rather than a decision — with two deliberate exceptions: anything that acts as **this machine** rather than as you (that is `apn`), and the interactive surfaces — a terminal, an ACP session — which are a console’s job rather than a script’s. ### Nodes and stations [Section titled “Nodes and stations”](#nodes-and-stations) ```sh fleet nodes # the fleet's nodes fleet nodes update [--node ] # roll the newest release to every node fleet nodes telemetry [--endpoint |--off] # each node's OpenTelemetry export (admin) fleet invite # mint a token a machine presents to `apn enroll` fleet stations detected --node # what the node reports right now fleet stations list --node # adopted stations, with their ids fleet stations adopt --node --key [--key …] fleet stations unadopt --station ``` **A detected station is not an agent until it is adopted.** Adopting re-detects on the node first, so a key that has gone away is not adopted from a stale list. `fleet nodes update` rolls a release without needing the hub’s root credential. ### One station [Section titled “One station”](#one-station) ```sh fleet station lifecycle --station --action start|stop|restart fleet station cleanup plan --station fleet station cleanup apply --station --path

[--path

…] fleet station changeset status --station [--base ] fleet station changeset diff --station --side uncommitted|committed [--path

] fleet station fs write|mkdir|move|delete --station … ``` The same operations as the console’s panels, each gated on a capability the station declares. See [What you can do to a station](/use/panels/). ### Git identity [Section titled “Git identity”](#git-identity) ```sh fleet stations git-identity --station # what it can push to forge as fleet stations grant-push --station # give it a forge push key fleet stations revoke-push --station # take that key away ``` **Push access is granted per station and never by adopting one.** Most stations never touch git, and a forge key for every station is an account nobody uses and a key nobody revokes. The keypair is generated **on the node** and the private half never leaves it — `grant-push` asks for the public half and registers that. The hub holds no secret. ### Forge API access [Section titled “Forge API access”](#forge-api-access) ```sh fleet stations forge-api list --station # state, mode, account, isolation fleet stations forge-api grant --station --provider forgejo \ --instance https://forge.example --repo org/repo --preset issues # minted: no credential pbpaste | fleet stations forge-api grant --station --provider github \ --instance https://github.com --repo org/repo --mode supplied --expect-login octo-bot fleet stations forge-api verify|rotate|revoke|confirm-revoked --station --grant fleet stations forge-api secret --station --grant # re-supply, from stdin fleet stations forge-api policy --station --grant --preset issues --rule 'deny: DELETE /repos/**' ``` **Operator only, and no credential on the command line.** Every verb but `list` needs the operator seat and the control pair’s reach over the agent. A supplied token or an App’s fields are read from stdin, sealed on this machine to the station’s broker key, and only the envelope is sent; there is no `--token` flag. The modes, presets, rules and limits are in [Stations](/use/stations/#forge-api-access). ### Staffing [Section titled “Staffing”](#staffing) ```sh fleet staff options fleet staff create --file |- fleet staff assign --station --file |- fleet staff unassign --station ``` Putting an agent in a station, or taking it out. ### Skills [Section titled “Skills”](#skills) ```sh fleet skills … # artifacts, releases, cohorts, canaries, native placement ``` Its own page: [Managed skills](/use/skills/). []() ### Plugins [Section titled “Plugins”](#plugins) ```sh fleet plugins inventory --station fleet plugins plan --station --action enable|disable fleet plugins show --station --operation fleet plugins inspect --station --operation fleet plugins apply --station --operation --plan-digest fleet plugins history --station ``` The same plan → inspect → apply shape as skills: the node plans, you read the plan, and the apply sends only the digest you reviewed. **Nothing here restarts a station.** These are the harness plugins behind [voice replies](/use/voice/), live turn reporting, and [error reporting](/use/errors/). ### Authority [Section titled “Authority”](#authority) ```sh fleet principals list # who exists in this workspace ``` People, roles, bans, signup, grants and service principals are **managed by your workspace’s account service**. The `fleet users`, `fleet grants`, `fleet settings signup` and `fleet principals suspend|restore|add-service|add-credential|revoke-credential` verbs remain for hubs that predate it; a current hub answers them with `410` and the account service’s URL. See the [`fleet` reference](/reference/fleet/#fleet-principals) and [Dispatch and grants](/use/grants/). ### The board bridge [Section titled “The board bridge”](#the-board-bridge) ```sh fleet bridge list fleet bridge add --key … fleet bridge set … fleet bridge rm ``` Which agent claims from which board, onto which station. See [Working a board](/use/boards/). ### Hub settings and substrate [Section titled “Hub settings and substrate”](#hub-settings-and-substrate) ```sh fleet settings show fleet settings signup|transcription|speech … fleet runtimes list|providers|create|start|stop|rm ``` `settings` is hub-wide configuration: the [voice](/use/voice/) defaults for transcription and speech. `runtimes` is the substrate nodes run on. ### Keeping it current [Section titled “Keeping it current”](#keeping-it-current) ```sh fleet update [--check] fleet version ``` The binary a person invokes drifts more than the one a service runs, not less. ## Commands that need no enrolment [Section titled “Commands that need no enrolment”](#commands-that-need-no-enrolment) ```sh apn scan apn acp --list apn acp --station ``` `apn scan` checks this machine’s agent runtimes for exposure, and needs nothing at all — no hub, no token, no network. It runs from a downloaded binary on a machine that has never been near AgentPod. See [Checking for exposure](/use/scan/). `apn acp` attaches a local ACP editor to a station on another machine. Like `fleet`, it takes a person’s token rather than this host’s credential, so a laptop can use `apn`’s `acp` command purely as a client without being enrolled. See [Attaching an editor](/use/acp/). ## Tokens on the command line [Section titled “Tokens on the command line”](#tokens-on-the-command-line) `fleet` takes its token from `$AGENTPOD_TOKEN` or from the file `fleet login` writes. There is no `--token` flag on it. `apn acp` does accept `--token`, and you should generally not use it: a token in an argument lands in your shell history and in the process list. Prefer `$AGENTPOD_TOKEN`, which is what the flag defaults to anyway. (`apn enroll --token` is a different thing — that is a single-use *enrollment* token for the machine, not a principal’s token.) ## Getting help [Section titled “Getting help”](#getting-help) ```sh apn help # apn's commands, grouped apn -h # one apn command in detail fleet help # fleet's commands fleet -h # the same text ``` `apn`‘s help text is generated from a single table in the binary, so `apn help ` and `apn -h` cannot disagree with each other. `fleet`, `fleet help` and `fleet -h` all print the same block, and `fleet -h` prints that verb’s usage. Both binaries’ verb lists come from the same tables the [`apn`](/reference/apn/) and [`fleet`](/reference/fleet/) reference pages are generated from, and a test fails when a command or flag has no entry. Help flags are checked **before** anything happens: `apn stop -h` prints help and does not stop the service, `apn service -h` cannot install or uninstall anything, and `fleet login -h` prints help without opening a browser. # Declared harness settings > Declaring what a harness setting should be, the three policies that govern how it may be reconciled, writing a declaration to a station through plan/inspect/apply, and the states a station can report back. A harness — Hermes, OpenClaw, Claude Code, Codex, OpenCode, Pi — keeps its own configuration, in its own file, in its own shape. AgentPod does not own that file. What it can do is let the fleet **declare** what a registered setting should be, write that declaration to a station through a reviewed operation, and tell you where each station’s actual value stands against what was declared. ## A declaration is not a write [Section titled “A declaration is not a write”](#a-declaration-is-not-a-write) ```sh fleet config set hermes.approvals.mode --value strict ``` **This records a declaration. It does not touch a station.** Nothing on disk changes when this command returns — the hub remembers that the fleet *wants* `hermes.approvals.mode` to be `strict`, at whichever level you declared it. Writing a harness’s own config file is a separate, reviewed operation — `plan`, then `apply` — described below in [Writing a declaration](#writing-a-declaration-plan-inspect-apply). The gap between declaring and applying is deliberate, not a missing feature. A harness persists an operator’s own decisions into the same file a declaration targets — see `additive-only` below — so `set` itself should never move a file unasked, and nothing reconciles on a timer either: a sweep running on a tick would race the harness or silently undo what an operator just changed through it. A user who treats `set` as something that changed a machine has been misled by it; treat it as writing a line into a policy document, not into the harness. Only `apply`, naming the exact plan a human reviewed, writes anything. ```sh fleet config unset hermes.approvals.mode ``` removes a declaration. Undeclaring a level that has nothing declared on it is a no-op, not an error. ## Scopes and levels [Section titled “Scopes and levels”](#scopes-and-levels) A declaration targets exactly **one level**: a station, a node, or the whole fleet (both null). Levels resolve with station the most specific and fleet the least — a station-level declaration wins over a node-level one, which wins over a fleet-wide one, for the same setting. ```sh fleet config set hermes.approvals.mode --value strict # fleet-wide fleet config set hermes.approvals.mode --value strict --node nod_123 # one node fleet config set hermes.approvals.mode --value strict --station st_abc # one station ``` A setting also carries its own **scope** — `profile`, `project`, or `user` — which says where it actually lives inside the harness’s document, independent of which level you declared it at. ### Declaring a value that is not a string [Section titled “Declaring a value that is not a string”](#declaring-a-value-that-is-not-a-string) A single `--value` is declared as a **string**. The declaration is stored as JSON and the node refuses a value whose shape its registry does not expect, so the shape has to be right here rather than at write time. For a list-valued setting — every `additive-only` setting is one — repeat `--value` once per entry, or use `--json` for the value exactly as JSON: ```sh # a list, by repeating --value fleet config set hermes.approvals.command_allowlist --value "git status" --value "ls" # a ONE-entry list, a number, a boolean — the forms repeating cannot express fleet config set hermes.approvals.command_allowlist --json '["git status"]' fleet config set hermes.approvals.timeout --json 900 ``` `--value` and `--json` are mutually exclusive, and `set` needs one of them. Declaring a list-valued setting with a single `--value` stores the string it is, and every later `plan` for that station is then refused `SHAPE_UNEXPECTED` — so if a plan says a declared value is not a list of strings, this is why. ### Why a station is not a config scope, except for Hermes [Section titled “Why a station is not a config scope, except for Hermes”](#why-a-station-is-not-a-config-scope-except-for-hermes) For five of the six harnesses, a station is a *project path*, while the harness’s configuration lives per *user*. Declaring a setting “for this station” would be declaring it for a document the station doesn’t actually own — the user’s config file is shared across every project that user opens, so a station-scoped declaration on, say, Claude Code or Codex has no document to land in. Hermes is the exception: there, a station **is** a profile, and a profile is exactly the document Hermes’s settings live in. That is the only harness where declaring a setting at the station level corresponds to a real, single document. `--station` is **accepted for any setting**, including one whose registered `scope` is not `profile`. The declaration is stored; nothing is refused at `set` time. The mismatch surfaces later, when the declaration is **read back** — every comparison for that station reports the setting as `out-of-scope`, with a reason naming the scope, instead of `matches`, `drifted` or `absent`. So a `--station` declaration of a `user`- or `project`-scoped setting is not an error you are told about when you make it; it is a state you see the next time you ask where things stand. ## The three policies [Section titled “The three policies”](#the-three-policies) Every registered setting carries a `policy`, which says what this system is permitted to do toward making a station’s value match the declaration: | Policy | Meaning | | --------------- | ------------------------------------------------------------------------------------------------------------- | | `reconcilable` | The value can be fully reconciled to the declaration — whatever is there can be replaced outright. | | `additive-only` | The declaration is a floor, never the whole value. Items can be added; nothing already there is ever removed. | | `report-only` | The value is only ever compared and reported. Nothing here writes it. | `additive-only` exists because a harness persists an operator’s own decisions into the same file a declaration targets. `hermes.approvals.command_allowlist` is the first example: an operator can extend that allowlist from Hermes’s own UI at any time, and that edit lands in the same document a fleet-wide declaration points at. Reconciling the list down to exactly what was declared — the `reconcilable` behaviour — would silently delete a grant the operator made minutes earlier through a surface this system never watched. A declared baseline must never be able to erase an operator’s own decision; `additive-only` is the policy that guarantees that, by only ever being allowed to add. `apply` enforces exactly this at write time, not just at comparison time: a `reconcilable` write replaces the key outright; an `additive-only` write only appends items that are not already present, so re-applying the same declaration twice is always safe; and a `report-only` setting is planned as a no-op and is never sent to a node to write — by design, not because writing it hasn’t shipped yet. ## Seeing what the fleet can declare [Section titled “Seeing what the fleet can declare”](#seeing-what-the-fleet-can-declare) ```sh fleet config settings ``` lists every setting any reachable node currently manages — the live registry, read from the node, never a copy held in the hub. A node that cannot currently be asked contributes nothing to this list, and is named separately so its absence isn’t silent. This registry currently covers seven settings — plus whatever else is live on the node you ask: | Setting | Scope | Policy | | ---------------------------------------- | --------- | --------------- | | `hermes.approvals.timeout` | `profile` | `reconcilable` | | `hermes.approvals.mode` | `profile` | `reconcilable` | | `hermes.approvals.command_allowlist` | `profile` | `additive-only` | | `hermes.plugins.enabled` | `profile` | `additive-only` | | `hermes.plugins.stream_reasoning_deltas` | `profile` | `reconcilable` | | `hermes.skills.external_dirs` | `profile` | `additive-only` | | `openclaw.hooks.allowConversationAccess` | `user` | `reconcilable` | The last four are the same four settings the `apn hermes-live`, `apn hermes-skills` and `apn openclaw-errors` verbs have always managed. Folding them into this registry did not replace those verbs, and does not retire them: **both paths call the exact same writer**, so a plan `fleet config` derives for one of these settings and the edit the matching `apn` command would make by hand are byte-identical, not merely equivalent. Run `apn hermes-live`, `apn hermes-skills`, or `apn openclaw-errors` directly whenever that is more convenient — nothing about declaring these through `fleet config` changes what those commands do. ### Two limits on the plugin settings [Section titled “Two limits on the plugin settings”](#two-limits-on-the-plugin-settings) `hermes.plugins.enabled` and `hermes.plugins.stream_reasoning_deltas` share one writer, and it is indivisible: Hermes only reports reasoning deltas from a plugin that also has `plugins.enabled` set, so declaring **either** setting enables the agentpod-live plugin as part of the same edit. Declaring `hermes.plugins.stream_reasoning_deltas` alone still touches `plugins.enabled` — both key paths are named in the plan’s entries, and the plan’s diff shows every line that moves, so this is visible to whoever reviews the plan before `apply` ever writes anything. It is not a silent side effect; it is one edit covering two keys, and an operator declaring one setting should expect the other key to move too. Separately, `hermes.plugins.enabled` can only ever **add** that one plugin, `agentpod-live`. Declaring any other plugin name is refused by name (`SHAPE_UNEXPECTED`) rather than appended — the setting id reads as if it manages Hermes’ whole plugin list, but the writer behind it only knows how to turn on the one plugin this system ships. ## Seeing where things stand [Section titled “Seeing where things stand”](#seeing-where-things-stand) ```sh fleet config show # every declaration in the fleet, as stored fleet config show --node nod_123 # one node's declarations, as stored fleet config show --station st_abc # one station: declared vs observed, compared fleet config drift # every station whose value differs from what's declared ``` **Only `--station` compares anything.** With no flag, and with `--node`, `show` returns the **declaration rows themselves** — what was declared, at which level, by whom — and no station is contacted: there is no `observed` value and no state in that answer. That is not a gap in the output, it is what the question means at those levels: a fleet-wide or node-wide declaration is one row that may apply to many stations, and comparing it requires naming which station you mean. `--station` names one, so that form asks its node for the current values and reports a state per setting. `fleet config drift` is the other compared form, across every station at once. Each **comparison** — `show --station` and `drift` — reports, per setting, a **state**: | State | Meaning | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `matches` | The observed value agrees with what’s declared. | | `drifted` | Declared and observed disagree. | | `absent` | Declared, but the key isn’t in the document at all. | | `opted-out` | An operator explicitly opted this station out of the declaration — see [Opting a station out](#opting-a-station-out). | | `awaiting-restart` | A value was written but the harness hasn’t picked it up yet — see [awaiting-restart](#awaiting-restart-written-but-not-yet-in-effect). | | `unreadable` | The document couldn’t be read, or the key is there but holds a shape this release can’t read — a nested map (see below). Never reported as `matches` — an unreadable value is not evidence of agreement. | | `out-of-scope` | A per-station declaration was made for a setting whose registered scope isn’t the station’s document (see above). | Each comparison also names the **level** the winning declaration resolved from — `station`, `node`, or `fleet` — the same precedence [Scopes and levels](#scopes-and-levels) describes. A station with its own declaration, one inherited from its node, and one inherited from the fleet all report differently here even when the declared value happens to be the same, so you can tell which declaration is the one actually governing this station without re-deriving precedence by hand. `fleet config drift` only reports states other than `matches`, plus the list of stations it could not reach at all — a station it couldn’t ask is never silently left out of the total. That list is narrower than “every station that is down”, and deliberately so. It holds the stations that **had something declared for them** and whose node could not be asked. A station with **no** declaration resolving to it is never contacted in the first place, so it appears neither in the observations nor in the unreachable list, however offline it is; and a station that does not advertise `config.manage` at all is not a candidate for this command. Read the list as “declarations I could not check”, not as a fleet health report — `fleet nodes` is where you see what is up. ### Nested-map settings read as `unreadable` [Section titled “Nested-map settings read as unreadable”](#nested-map-settings-read-as-unreadable) This release reads **scalar** and **list** values. A registered setting whose key holds a nested map is reported `unreadable`, with a reason saying so — not `absent`, and not `drifted`. `hermes.approvals.command_allowlist` is a list in every real document, and reads normally: a block list, an inline list, and an empty list all come back readable, with the list itself as the observed value. It stays in the registry even on the shape it can’t read — the alternative was to report a key that is plainly in the document as “not in the document at all”, which is a false sentence, or to compare an inline list’s raw text and report drift forever. An honest `unreadable` with a reason is the state that tells you what is actually true. ### Opting a station out [Section titled “Opting a station out”](#opting-a-station-out) `opted-out` names an operator’s explicit choice, and it outranks the ordinary comparison — an opted-out setting is reported `opted-out` even when the observed value differs from the declaration, never `drifted`, because `drifted` would invite the exact `apply` the opt-out exists to prevent. The opt-out lives in a **hub-side register** (`harness_config_opt_out`) — **not** as a key inside the harness’s own config file. A harness like Hermes rewrites and migrates its own config document on its own schedule; a key this system invented there would be a key whose survival this system does not control, so the opt-out is kept somewhere this system does control instead. #### A harness’s own opt-out is a second, distinct source of `opted-out` [Section titled “A harness’s own opt-out is a second, distinct source of opted-out”](#a-harnesss-own-opt-out-is-a-second-distinct-source-of-opted-out) A harness can also carry **its own** record that an operator turned a setting off, made through the harness’s own UI rather than through agentpod at all: * **Hermes:** the plugin named in `plugins.disabled`. * **OpenClaw:** `plugins.entries.agentpod-errors.enabled` explicitly `false`. Either one reports `opted-out`, with a reason naming the harness as the source — distinct from the hub-register reason, so you can tell “I exempted this through `fleet config`” from “I disabled this inside the harness itself” by reading the reason text alone. And either one now refuses a **write**, not only a comparison: declaring `hermes.plugins.enabled` while `plugins.disabled` names `agentpod-live` is refused `OPTED_OUT`, the same as an operator opt-out recorded in the hub’s own register. **One asymmetry, stated plainly rather than papered over.** For Hermes, the check lives inside the one writer both `apn hermes-live` and the registry call, so both paths are protected — running `apn hermes-live enable` by hand against a profile where the plugin is listed in `plugins.disabled` refuses exactly as a `fleet config apply` would. For OpenClaw, the check lives only in the registry’s own plan derivation, not inside the writer `apn openclaw-errors` and the registry share. That means `apn openclaw-errors enable --apply`, run by hand, does **not** check OpenClaw’s own opt-out and can still re-enable a plugin an operator disabled through OpenClaw’s own UI. This is pre-existing behaviour, not something this feature introduced, and it is a known follow-up rather than something closed here. `compare()`, adopt-time reconcile, and `plan`/`apply` all honor a row in this register the moment one exists — there is no path left that writes an opted-out setting silently. `plan` excludes an opted-out setting from what it asks the node to plan and names it in a `refused` list alongside whatever it *did* plan (a mixed request — some settings opted out, the rest not — refuses only the opted-out ones, never the whole plan: `fleet config plan --station ID` plans every setting currently declared for a station with no way to narrow it, so a whole-plan refusal would let one opt-out block writing anything else to that station). `apply` re-checks the register too, independently of `plan` — an operator can opt a station out *after* a plan was reviewed and before someone applies it, and that window is exactly where the check matters most. Because one `apply` call writes one journaled plan as a single atomic operation, a setting opted out after its plan was made refuses the **whole** apply, naming the setting; nothing in that plan is written, and a fresh plan (which will exclude the now-opted-out setting) is the remedy. **An opt-out stops this system *writing* a setting from here on. It does not revert what is already in the file.** A station that was already drifted, or that already carries a value written before the exemption existed, stays exactly as it is — opting out changes what `compare()` reports and what `plan`/`apply` will do next, never what is already on disk. #### Two levels, no third [Section titled “Two levels, no third”](#two-levels-no-third) An exemption targets exactly one of two levels — a **station**, or a **node** (every station on it, unless a station overrides it; see below). There is deliberately no fleet-wide exemption: `fleet config unset` already means “nothing is declared for this setting, anywhere” — a fleet-level opt-out row would only ever be a second way to say something `unset` already says, and a second way to say one thing is how a refusal becomes ambiguous. A row naming neither a station nor a node, or naming both, is refused before it is stored: the CLI requires exactly one of `--station`/`--node`, the route validates the same rule, and the table’s own CHECK constraint keeps it true for any other caller too. A **station-level row always beats a node-level one**, for the same setting: when the station has its own row — whichever way it reads — that row decides outright and the node row is never consulted. Only when the station has **no row at all** does the node’s row apply. #### `opt-in` versus `--clear` [Section titled “opt-in versus --clear”](#opt-in-versus---clear) This is the subtle half of the feature, and the two are not interchangeable: ```sh fleet config opt-in hermes.approvals.mode --station st_abc ``` records a row for `st_abc` with `optedOut: false` — **“this station is explicitly NOT exempt.”** That row is not the absence of a row, and it stops the search right there: a station row decides outright, true or false, without the node row ever being read. So `opt-in` at the station overrides `opt-out` at the node — if `nod_123` is opted out of a setting and `st_abc` is one of its stations, `fleet config opt-in ... --station st_abc` pins `st_abc` back under ordinary reconciliation while every *other* station on `nod_123` stays exempt. ```sh fleet config opt-out hermes.approvals.mode --station st_abc --clear ``` does something different: it **forgets the row entirely**, rather than recording a `false` one. With no station row left at all, the search falls through to the node — so if `nod_123` is still opted out of that setting, `st_abc` becomes exempt again too, the moment its own row is gone. `--clear` works at the node level the same way: a cleared node row leaves any station with no row of its own under no exemption at all, since there is nothing left to fall through to. Use `opt-in` to pin one station in despite its node. Use `--clear` to stop having an opinion at that level and let whatever the other level says take over. #### `--station` is a stationKey, not a row id [Section titled “--station is a stationKey, not a row id”](#--station-is-a-stationkey-not-a-row-id) Unlike `set`, `show`, `plan`, `inspect` and `apply` — whose `--station` is the station’s database row id — `opt-out` and `opt-in` take `--station` as the station’s **stationKey**. That is deliberate: a stationKey survives unadopt and re-adopt (re-adopting a station gives it a new row). An exemption recorded against a stationKey is still there, and still honored, after the station it names is unadopted and re-adopted; one recorded against a row id would silently stop applying the moment that happened. #### A worked example [Section titled “A worked example”](#a-worked-example) ```sh # exempt every station on a node while a new default is piloted $ fleet config opt-out hermes.approvals.mode --node nod_123 --reason "piloting a new default" # pin one station on that node back in, despite the node-wide exemption $ fleet config opt-in hermes.approvals.mode --station st_abc # see the register, narrowed to that station $ fleet config opt-out --station st_abc [ { "id": "cfgoo_9f1c3a7b", "stationKey": "st_abc", "nodeId": null, "settingId": "hermes.approvals.mode", "optedOut": false, "reason": null, "optedOutBy": "usr_4b2e", "createdAt": "2026-10-05T09:00:00Z", "updatedAt": "2026-10-05T09:00:00Z" } ] # change your mind: let st_abc fall back to following its node again $ fleet config opt-out hermes.approvals.mode --station st_abc --clear {"cleared": true} ``` (Illustrative, not literal output; `tenantId` is present on every row but omitted above for brevity.) After the final `--clear`, `st_abc` has no row of its own, so it inherits `nod_123`’s exemption again — it is now opted out for the same reason `nod_123` is, not because of anything recorded against `st_abc` itself. With no `SETTING_ID`, `fleet config opt-out` lists the whole register, unresolved — every row, at either level, with its `reason` and who set it; `--station KEY` or `--node ID` (never both) narrows that listing. That is the register **as stored**, the same sense in which `fleet config show` with no `--station` returns declaration rows rather than a comparison. To see what a specific station is exempt from right now, after station-beats-node resolution, read its `opted-out` states from `fleet config show --station ID` or `fleet config drift` instead. The register is visible on a station’s own page in the console (see [Seeing and applying it from the console](#seeing-and-applying-it-from-the-console)) — but `fleet config opt-out` / `opt-in` remains the only surface that **writes** it. ## Writing a declaration: plan, inspect, apply [Section titled “Writing a declaration: plan, inspect, apply”](#writing-a-declaration-plan-inspect-apply) ```sh fleet config plan --station ID the edit that would be made, and its digest fleet config inspect --station ID --operation ID a plan already made, as it was reviewed fleet config apply --station ID --operation ID --plan-digest SHA256 ``` `plan` asks the hub to derive the exact edit against the station’s current document and returns a `ConfigPlan`: the before/after diff, which action each setting takes (`create`/`modify`/`append`/`noop`), whether a restart is needed, and a `planDigest` — a SHA-256 over everything in the plan except the timestamp and operation id, so two plans of an unchanged document always digest the same and a plan drawn against a changed document never does. `inspect` reads back exactly the plan or receipt a station’s own node journaled for that operation — it never re-derives anything. `apply` is the only verb that writes, and it refuses to run without `--plan-digest`: that digest must be the one `plan` printed for this operation, so a human reviewed the exact edit being written rather than whatever the current plan happens to be by the time `apply` runs. `create` covers a key the document does not have yet, including the case where its whole top-level section is absent — the ordinary state of a freshly adopted station whose operator has customised nothing. That section is **appended at the end of the document**, holding only the declared key, so no line already there is moved, reindented or reflowed. A section that *is* there but holds something this editor will not extend — a scalar, a bare key with nothing under it, a list, or an inline `{...}` mapping — is refused `SHAPE_UNEXPECTED` and named, never rewritten. Only one level of section is ever created. `fleet config plan --station ID` plans **every** setting currently declared for that station, at whatever level resolves to it — there is no flag to narrow the plan to one setting. A setting the station is opted out of is left out of that plan and named in `refused` instead (see [Opting a station out](#opting-a-station-out)) — the rest of the declared settings still get planned. A worked example, declaring and then writing `hermes.approvals.mode` for one station: ```sh $ fleet config set hermes.approvals.mode --value strict --station st_abc $ fleet config plan --station st_abc { "schemaVersion": 1, "operationId": "cfgop_3f9a2c7b1e6d48a09c21", "stationKey": "st_abc", "entries": [ { "settingId": "hermes.approvals.mode", "file": "/home/agentpod/.hermes/profiles/st_abc/config.yaml", "keyPath": "approvals.mode", "policy": "reconcilable", "current": "standard", "intended": "strict", "action": "modify", "restartToTakeEffect": true } ], "beforeSha256": "1b80374ee731c363440504e674caf2d26d72ee818a2482571e6beff6de397d5a", "diff": "- mode: standard\n+ mode: strict\n", "diffTruncated": false, "noOp": false, "restartRequired": true, "createdAt": "2026-10-05T09:12:03Z", "planDigest": "c68e0c65d2a58aa481e8fdf83422dbb4712a8b97e41fb907bdb3109adc503077" } $ fleet config inspect --station st_abc --operation cfgop_3f9a2c7b1e6d48a09c21 { "plan": { ... as above ... }, "phase": "planned", "updatedAt": "2026-10-05T09:12:03Z", "written": [] } $ fleet config apply --station st_abc --operation cfgop_3f9a2c7b1e6d48a09c21 \ --plan-digest c68e0c65d2a58aa481e8fdf83422dbb4712a8b97e41fb907bdb3109adc503077 { "plan": { ... }, "phase": "applied", "written": [{"settingId": "hermes.approvals.mode", "action": "modify", "wrote": "strict"}], "afterSha256": "318e20b3a675cd8701af3cab0b019abbab63ca2e1183b77fa39a7e538ffb50ab", "updatedAt": "2026-10-05T09:12:41Z" } ``` (Operation ids and digests above are illustrative, not literal output.) On the node, the plan and its receipt are journaled in `.agentpod/config-operations.json` — a sibling directory next to the harness’s own config, not inside it, because the harness rewrites and migrates its own config document and a receipt that must survive that rewrite cannot live anywhere under it. Re-deriving the plan at `apply` time, rather than trusting the one `plan` printed earlier, is what catches a document that changed in between — see [refusals](#every-refusal-and-what-distinguishes-it) below for `PLAN_STALE` and `PLAN_DIGEST_MISMATCH`. ### A lost race preserves both edits [Section titled “A lost race preserves both edits”](#a-lost-race-preserves-both-edits) `PLAN_STALE` catches a document that changed **before** the write. A narrower window remains **after** it: the write itself lands, and then, in the moment between that write and the read-back that confirms it, something else changes the same file again — an operator’s own “Allow always” through Hermes’ UI, say, landing a heartbeat after `apply` wrote its own edit. When that happens, this system does not pick a winner. The document on disk is left **exactly as the read-back found it** — whatever landed there in that window is not overwritten, and the edit `apply` itself intended is not thrown away either. It is written instead to a sidecar file beside the original, named by appending `.agentpod-rejected` to the document’s own filename — `config.yaml` gets `config.yaml.agentpod-rejected`, `openclaw.json` gets `openclaw.json.agentpod-rejected`. The operation’s receipt (`fleet config inspect`) records `"phase": "conflict"` with an `error` naming both paths and stating plainly that neither edit was lost. That sidecar is bookkeeping for a human, not for this system: a later lost race against the same document overwrites whatever an earlier one left there, and nothing here ever reads a sidecar back — not on a retry, not on the next `plan`. Reconciling the two files, if they need reconciling at all, is a manual step. A fresh `fleet config plan` against the document as it now stands is the way forward; it has no reason to know the sidecar exists. ### `awaiting-restart`: written, but not yet in effect [Section titled “awaiting-restart: written, but not yet in effect”](#awaiting-restart-written-but-not-yet-in-effect) A setting whose policy requires a restart to take effect — every registered Hermes and OpenClaw setting today — is reported `awaiting-restart` right after `apply` writes it, until the harness’s gateway actually restarts. **AgentPod will not restart the harness for you.** Nothing in this system issues a restart; an operator restarts the gateway, from wherever they already do that, and the state changes on its own once observed. The decision is made by comparing a gateway process id, not by a timer. Hermes and OpenClaw both multiplex one gateway process across every profile or subagent on a host, so that process’s pid is exactly the one that re-reads config. `apply` reads the station’s health right after a successful write and records that pid. The next comparison reads the station’s **current** gateway pid and reports `awaiting-restart` whenever that pid is unchanged from the one recorded at write time — or when either pid cannot be confirmed at all: the current one (health degraded, harness stopped) or the one that was supposed to be recorded at write time, which is left unset whenever that post-write health read timed out or came back in an unexpected shape. An unconfirmable pid on either side deliberately stays `awaiting-restart` rather than resolving to `matches`: claiming a restart is still needed when it already happened costs one needless restart; claiming a restart already happened when it hasn’t produces false agreement, which is the worse mistake. Whether `hermes.approvals.*` truly needs a restart to take effect is itself unverified — Hermes’s own documentation lists hot-reloadable settings and settings needing a restart, and `approvals.*` is in neither list. The registry assumes `true` deliberately: an unnecessary restart is cheaper than a running gateway that silently disagrees with its own config file. See `docs/superpowers/notes/2026-10-05-hermes-approvals-restart.md` for what was checked and why it stayed inconclusive. `hermes.plugins.*` and `hermes.skills.external_dirs` are not the same kind of guess: Hermes reads a profile’s plugin and skill configuration when its gateway starts, which is what `apn hermes-skills` itself already tells an operator after writing, so `RestartToTakeEffect` for those three is a documented fact rather than an assumed one. ## Seeing and applying it from the console [Section titled “Seeing and applying it from the console”](#seeing-and-applying-it-from-the-console) Everything above has a CLI-free path too, on a station’s own page — its **Configuration** tab, which also holds that station’s plugins and its skills, because on a Hermes station all three write the same configuration document. A **declared configuration** panel sits at the top of that tab whenever the station advertises the `config.manage` capability; a station that does not advertise it shows nothing in its place, not an empty panel, and a station with nothing configurable at all has no Configuration tab. A link to the older `?tab=skills` still works — it lands on the Configuration tab. The panel lists one row per registered setting with something declared for that station: the declared value and the level it came from (station, node, or fleet), the observed value, and the state — all seven: `matches`, `drifted`, `absent`, `unreadable`, `out-of-scope`, `opted-out`, `awaiting-restart`, each with its reason shown alongside it. It is read-only for exactly as long as there is nothing to review: a “Review changes” button asks the hub for a plan over this station’s declared settings, the same `plan` a `fleet config plan` call would get back — the file, the key path, current → intended, the action, and the diff, plus the plan’s own digest. Nothing is written at that point. “Apply reviewed plan” sends that operation id and **that exact digest**, never a digest re-fetched or re-derived at apply time — the console goes through the identical reviewed-plan-then-digest discipline the CLI’s `plan`/`apply` pair enforces, against the same routes, because the digest is the record that a human saw the edit before it was written. A plan that refuses some of the requested settings (an `OPTED_OUT` entry mixed in with plannable ones, say) names each refused setting, its code and its message, beside whatever the plan did manage to derive — a partial refusal never quietly disappears the rest of the plan. **The console never restarts anything**, here or anywhere else. A row reading `awaiting-restart` says so plainly, and if a restart is needed to apply what you are about to review, the panel says that too, before the apply button, not after. Use the station’s own restart control (reached from this same panel when the page wires one up) once you are ready. **Exemptions are visible here, not editable.** An `opted-out` row shows which level the exemption came from, who recorded it and why, when it rode in through agentpod’s own register — and it reads differently again when the harness’s own record is the source (Hermes’ `plugins.disabled`, OpenClaw’s `plugins.entries.agentpod-errors.enabled: false`), never conflating the two. Recording or clearing an exemption is still only `fleet config opt-out` / `fleet config opt-in` — the console does not write this register, by design: reading it is what made the `opted-out` state legible, and giving it its own write surface here was ruled out rather than added silently. None of this replaces the raw config editor reached from the file browser’s own “Edit” action on an open file. That editor is unchanged, and it is still the only way to touch anything in a harness’s config file that is not a registered setting — this panel sits beside it, for exactly the settings the registry knows about, and absorbs none of its job. ## Adopting a station: one reconcile, not a sweep [Section titled “Adopting a station: one reconcile, not a sweep”](#adopting-a-station-one-reconcile-not-a-sweep) When a station is adopted, every setting already declared for it — at station, node, or fleet level — is reconciled exactly once, right after the station’s row is written. A setting is skipped, not written, when its policy is `report-only`, when it’s opted out, or when it already matches; otherwise it’s planned and, if the plan isn’t a no-op, applied with that plan’s own digest — the same `plan`-then-`apply` path described above, run on the station’s behalf. **A write failure here never fails the adoption.** Every setting on every station is isolated: a plan refusal, an apply refusal, an unreachable node, or an unreadable document is recorded against that one station (visible as its config reason) and never thrown. A station adopted with one setting unwritten is better than one not adopted at all. **And it never holds the adoption open for long.** Stations are reconciled concurrently rather than one after another, and the whole pass is capped: past the cap the adoption answers, and the stations still in progress keep going and keep recording their own config reason. That matters because a node can be *connected but wedged*, where every round trip pays its full timeout instead of failing fast — adopting many stations onto such a node used to leave the request running long after the rows were committed, so a client that gave up reported a failed adoption of stations that were adopted. The cap is one round trip’s worth of time, while a station’s pass makes several of them in sequence, so a merely *slow* node reaches the cap too — that is the ordinary case, not the exceptional one. A station still in progress when the cap fires is reported as **pending**, never as failed: nothing has gone wrong, the pass simply has not finished, and it records its own outcome against the station when it does. Reconcile also re-checks that a station is still adopted before it writes anything, so unadopting a station right after the adoption answers cannot leave a write landing on it afterwards. This reconcile runs exactly once, at the moment of adoption — never on a timer and never swept across already-adopted stations. A station that drifts afterward stays drifted until someone asks for it (`fleet config drift`) or declares and applies again; running this on a schedule would race the harness’s own rewrites of the same file, or silently undo a change an operator just made through it. ## Every refusal, and what distinguishes it [Section titled “Every refusal, and what distinguishes it”](#every-refusal-and-what-distinguishes-it) A refusal that cannot be told from a pass is the failure this whole area keeps hitting, so every one of these is distinct and carries its own message. There are ten: | Refusal | What distinguishes it | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `UNKNOWN_SETTING` | The setting id isn’t in any reachable registry — or no node could be asked to confirm either way; the message, not the code, tells you which. | | `NOTHING_DECLARED` | The setting id IS in the station’s live registry, but nothing is declared for that station at any level and no value was given to plan against. Remedy: `fleet config set`. Distinct from `UNKNOWN_SETTING` because the id is real and the remedy is a different command. | | `NODE_UNREACHABLE` | The node could not be asked at all (offline, timed out, disconnected, or answered in an unexpected shape). Remedy: retry once it is back. Never used for a refusal the node itself named — a node that answers with a refusal is a node that was reached. | | `OUT_OF_SCOPE` | The declaration’s own target has no document to land in at all (for example, declaring against Hermes’s composite root, which has no profile-scoped document of its own) — a plan-time refusal, distinct from the `out-of-scope` *comparison state* reported for a per-station declaration of a non-`profile`-scoped setting. | | `SHAPE_UNEXPECTED` | The declared value doesn’t fit the setting’s registered shape (an `additive-only` value that isn’t a list of strings, say), or the derived edit would change something outside the keys this plan claims to touch. | | `PLAN_STALE` | The document changed after this plan was reviewed. Remedy: re-plan and re-review. | | `PLAN_DIGEST_MISMATCH` | The digest `apply` was given does not match the one this station’s journal recorded for that operation. Remedy: re-read the plan that was actually reviewed (`inspect`), not re-plan — the document itself hasn’t necessarily changed. | | `OPTED_OUT` | An explicit operator opt-out (see [Opting a station out](#opting-a-station-out)). `plan` produces it per setting — refusing just the opted-out entries of a mixed request, or the whole request when every setting named was opted out — before `config.plan` is ever dispatched for one of them. `apply` produces it for the whole operation when a setting the plan covers was opted out after the plan was made, before `config.apply` is ever dispatched; nothing in that plan is written. | | `UNREADABLE` | The document could not be read or parsed at all; nothing about its contents is inferred. | | `CREDENTIAL_PATH` | The setting’s target resolves to a credential file (`auth.json`, `.env`, or a path under a `credentials` directory) and is refused before it is ever opened, let alone read or edited. | `PLAN_DIGEST_MISMATCH` was added during implementation, after the seven above were specified, because collapsing it into `PLAN_STALE` would make two conditions with different remedies indistinguishable from their refusal code alone. `NOTHING_DECLARED` and `NODE_UNREACHABLE` are the two the hub produces on its own: no node can produce them, because only the hub knows what is declared and only the hub knows it could not get an answer. **A refused plan is a refusal, not a plan.** `fleet config plan` exits non-zero on one, and the HTTP route answers 400 when re-sending the same request could never work (`UNKNOWN_SETTING`, `NOTHING_DECLARED`, `OUT_OF_SCOPE`, `SHAPE_UNEXPECTED`, `OPTED_OUT`, `CREDENTIAL_PATH` — the remedy is a different request, usually `fleet config set`) and 409 when the request was well-formed and lost to the state of the document or the station (`UNREADABLE`, `PLAN_STALE`, `PLAN_DIGEST_MISMATCH` — the remedy is to re-read and re-send) — never 200 with a refusal in the body, which no script could tell from a plan. `SHAPE_UNEXPECTED` is a 400 because neither of its causes resolves itself: a declared value that does not fit the setting has to be re-declared, and a derived edit that would disturb the document never succeeds on a retry of the same request. `OPTED_OUT` is a 400 whether the hub or the node is the one that noticed. The code and the sentence are the node’s own wherever the node is the one that refused, and at adopt time they are what gets recorded against the station, so a recorded reason names the refusal rather than blaming the connection. ## A setting that can’t be found [Section titled “A setting that can’t be found”](#a-setting-that-cant-be-found) Declaring a setting the hub doesn’t recognize is refused with `UNKNOWN_SETTING`. The refusal’s reason is read verbatim from the hub — it may mean the setting genuinely doesn’t exist in any reachable harness’s registry, or it may mean no node was reachable to confirm either way. Those are two different problems with the same refusal code, and only the reason text tells you which one you have. # When a turn fails > One error shape whichever harness failed, the card a client can draw, and the plugins that recover what ACP drops. A model provider runs out of quota. A credential expires. A request is malformed. What should reach the room is the provider’s own sentence. What used to reach it depended entirely on which harness the agent ran. Six harnesses fail six different ways over ACP: some reject the prompt with real text in the message, some emit the error as an ordinary message chunk and resolve the turn normally — so the error arrives **posing as the agent’s answer** — and some drop the error entirely and resolve the turn, so the room says “the agent completed without a reply” while the real reason sits in a log on a machine you are not looking at. AgentPod standardises this itself rather than waiting for six upstreams. ## One error shape [Section titled “One error shape”](#one-error-shape) Every failed turn is normalised into one shape, whichever harness produced it: a **kind**, the provider’s or harness’s **own words untrimmed**, which harness it was, and — when they are knowable — the provider, the model, and each attempt that was made. The kinds are a closed vocabulary: `quota`, `rate_limit`, `auth`, `bad_request`, `context_exhausted`, `timeout`, `provider_unavailable`, `refusal`, `max_tokens`, `cancelled`, `node_offline`, `harness_exited`, and `unknown`. `unknown` is there deliberately. A failure that does not fit is reported as not fitting, rather than squeezed into the nearest kind and misread later. ## The card in the room [Section titled “The card in the room”](#the-card-in-the-room) A failure is posted as a notice carrying a **card a client can draw** — the kind, the sentence, the harness, and the attempts if there were several. A client that does not understand the card shows the prose; a client that does draws the card. Four rules keep it honest: * **One error per failed turn**, led by the model that was actually asked for. A turn that tried three models does not produce three cards; it produces one card listing three attempts. * **The error is held until the turn ends, and dropped if the agent answers.** An error mid-turn that the harness then recovers from is not a failure, and reporting it would teach you to ignore the cards. * **A recovered fallback is no error.** If the first provider failed and the second answered, you got an answer. * **A run that ended well after its failure was reported is no error either.** Errors reach encrypted rooms like anything else. ## Recovering what ACP drops [Section titled “Recovering what ACP drops”](#recovering-what-acp-drops) Two harnesses dropped their error before it ever reached the hub, so no amount of care at the hub could recover it. For those, AgentPod ships a small plugin that runs inside the harness and reports what the harness knows and the protocol discards: | harness | what it ships | installed with | | -------- | ------------------------------- | ------------------------------------ | | OpenClaw | the `agentpod-errors` plugin | `apn openclaw-errors enable --apply` | | Pi | the `agentpod-errors` extension | `apn pi-errors enable --apply` | They report the failure; the hub normalises it into the shape above. Both are installed **on the node, with `apn`** — they are not reachable through the console or `fleet plugins`, which manage one plugin on one harness (see below). Without `--apply` each command prints the exact change and writes nothing. Neither restarts anything you did not ask it to: the OpenClaw gateway is yours to restart, and each new Pi session picks its extension up by itself. ### The third plugin, and the one the console manages [Section titled “The third plugin, and the one the console manages”](#the-third-plugin-and-the-one-the-console-manages) `agentpod-live` reports a turn’s *progress* rather than its failure — that the agent is thinking, that an answer has begun streaming, and when the turn ended. A harness-mode Hermes profile streams into AgentPod clients only with it. It is the **only** plugin the console and `fleet plugins` can install, enable or disable, and only on Hermes. That path is off by default and a node opts in with `apn plugin-management enable`; after that each change is planned on the node, reviewed, and applied only if the profile still matches the review. Nothing there restarts a gateway. `apn hermes-live` does the same thing locally, per profile. ## Why not just fix the harnesses [Section titled “Why not just fix the harnesses”](#why-not-just-fix-the-harnesses) The upstream issues are reported. Nothing here waits on them being accepted, because a fleet running six harnesses cannot have its error reporting gated on six other projects’ release schedules — and a harness you pinned for a different reason would keep the old behaviour anyway. ## Next [Section titled “Next”](#next) * [Talking to an agent in a room](/use/rooms/) — where the card arrives * [apn and fleet](/use/cli/) — installing and reviewing plugins # Dispatch and grants > Who may send an agent work — read from the signed token, never from the request, and never read as "everyone". Sending an agent a message spends its time. So does approving a tool call for it, and so does putting a card in front of it. All three are **dispatch**, and all three ask the same question: may this principal dispatch that one? ## The answer is in the token [Section titled “The answer is in the token”](#the-answer-is-in-the-token) ```plaintext GET /api/fleet/dispatchable ``` Returns the agents the token’s holder may dispatch. The answer is read from a `mayDispatch` claim the hub **signed into the token** — not from a query parameter, not from a header, and not derived from anything else the caller sent. There is nothing in the request that can change it. **A missing or non-array claim is read as *permitted nothing*.** Never as *permitted everything*. An absent claim means the issuer does not speak that control, and reading that as “all” is the one mistake that cannot be walked back. An agent’s token is refused at this endpoint regardless of what it may dispatch. An agent enumerating its siblings is an agent doing reconnaissance. ## A grant is a document [Section titled “A grant is a document”](#a-grant-is-a-document) ```sh fleet grants list fleet grants show fleet grants set --file grant.json # or --file - for stdin fleet grants rm ``` `set` takes a **file**, not flags. A grant is a document rather than a field: the hub validates its whole shape, and flattening it into flags would mean the CLI modelling a schema the hub owns and then disagreeing with it the first time that schema moves. `-` reads stdin, so a grant can be piped from whatever produced it. ## Where a grant is checked [Section titled “Where a grant is checked”](#where-a-grant-is-checked) | act | checked | | ----------------------------------------------------- | ------------------------------------------------------------- | | messaging an agent in its room | ✓ | | approving a permission question from a room | ✓ — approving is dispatching by another name | | claiming a card for an agent through the board bridge | ✓, on the board’s side, against what the queuer was permitted | | reading a station’s logs or files | no — that is operating a machine, not dispatching an agent | The distinction is worth holding onto. Operating the ground an agent stands on is a different authority from spending the agent’s time, and AgentPod keeps them apart. ## Principals, and switching one off [Section titled “Principals, and switching one off”](#principals-and-switching-one-off) ```sh fleet principals list fleet principals suspend fleet principals restore ``` Suspending a principal stops it acting without deleting what it did. The record of its past work stays answerable, which is the point of having principals at all. For people rather than principals: ```sh fleet users list fleet users show fleet users role … fleet users ban fleet users unban ``` ## A node’s credential is not an authority [Section titled “A node’s credential is not an authority”](#a-nodes-credential-is-not-an-authority) A node’s secret says **“I am this host”**. It is not permission to operate the fleet, and AgentPod deliberately never lets it become one — `fleet` commands never fall back to a node’s credential. This is why there are two binaries. See [apn and fleet](/use/cli/). ## Next [Section titled “Next”](#next) * [Authentication](/build/auth/) — where a token comes from, and where it may be spent * [Talking to an agent in a room](/use/rooms/) — the most common way a grant is spent # Nodes > Running the node agent day to day — service control, logs, status, updates, and removing a node from the fleet. A node runs one background service. These are the commands that manage it. Every command on this page is a **node verb**: it acts on *this machine*. Each also has an explicit spelling — `apn status` and `apn node status` are the same command. The bare forms are kept because existing runbooks name them. ## Is it working? [Section titled “Is it working?”](#is-it-working) ```sh apn status ``` Two blocks. **Local**: whether the service is installed, enabled and running, with its PID, the binary version and the config path. **Hub**: whether the hub is reachable and the stored credential is still valid. The exit code is `0` only when the service is running *and* the credential is valid, so this drops straight into a health check. Add `--json` for scripts. ## Starting and stopping [Section titled “Starting and stopping”](#starting-and-stopping) ```sh apn start # enable and start apn stop # stop and disable apn restart # restart in place ``` `apn stop` is **sticky**: it disables the service as well as stopping it, so it will not come back on its own after a reboot or a fresh login. `apn start` is the exact inverse — it re-enables and starts. This is deliberate; a “stop” that silently undoes itself overnight is worse than no stop at all. Underneath, macOS uses `launchctl` and Linux uses `systemctl`. ## Logs [Section titled “Logs”](#logs) ```sh apn logs # recent apn logs -f # follow apn logs -n 200 # last 200 lines ``` On macOS this reads `~/Library/Logs/agentpod-node.log`. On Linux it hands off to `journalctl`. ## Installing and removing the service [Section titled “Installing and removing the service”](#installing-and-removing-the-service) ```sh apn service install apn service uninstall ``` `install` writes a launchd plist or a systemd unit from a template embedded in the binary, then enables and starts it. It is idempotent — running it again replaces the file and restarts. Where it installs depends on who you are. Non-root Linux gets a `--user` unit; root Linux gets a system unit; macOS always gets a LaunchAgent and **refuses to run as root**. `uninstall` stops, disables and removes the unit. It is idempotent too, and it leaves your config and enrollment alone — uninstalling the service does not un-enroll the machine. ## Running in the foreground [Section titled “Running in the foreground”](#running-in-the-foreground) ```sh apn run ``` This is what the service runs under the hood. Run it directly when you are debugging and want the output in front of you. ## Updating [Section titled “Updating”](#updating) ```sh apn update --check # report current and latest, change nothing apn update # update apn update --force # update even if already latest ``` On success the service restarts automatically. If that restart fails, the binary has already been swapped — the command tells you so and prints how to restart by hand, rather than leaving you to guess which half happened. ## Enrolling again [Section titled “Enrolling again”](#enrolling-again) ```sh apn enroll --hub https://hub.example.com --token ``` Falls back to `$AGENTPOD_HUB_URL` and `$AGENTPOD_ENROLL_TOKEN` when the flags are omitted. Running it on a machine that is already enrolled is a no-op, unless the stored credential has stopped being valid or you pass `--force`. ## Removing a node from the fleet [Section titled “Removing a node from the fleet”](#removing-a-node-from-the-fleet) A machine you retire does not leave the fleet on its own; uninstalling the service only stops it. To take it off the list, remove it from the hub — from the node’s page in the Console (**Remove node**, confirmed by typing its name), or with `fleet`: ```sh fleet nodes rm build-01 # a node that is offline fleet nodes rm build-01 --force # a node that is still connected: disconnect it too ``` Removal unregisters every station on the node, the same way **Remove station** does, and revokes the node’s credential. If the machine dials back, the hub refuses it, and `apn enroll` with its old token fails too: to join again it needs a fresh token from `fleet invite`. Nothing on the machine itself is touched — workspace files stay, and the node agent keeps running until you `apn service uninstall` it. Two kinds of node are refused, each with the command that does apply: * **A provisioned runtime’s node** goes with its runtime: `fleet runtimes rm ` destroys the container and removes the node. * **A node where a bridge agent runs** keeps its roster row, which holds a credential, until you remove it on purpose with `fleet bridge rm `. The hub records each removal in its admin audit log. ## Next [Section titled “Next”](#next) * [Stations](/use/stations/) — what lives on the node * [apn and fleet](/use/cli/) — the `fleet` client, which acts as *you* rather than as the machine # What you can do to a station > The panels — files, logs, terminal, config, health, lifecycle, cleanup, changesets, skills, voice and activity — and what each one requires. Open an adopted station in the console and you get a set of panels. Which ones appear depends on what the station declares it can do. ## Capability gating [Section titled “Capability gating”](#capability-gating) Several operations are gated on a capability the station declares: | Capability | Gates | | ----------- | ------------------------------------ | | `fs.write` | Writing to the filesystem | | `terminal` | The interactive shell | | `lifecycle` | Start, stop, restart | | `cleanup` | Planning and applying a disk reclaim | | `changeset` | Reading the working-tree diff | A request for a capability the station has not declared is refused **at the hub, without contacting the node**. Reading is not gated this way — the read paths are available wherever the station supports them. ## Files [Section titled “Files”](#files) Browse the tree, preview a file, jump straight to a path with quick-open, and edit where `fs.write` is declared. Under the hood these are the node’s own verbs — `fs.list`, `fs.read`, `fs.write`, `fs.mkdir`, `fs.move`, `fs.delete` — executed on the host by the local agent, never by the hub reaching in. ## Logs [Section titled “Logs”](#logs) Tail the harness’s logs live. The node knows where each harness keeps them, because the descriptor for that harness knows. ## Terminal [Section titled “Terminal”](#terminal) A real interactive shell on the host, brokered through the hub. Requires the `terminal` capability. ## Config [Section titled “Config”](#config) Read and edit the harness’s own configuration file, in place and in its own format — AgentPod does not convert it to something else and hand it back. ## Health [Section titled “Health”](#health) What the station reports about itself, asked live. The console shows this next to the node’s own online/offline state, which is a different question: a node can be perfectly online while a station on it is not. ## Lifecycle [Section titled “Lifecycle”](#lifecycle) `start`, `stop`, `restart`. Requires the `lifecycle` capability. Every lifecycle action is written to the audit trail **before** it is dispatched, not after. An action that is attempted and fails still leaves a record — an audit log that only lists successes is one that hides exactly the events you most need. The response is the station’s health *after* the action, so you can see the result rather than assume it. ## Cleanup [Section titled “Cleanup”](#cleanup) Two steps, deliberately: ```plaintext POST /api/stations/:id/cleanup/plan what would be removed, and how much it frees POST /api/stations/:id/cleanup/apply do it ``` The plan is shown to you first. Nothing on disk is touched until you apply it. Requires the `cleanup` capability. ## Changesets [Section titled “Changesets”](#changesets) What the agent has actually changed in its working tree — `status` for the summary, `diff` for the detail. Requires the `changeset` capability. A node advertises `changeset` only for a station whose workspace is a **git repository**. A newly provisioned runtime’s `/workspace` is an empty directory, so it has no Changes panel, and `fleet station changeset` is refused with 403 and “its workspace is not a git repository”. That is the design, not a fault: clone or `git init` into the workspace, and the hub picks the capability up the next time the station is adopted or its node reconnects. This is the panel for the question “what has this thing been doing”, answered from the files rather than from what the agent says about itself. ## Activity [Section titled “Activity”](#activity) The audit trail for the station: what was done, when, and by which principal. ## Chat [Section titled “Chat”](#chat) Where a station has a chat identity, talking to the agent is messaging it. See [Stations](/use/stations/#matrix-identity) for the two identity modes, and [Talking to an agent in a room](/use/rooms/) for what else arrives there. ## Skills [Section titled “Skills”](#skills) What this station actually has, and the review screen for installing something. Present, eligible and loaded are reported as separate observations rather than inferred from each other — see [Managed skills](/use/skills/). ## Plugins [Section titled “Plugins”](#plugins) Harness plugins on this station, and reviewed changes to them: the node plans, you read the plan, and the apply sends the digest you reviewed. Nothing here restarts the station. ## Voice notes and voice replies [Section titled “Voice notes and voice replies”](#voice-notes-and-voice-replies) Two sections, each offering *inherit*, *off*, or settings of its own, over the hub-wide defaults in **Admin → Transcription** and **Admin → Speech**. The voice-replies section also shows which voice this station was assigned, and lets the owner pick another. See [Voice notes](/use/voice/). ## Git identity [Section titled “Git identity”](#git-identity) What this station can push to a forge as, if anything. Push access is granted per station and never by adopting one — most stations never touch git, and a key for every station is an account nobody uses and a key nobody revokes. The keypair is generated **on the node**; the private half never leaves it and the hub holds no secret. Granted and revoked with `fleet stations grant-push` / `revoke-push`. ## Posture [Section titled “Posture”](#posture) A banner surfaces exposure problems found on the host — the same checks [`apn scan`](/use/scan/) runs locally. ## Next [Section titled “Next”](#next) * [Attaching an editor](/use/acp/) — your own editor against a remote station * [Managed skills](/use/skills/) — the review model behind the Skills panel * [Talking to an agent in a room](/use/rooms/) — the other half of a station * [Checking for exposure](/use/scan/) # Talking to an agent in a room > The Matrix bridge — every station gets an identity and a room, so an agent whose harness never spoke Matrix can be reached from a phone. A station is a place an agent works. It is also, if you turn the bridge on, **somebody you can message**. Every adopted station gets a Matrix identity and a room of its own. You send a message, the hub hands it to the harness as a turn, and the agent’s reply comes back as that agent — in a client you already have, on a phone you already carry. The harness itself never learns Matrix exists. This is the half of AgentPod that is not a console. The console is for operating the ground an agent stands on; the room is for working with the agent. ## What you get per station [Section titled “What you get per station”](#what-you-get-per-station) | | | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **an identity** | the station appears as a user, named for its node and station key. The member list shows the readable form. | | **a room** | one per station, created when the station is adopted | | **a space** | rooms hang under a space named for the machine they run on. A flat roster is fine at thirty agents and unusable at two hundred; clients that read the hierarchy group the list for free. | Rooms are **encrypted at creation**. Any client that can read an encrypted room can read these; there is nothing extra to turn on. ## What reaches the room [Section titled “What reaches the room”](#what-reaches-the-room) * **The agent’s replies**, posted as the agent, streaming as the turn progresses * **Voice notes**, in both directions — see [Voice notes](/use/voice/) * **Permission questions**, when the agent wants to run a tool and its mode says to ask * **A card when a turn fails**, saying what failed rather than going silent — see [When a turn fails](/use/errors/) * **Approval gates and questions from a board**, if the workspace is linked to a [SuperPipeline](https://docs.superpipeline.dev) board — see [Working a board](/use/boards/) * **Images** you send, passed to the agent rather than just their file name ## Answering a permission question [Section titled “Answering a permission question”](#answering-a-permission-question) An agent in `ask` mode parks when it wants to run a tool, and the question is posted where you are: ```plaintext Permission needed: Write src/main.ts 1. Allow once 2. Allow always 3. Reject Reply with the number, or the option's name. ``` Reply `1`, or `Allow once`. **Nothing else counts.** A reply that is not plainly one of the options approves nothing and shows the list again. That pedantry is deliberate. Against options named *Allow once* and *Allow always*, a bare “yes” does not say which — and approving a tool call you did not mean to approve is the one failure worth being awkward about. A question **stops standing** the moment the turn moves on: answered in the console, cancelled, or failed. A room can never approve something that was already decided. **It expires after 30 minutes.** A question nobody answers in that time is withdrawn: the agent is told it was not given permission (it is not left waiting for ever), and the room says so. A reply that arrives after a question closed — expired, or answered in the console a moment earlier — is refused in the room. It is **never** passed to the agent as a message, so a late `1` cannot turn into an instruction. Once you have talked to the agent again, a number is an ordinary message. **Updated apps show what you are allowing.** The same message also carries the request in full for apps that understand it: the tool, its command, each file change and the files it touches, with buttons that say what each option does (*allow once*, *always*, *reject*). Such an answer is bound to exactly the request you saw — if the agent asked something else since, it is refused. When the card could not show everything (a very long change, a terminal), nothing in the room can allow it — not a button, and not a typed number either; answer in the console. Whoever answers, by button, by number or in the console, is recorded with the answer, and the card closes wherever it was answered. **A board room carries more than gates and questions.** A card waiting on a person (blocked, failing, not authorised) and a SuperToolkit approval request arrive there too — see [Boards](/use/boards/#a-card-waiting-on-you). Answers to them are never passed to an agent as a message. A decision nobody answers in time is reminded and then escalated in the same room, as a reply to its card — see [When nobody answers](/use/boards/#when-nobody-answers). ## Messaging an agent is dispatching it [Section titled “Messaging an agent is dispatching it”](#messaging-an-agent-is-dispatching-it) Sending a message spends the agent’s time, so it needs the same authority as dispatching it any other way — and **approving a permission question is dispatching by another name**, so it needs that authority too. Who may message which agent is read from a `mayDispatch` claim the hub signed into your token. It is not a header, not a parameter, and not derived from anything sent alongside. See [Dispatch and grants](/use/grants/). ## Mid-turn messages are held, not refused [Section titled “Mid-turn messages are held, not refused”](#mid-turn-messages-are-held-not-refused) A message that arrives while the agent is mid-turn is **held and delivered when the turn ends**, rather than refused. The room says so as a notice. Refusing would make the room a worse place to talk than a terminal, which is the opposite of the point. ## Notifications on a phone [Section titled “Notifications on a phone”](#notifications-on-a-phone) The hub is the **push gateway**. A client registers a pusher with the homeserver, the homeserver calls the hub for every event that should reach a phone, and the hub turns each call into one push per device. A **message** push carries the room, the event id and an unread count — not the message. The device fetches and decrypts the event itself and words the notification locally. A push that carried the text would put an encrypted room’s contents through a service that has no business seeing them. One addition, and it is counts rather than text: the push for an agent’s answer that ended a turn with tool calls also carries how many steps ran and how many failed, which is what a client’s widget recap reads. ### The one exception: the fleet Live Activity [Section titled “The one exception: the fleet Live Activity”](#the-one-exception-the-fleet-live-activity) By operator decision of 2026-09-29, the Lock Screen fleet card is **pushed by the hub**, and those pushes carry **agent names, the current step’s title, and a pending decision’s question and option labels in plaintext** — readable by Apple in transit. That is a deliberate widening of the gateway’s otherwise strict line, taken so the card can say something useful rather than “an agent needs you”. It is the only place it applies: message pushes still carry ids only, and nothing else changed. A gate that offers a choice of versions never offers an approval on the Lock Screen — the card cannot show the versions — only a rejection; you choose in an updated app or on the board. A permission request or a gate the hub posted is also tagged on the push as time-sensitive, so it reaches a person through Focus — the tag, not the text. Pushes are configured per deployment and are optional: a hub with no push configuration answers that route with a `404` and still boots. Pushes are not worth taking a control plane down for. ## Turning it on [Section titled “Turning it on”](#turning-it-on) The bridge is off unless the hub is configured for it: a homeserver URL, a server name, and the application-service tokens. Deployment detail lives in the operator guide rather than here, because it is a property of your hub and not of the product. With the bridge off, everything else in AgentPod works exactly as it does now. Nothing in the console depends on it. ## Next [Section titled “Next”](#next) * [Voice notes](/use/voice/) — speaking to an agent, and being answered * [When a turn fails](/use/errors/) — the error card, and the harness plugins behind it * [Working a board](/use/boards/) — agents that take work off a SuperPipeline board * [Dispatch and grants](/use/grants/) — who may talk to whom # Checking for exposure > apn scan — a local security check for agent runtimes that needs no hub, no account and no network. ```sh apn scan ``` `apn scan` checks the agent runtimes on this host for the two ways they get taken over: * **a listener bound to every network interface** — an agent’s API reachable from outside the machine * **credential files other users can read** — the keys the agent works with, sitting where anyone on the box can take them It needs **no hub, no account and no network**. It works on a machine that has never heard of AgentPod, which is the point. ## Running it without installing AgentPod [Section titled “Running it without installing AgentPod”](#running-it-without-installing-agentpod) The installer in [Your first node](/start/first-node/) enrolls the machine, so it wants a hub and a token. `apn scan` wants neither — download the released binary on its own and run it where it lands: ```sh curl -fsSLO https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/agentpod-node-linux-amd64 curl -fsSLO https://github.com/SuperJackfruitLabs/agentpod/releases/latest/download/SHA256SUMS sha256sum --ignore-missing -c SHA256SUMS # macOS: shasum -a 256 --ignore-missing -c SHA256SUMS chmod +x agentpod-node-linux-amd64 ./agentpod-node-linux-amd64 scan ``` Each release publishes `agentpod-node-linux-amd64`, `agentpod-node-linux-arm64`, `agentpod-node-darwin-amd64` and `agentpod-node-darwin-arm64`. `apn` is only the installer’s name for this binary; downloaded directly, it keeps the release name. Nothing is written to the machine besides the file you downloaded. ## Exit codes [Section titled “Exit codes”](#exit-codes) | Code | Means | | ---- | -------- | | `0` | Clean | | `1` | Warnings | | `2` | Critical | That makes it usable directly in cron or CI. ```sh apn scan --json # machine-readable apn scan --no-color # plain output for logs ``` ## It does not guess [Section titled “It does not guess”](#it-does-not-guess) A check that cannot determine an answer reports **unknown**, and says why. It is never reported as a pass. This is a deliberate design rule, and it costs something: you will sometimes see “unknown” where a more confident tool would show a green tick. That is the trade being made. A scanner that cries wolf gets ignored, and a scanner that quietly calls an unknown a pass is worse than one that says nothing. There is also **no CVE feed**. A version-to-CVE database rots the moment it ships, and it would turn a static binary into something that has to phone home to stay honest. Every check here is a property of the machine as it is right now. ## In the console [Section titled “In the console”](#in-the-console) The same checks surface as a posture banner on nodes and stations, so an exposure found on a host you have not logged into lately is still in front of you. ## Next [Section titled “Next”](#next) * [apn and fleet](/use/cli/) — the rest of the surface # Managed skills > Knowing what skills a station actually has, installing one under review, and proving a session loaded it. A skill is a package an agent can use. Copying one onto a machine is easy; **establishing that a running session loaded it** is the hard part, and it is the part that matters — an agent with a skill it did not load behaves exactly like an agent without it. AgentPod treats that as the whole problem. Nothing here grants a product permission: a skill is a capability of an agent’s environment, not an authority. ## Inventory: what is actually there [Section titled “Inventory: what is actually there”](#inventory-what-is-actually-there) ```sh fleet skills station verify --station --profile ``` Inventory asks a station what skills it has and reports each one’s name, where it was observed, its scope, the digest of its entrypoint, its declared dependencies, and whether anything shadows it. The design rule is that **nothing is inferred from anything else**. These are separate observations: | observation | means | | ----------- | ------------------------------------- | | catalogued | the catalog knows about it | | present | it is on disk where it should be | | eligible | the harness would consider loading it | | loaded | a session has it | | exercised | it has actually run | Present does not imply eligible. Eligible does not imply loaded. An empty compatibility list proves nothing. A known observation carries a timestamp; an **unknown observation carries a reason** — never a silent `false`. The same applies to coverage. Every response says which roots it read and which it could not, so an unread root is reported as unobserved rather than treated as empty. Unreadable roots, malformed entries and bounds reached all stay visible. **Inventory never imports code**, executes hooks, starts a model, probes credentials, refreshes a session or restarts a station. Skill bodies and credentials never leave the node. A request carries only a station key. A caller cannot supply a filesystem root, another profile, a command, or an authentication path — the node resolves the station it has actually detected and scans only the roots that station’s descriptor selects. ## Plan, inspect, apply [Section titled “Plan, inspect, apply”](#plan-inspect-apply) Every change is a reviewed change, in three steps: ```sh fleet skills station plan --station --artifact fleet skills station inspect --station --operation fleet skills station apply --station --operation --plan-digest ``` The apply names the **digest of the plan it is applying**. If the plan changed between your reading it and your applying it, the apply does not match and does not proceed. This CLI never turns a plan into an implicit apply: every mutation returns the hub’s reviewed record, and you are expected to read it. Rolling back is a plan of its own: ```sh fleet skills station rollback-plan --station --profile ``` ## Artifacts and releases [Section titled “Artifacts and releases”](#artifacts-and-releases) ```sh fleet skills upload --harness --profile

archive.tgz fleet skills artifacts fleet skills artifact delete --id fleet skills release import release.json fleet skills releases ``` An uploaded archive is **validated at admission** rather than at install time, and an artifact is immutable once admitted. A release names a set of artifacts, and a release is a record you import rather than a thing you edit — so what a cohort was given stays answerable after the fact. ## Cohorts and canaries [Section titled “Cohorts and canaries”](#cohorts-and-canaries) A release reaches a fleet gradually: ```sh fleet skills cohort create --release --digest --station fleet skills cohorts fleet skills canary plan --cohort --release --digest --station fleet skills canary inspect --cohort --release --digest --station --operation fleet skills canary apply --cohort --release --digest --station --operation --plan-digest ``` A cohort is **immutable and bound to a release**: you cannot quietly change which stations got what. A canary applies that release to one station and verifies the result, so a bad skill is found on a station you chose rather than across the fleet. ## Native placement [Section titled “Native placement”](#native-placement) Some harnesses load skills as native plugins at a discovery root rather than as files in a workspace. Those have their own verbs, because activating one is a different act from copying it: ```sh fleet skills native plan --station --profile

--action activate|deactivate|rollback fleet skills native verify --station --profile

fleet skills native apply --station --operation --plan-digest ``` Native placement is **fail-closed** and gated on an operator setting: a station does not start loading native skills because a package arrived. Verification opens a fresh session and checks the skill is loaded there, because a package manifest is content evidence and not proof of registration or execution. Duplicate names across harnesses are **reported, not resolved**. AgentPod does not invent a cross-harness precedence rule it would then have to defend. ## From the console [Section titled “From the console”](#from-the-console) The console carries the same surface: a station’s skill inventory, the review screen for an installation, which harness an upload is for, trusted releases, and canary controls. The CLI exists so the same operations can be scripted and so a release can be rolled without a browser. ## Next [Section titled “Next”](#next) * [What you can do to a station](/use/panels/) — the rest of a station’s surface * [apn and fleet](/use/cli/) — the two binaries, and which one acts as you # Stations > Detecting, adopting and organising the runtimes on your nodes. A **station** is one runtime instance on a node — a single place where an agent works. ## Detection versus adoption [Section titled “Detection versus adoption”](#detection-versus-adoption) These are two different things, and the distinction is the point. **Detection** is the node agent looking at the host and reporting what it finds. It happens without you asking, and it changes nothing. ```sh apn detect ``` Prints what this host has, as JSON. No hub connection, no account required. **Adoption** is you deciding a station should be managed. Only an adopted station is in the registry, and only an adopted station has panels. The gap between them is deliberate. A machine may run agents that are none of AgentPod’s business, and detecting one is not consent to manage it. ## The tree [Section titled “The tree”](#the-tree) Stations nest. A harness that manages sub-runtimes appears as a parent with children beneath it, which is why the console shows a tree under each node rather than a flat list: ```plaintext node: workhorse └── hermes ├── coder-kai └── hanuman ``` Deleting a parent removes its children with it — a child station cannot outlive the station it runs inside. ## Capabilities [Section titled “Capabilities”](#capabilities) Each station declares which operations it supports. A request for a capability a station has not declared is refused **at the hub**, with no call to the machine at all. That ordering matters for a reason worth stating: a refusal that never leaves the hub cannot be turned into a way to probe what is on the host. ## Purpose [Section titled “Purpose”](#purpose) A station’s **purpose** is what it is for — `personal`, `work`, or any label you pick. It is not where it runs. Set it on the station. A node’s purpose is only the default applied at adoption to a station that has none of its own; the station’s is the one anything actually reads. A station nobody has labelled is filed under no Matrix space at all. ## Matrix identity [Section titled “Matrix identity”](#matrix-identity) A station can have a Matrix identity, so that talking to an agent is just messaging. Two modes, and never both at once: | Mode | Means | | --------- | ---------------------------------------------- | | `bridge` | The Application Service speaks for the station | | `harness` | The station runs its own Matrix client | In `bridge` mode the identity is minted by the Application Service and stored separately from whatever the harness reports about itself. Nothing on the host can report that identity, so nothing on the host can erase it. The station’s room is created with it, and **created encrypted** — there is nothing extra to turn on, and no window in which it existed in the clear. Rooms hang under a space named for the machine they run on, so the roster groups itself in any client that reads the hierarchy. An agent can be given an **avatar**: preview a workspace image and set it as the station’s picture. A roster of thirty identical default avatars is a roster you read by name only. What else arrives in that room — voice notes, permission questions, a card when a turn fails, a board’s approval gates — is [Talking to an agent in a room](/use/rooms/). ## Git identity [Section titled “Git identity”](#git-identity) A station can be given a key to push to a forge with: ```sh fleet stations git-identity --station fleet stations grant-push --station fleet stations revoke-push --station ``` **Granted per station, never by adopting one.** Most stations never touch git, and a forge key for every station is an account nobody uses and a key nobody revokes. The keypair is generated **on the node** and the private half never leaves it: `grant-push` asks for the public half and registers that. The hub holds no secret, so a hub compromise does not hand over commit access. **Commits carry the agent, not the host.** With the key, the hub sends the station’s commit author: the agent’s display name in readable form (a handle like `fixture-agent` — or a station key like `harness:fixture-agent` — becomes `Fixture Agent`; without a display name, the forge account’s full name minus its `(agent)` suffix), and the forge account’s email exactly as forge stores it. That email is synthetic — an agent has no mailbox — and it is what the forge links a commit to the account by, so the name carries no “(agent)” suffix: the email’s domain already says it. The node records the author beside the key and starts the station’s harness and terminal with `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME` and `GIT_COMMITTER_EMAIL` — environment, never git config, so nothing is written to any repository or to the host. A station with no identity gets none of these and keeps the host’s git config. `git-identity` shows the author as `authorName`/`authorEmail`. An identity provisioned before authors existed gets one the next time its node connects (a node update reconnects it), with no key regenerated. A harness or terminal already running keeps the environment it started with. ## Forge API access [Section titled “Forge API access”](#forge-api-access) A station’s agent can be given scoped access to a forge’s **API** (issues, pull requests, comments, reviews) for named repositories. This is separate from [git identity](#git-identity): SSH keeps carrying git, and neither grant ever creates the other. ```sh fleet stations forge-api list --station fleet stations forge-api grant --station --provider forgejo \ --instance https://forge.example --repo org/repo --preset issues fleet stations forge-api verify --station --grant fleet stations forge-api rotate --station --grant fleet stations forge-api policy --station --grant --preset issues \ --rule 'deny: DELETE /repos/**' fleet stations forge-api revoke --station --grant ``` **The agent never holds the token.** The station’s broker (`stk`) holds the credential and adds it to the requests its policy allows, whichever client the agent uses (`tea`, `gh`, `curl`, an MCP server). The hub delivers the credential **sealed** to the station’s broker key. For a supplied or app grant the hub relays ciphertext it cannot open; for a minted grant it holds the token briefly between minting and sealing (see Limits). **Operator only.** Every verb except `list` needs the hub operator seat **and** the control pair’s reach over the station’s agent; owning the station is not enough (403). An agent must not, and cannot, grant itself access. The forge account is never taken from the request: a minted grant is for the station’s occupying agent, a supplied or app grant for the `--expect-login` you declare. A minted grant on a station with no occupant is refused (409); a supplied or app grant does not need one. **Three credential modes.** | Mode | What it is | Where the credential comes from | | ------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `minted` (default) | The agent’s own forge account | The hub mints a token, limited to the listed repositories, on its configured forge | | `supplied` | A machine account you made by hand (a GitHub PAT, for instance) | You, on stdin | | `app` | A GitHub App installation, acting as the bot | You, on stdin: `app_id=…`, `installation_id=…`, `private_key=…` lines | Today the hub accepts Forgejo `minted` (only on its configured forge) and `supplied`, and GitHub `app` and `supplied`. Gitea and Bitbucket are refused until their adapters exist. A supplied or app credential is **never a flag** (there is no `--token`). It is read from stdin, sealed on your machine to the station’s broker key (fetched once from the hub, before anything is created), and only the sealed envelope is sent: ```sh pbpaste | fleet stations forge-api grant --station --provider github \ --instance https://github.com --repo org/repo --mode supplied --expect-login octo-bot ``` If the grant is created but the envelope does not arrive, the CLI names the grant: re-send with `fleet stations forge-api secret --station --grant ` (add `--mode app` for an App’s fields). A sealed credential the station does not acknowledge within 24 hours is discarded. **Policy.** A grant names its repositories and a preset: `read`, `issues`, `pull-requests` (no merge), `maintain`, or `none` (start from nothing). `--rule 'allow: METHOD /path'` and `--rule 'deny: METHOD /path'` add to it, repeatable; a rule with a body condition goes in `--rules-file`, a JSON array such as `[{"allow": "PATCH /repos/org/repo/issues/*", "when": {"body.state": "absent"}}]`. Rules narrow the token and never widen it; a `deny` wins; `DELETE` is denied by every preset. `policy` **replaces** the preset, rules and GraphQL setting (GraphQL is off unless `--allow-graphql`, so omitting the flag turns it off), and needs no new credential. A minted token’s provider-side scopes follow a changed preset only from the next `rotate`; until then the broker’s rules are what apply. **Status.** `list` prints each grant’s state, mode, account, isolation, provider, instance, preset and repositories, and never a token. States run `authorised → provisioned → delivered → verified`, plus `failed`, `revoking` and `revoked`. `verified` means the broker made a real read call as the expected account **and** a node that reports bypass evidence showed the broker carries the station’s traffic. How `verify` reads that evidence: * A client that **bypassed** the broker fails the grant (`failed`, reason `bypassed: `), even one that was `verified` before. * **No path routed** through the broker (`not-wired: `) is inconclusive: the state is kept, so a first verify stays `delivered`; the reason is written only if it would not replace another lifecycle reason. * The identity matched but the node could not show the traffic is brokered: the grant stays (or goes back to) `delivered` with `coverage-incomplete: `. * Brokered, but not every client was measured: `verified` with the note `coverage: partial (untested: )`. The harnesses’ own clients have no safe probe today, so they report `untested` and **most grants verify with partial coverage**: the generic clients were seen going through the broker, the harness’s own client was not checked. * A node too old to send bypass evidence verifies as before (identity only). A verify can take up to 90 seconds, because the node probes its clients first. Isolation is `separate-user` (the broker runs as its own OS user) or `same-user` (degraded, see below). **Rotation and revocation.** `rotate` (minted) mints a new token, delivers and verifies it, then revokes the old one; if verification fails the old token stays live. For a supplied or app grant, rotate by sending a new credential with `secret`. `revoke` first wipes the station’s copy (best effort; queued if the node is offline, which cuts the station’s access at once), then revokes a minted token at the forge, retrying until it succeeds. Reassigning, unassigning or deleting a station (or removing its node) marks its grants `revoking` in the same change and never waits on the forge; the reconcile sweep (every 5 minutes, and when a node reconnects) finishes the job. A new occupant starts with nothing. **Needs a node that supports it.** A station’s node must advertise the `forge.api` capability; an older node answers `capability-missing` rather than doing nothing. **One broker per station.** Each station gets its own broker on the node, with its own OS user, store, policy and proxy key, so one station’s credential is never in another’s store, and two stations can hold grants for the same forge host. The node creates it the first time it is needed and must run as root on Linux with systemd to do so; otherwise the node does not offer forge API access at all. **What that does not do.** The node starts agent sessions and terminals as itself, not as a user of the station’s own, so the agents it starts run as the node’s user (root, on a node with brokers). An agent that is root, or that shares a user with another station, can read what the broker protects, so such a station’s grants report isolation `same-user` (`isolationDetail`: `node-uid` or `shared-uid`) and the node refuses a second station on a node (`station-uid-shared`) unless the operator acknowledges that the stations are not isolated from each other. Separate brokers keep credentials apart, not agents. An instance is never removed automatically: `apn forge-api prune --station --yes` does it, by hand. A grant’s `present` in `list` means the node holds a policy entry for it, not that the credential was delivered or works; `verify` says that. An operator can also list services every station needs in a *shared base policy* file, which the node copies into each station’s own policy (`/etc/agentpod/stk-shared.yaml` by default; its credentials are not delivered for you). The proxy port range and the file locations are settings in the node config; the operating guide lists them. ### Limits [Section titled “Limits”](#limits) * **No protection against root.** An agent or user with root on the station can read the broker’s store. * **Same-user mode is weak, and today it is every station; write grants need your acknowledgement.** When the broker runs as the agent’s own user, only file permissions separate them, and the agent can read the broker’s store. Agents spawned by the node run as the node’s uid, so every station currently reports isolation `same-user` (`isolationDetail` `node-uid`, or `shared-uid` when acknowledged on the node). A **write grant** is any preset except `read` (`issues`, `pull-requests`, `maintain` and `none` all count), or `read` with an `allow` rule for a method other than GET, HEAD or OPTIONS. The hub refuses a write grant (409 `same-user-acknowledgement-required`) unless the operator passes `--acknowledge-same-user` (`acknowledgeSameUser: true`), on `grant` and on a `policy` that moves a grant to write. It assumes same-user unless the node has shown `separate-user` for the station’s grants, so a supplied or app write grant always needs it today. If a node’s delivery reports `same-user` for an unacknowledged write grant anyway, the grant is not delivered: the node’s copy is wiped, a minted token is revoked, and the grant is `failed` with that reason. The acknowledgement is an operator decision: the hub records who gave it and when (from the signed-in account, never the request body), the audit row records the request, `list` shows only that it was given, and a rotation keeps it. Read-only grants never need it. Agents must never pass the flag. * **A node preparing its broker answers later, not never.** The first grant on a station can make the node install its broker instance; the hub waits up to 120 s for that (`forge.api.status`, `forge.api.deliver`). If an install is already running the grant answers 503 `station-preparing` with `Retry-After`, and nothing is created; a delivery that meets it is retried with the same sealed blob. A node that refuses the station answers 409 with its code: `station-not-hosted`, `station-key-changed`, `station-uid-shared` or `stk-unavailable`. * **The hub sees a minted token for an instant**, between minting and sealing it. It stores only the sealed envelope. * **Every forge-api route needs the hub’s Forgejo admin configuration.** Without `FORGE_URL` and `FORGE_ADMIN_TOKEN` on the hub, every `forge-api` route answers 503, even for a GitHub-only (`app` or `supplied`) grant, and the reconcile sweep does not run. * **agentpod cannot revoke a supplied token at its provider.** `revoke` removes it from the station; revoke it at the provider yourself, then run `fleet stations forge-api confirm-revoked`. * **A client that ignores proxy settings bypasses the broker.** It sends a placeholder and gets a plausible 401; detection is by test, per harness, not by prevention. `verify` runs stk’s probe through the station’s own instance (curl, python3, node and a Go client against a local stand-in at a host that cannot resolve) and reports each as `honoured`, `bypassed`, `refused-ca`, `untested` or `failed` in the ack’s `bypass.clients`. A `bypassed` client makes the verify `ok: false` with a reason starting `bypassed:`. The station’s own harness client is `untested` unless a safe probe command is configured (none ships), and is never reported `honoured` without one. `bypass.paths` says which harness paths the node routed through the broker (`spawn`, and `gateway` for hermes/openclaw) and `bypass.wiringBlocked` why a path is not. `bypass.brokered` is wiring AND measurement: true only when every path is wired, the probe ran, and no client is `bypassed`, `failed` or `refused-ca`. `bypass.measured` is `complete` only when every client row (including the harness’s own) is `honoured`, else `partial`; an untested client is never evidence. The probe takes up to 45 s, is skipped when stk’s own verify already failed, and a clean result is reused for 60 s per station (never a failure; any deliver, policy, revoke or wiring change drops it). Harness probes are refused on a root node. A station with no wired path at all is `ok: false` with `not-wired:`. The hub reads all of this as described under **Status**: `bypassed` fails the grant (a verified one too), `not-wired` is inconclusive (state kept), `brokered: false` keeps or puts the grant at `delivered` with a `coverage-incomplete` note, and `brokered: true` with `measured: partial` verifies with a `coverage: partial` note. A rotation still finishes on a station that cannot show brokered traffic: once the node acknowledged the new token and its identity matched, the old token is revoked; the grant stays `delivered` with the coverage note. The reconcile sweep does not re-verify a not-brokered grant within 30 minutes; `fleet stations forge-api verify` always runs. * **Body-level rules cover JSON REST only.** A rule that would have to read a GraphQL or non-JSON body cannot be enforced. * **Forgejo’s token listing and repository confinement are to be verified live** against the real forge before relying on them for orphan cleanup and repository restriction. ## Staffing [Section titled “Staffing”](#staffing) Putting an agent in a station, or taking it out: ```sh fleet staff options fleet staff assign --station --file |- fleet staff unassign --station ``` ## Cleaning up [Section titled “Cleaning up”](#cleaning-up) Removing a station from the registry is not the same as deleting anything on the machine. For reclaiming actual disk, see the cleanup panel in [What you can do to a station](/use/panels/) — which plans first, shows you the plan, and only then applies it. ## Next [Section titled “Next”](#next) * [What you can do to a station](/use/panels/) * [Talking to an agent in a room](/use/rooms/) * [Managed skills](/use/skills/) * [Attaching an editor](/use/acp/) # Voice notes > Speaking to an agent, and being answered out loud — transcription in, synthesis out, and what each falls back to. An agent in a bridged room can be sent a voice note, and can answer with one. Both directions are optional, both are configured hub-wide and overridable per station, and neither invents anything when it is not configured. ## Sending a voice note [Section titled “Sending a voice note”](#sending-a-voice-note) A voice note is **transcribed before the agent sees it**. The transcript is posted in the room as a reply to the note, and the agent receives the text marked as what it was: ```plaintext [Voice note, 0:42, transcribed] ``` So the agent works from text, you keep the audio, and anybody reading the room later can see both. **Configure** it in the console under **Admin → Transcription**: the provider, its URL, the model, the API key, and the longest note to accept. *Test connection* sends one second of silence rather than making you record something. Per station, the station page’s **Voice notes** section offers *inherit*, *off*, or its own settings. Anything that speaks OpenAI’s `/v1/audio/transcriptions` works, self-hosted or hosted. **With nothing configured, a voice note reaches the agent as a note saying it could not be heard.** It is not silently dropped and not silently passed through as an empty message — the agent is told there was something it could not hear, which is a thing it can respond to. API keys are stored encrypted and never shown again. ## Being answered out loud [Section titled “Being answered out loud”](#being-answered-out-loud) A bridge-mode agent — any harness whose messages the hub posts — can follow its text with a voice note. | setting | meaning | | -------------------- | ------------------------------------- | | `voice_in` (default) | answer a voice note with a voice note | | `always` | speak every turn that ended with text | | `off` | never | `voice_in` is the default because it mirrors you: speak to the agent and it speaks back; type and it types. ### Which voice [Section titled “Which voice”](#which-voice) Most specific first: the station’s own voice, else the hub’s default, else **one assigned from the station’s id**. The assignment is a hash over a curated list, so it is stable across restarts and different agents mostly sound different without anybody choosing. The station page shows it as *Assigned: …*, and the owner may pick another — or type a blend of two. ### What is spoken, and what is not [Section titled “What is spoken, and what is not”](#what-is-spoken-and-what-is-not) The whole turn’s text as posted. A turn that flushed in parts — around a permission question, say — is joined, so you hear one answer rather than fragments. Over the length limit, it is spoken up to the last sentence end that fits, and **nothing is added**. No “truncated” announcement: the text above has the rest, and a voice note that editorialises about itself is worse than one that simply stops at a sentence. Never spoken: an error turn, a silent turn, an empty turn. One voice reply per turn. ### Text first, always [Section titled “Text first, always”](#text-first-always) The text is posted first and is **never delayed by speech**. Synthesis starts after the turn ends and is not awaited. If the speech service is down, busy, slow, or refuses the upload, the hub logs one warning and posts nothing — **no error card**. The answer already arrived; a second message saying the answer could not be read aloud is noise about a cosmetic failure. Logs carry duration, character count, voice and latency. Never the text. ## In an encrypted room [Section titled “In an encrypted room”](#in-an-encrypted-room) A voice note is encrypted like any other attachment — the audio is encrypted, the ciphertext uploaded as opaque bytes, and the event itself encrypted like every other message from that agent. Clients that understand voice notes draw the waveform; clients that do not show an audio file. A voice reply is **not** sent as a reply-to of its own text. Every client that does not know the key for “this speaks that message” would quote the whole answer again above the voice note, so the link is carried in a field instead. It is also marked as a quiet event, so your phone does not buzz twice for one answer. ## Next [Section titled “Next”](#next) * [Talking to an agent in a room](/use/rooms/) — the bridge these ride on * [What you can do to a station](/use/panels/) — where per-station settings live