Skip to content

Known issues

Real gaps and bugs found while writing this documentation — read out of the actual code, not filed as GitHub issues yet. Each entry names the mechanism, why it's true, and whether a fix is obvious. Nothing here has been fixed as part of the docs work that found it; fixing was out of scope for a docs change.

Security

Mutating webview API routes have no authorization check

handleAgentAdd (/api/agents/add), handleApplyAll (/api/apply-all), and every /api/host/<host>/... handler (catalog add/remove, defaults set, gateway install, slack-gateway install) validate HTTP method and request body shape, then execute — none of them call scopeFor, allowAgent, fleetView, or adminScope first. The sibling route /api/agents/restart does gate on fleetView (if !s.fleetView(r) { ... }), so the pattern exists in the codebase and simply wasn't applied to these handlers.

Fix is mechanical: mirror handleAgentRestart's gate into each of the ungated handlers.

The webview gateway trusts an absent auth header as "unscoped operator"

scopeFor treats a missing X-User-Email header as the unscoped-operator identity rather than rejecting the request. Combined with the two findings below, this is the root cause both downstream issues trace to.

Any local co-resident process gets full-fleet cockpit view

Port 8731 (the cockpit) is genuinely loopback-bound in code — verified, not just documented — so remote callers are correctly excluded. But loopback-bound only excludes remote callers; nothing restricts local ones. On a host where agents run beside the gateway (this host included), any agent's Linux uid can curl http://127.0.0.1:8731/ with no header and be served as an unscoped operator with full-fleet view. The nftables port guard only covers per-agent port bands, not this fixed host-service port.

The same gap grants a shell, not just a view, via the terminal route

When the cockpit runs with no login proxy in front of it (the loopback top --web case), allowAgent returns true for every agent once scopeFor reports unscoped — so the admin marker gating /terminal/ is bypassed entirely, and whoever reaches the port gets a shell as any agent holding that marker. Two independent passes over unrelated pages (the webview gateway and the terminal-access mechanism) converged on this same root cause from different angles.

webview-login (:8736) binds all interfaces, not loopback

http.ListenAndServe(":8736", …) — empty host. Existing docs (quickstart, cockpit) claimed "both listeners bind to localhost"; only :8731 actually does. :8736 is authenticated, so an exposed listener is a login page rather than an open door, but the reverse proxy is doing more of the real access-control work here than documented.

The login rate limit is bypassable off the proxy path

clientIP reads X-Forwarded-For first. A caller that reaches :8736 directly (not through the reverse proxy) supplies its own XFF and mints a fresh rate-limit bucket per attempt. The preemptive Authorization: Basic path has no rate limit at all.

Admin is root-equivalent on its host, contrary to the file modes

rootOnlySecretDenial exempts any admin-scoped caller: AGENTCTL_HOST_OP_SECRET and AGENTCTL_HOST_OP_GRANT_SECRET (the two keys that sign fleet-wide root executions) are 0600 root:root on disk but readable by an admin agent via agentctl secret get, while refused to every other agent in every scope including self. Model the admin role as root-equivalent on its host, not a lesser privileged tier.

A pi self-scope mcp add can write shared catalog state

ensureWrappedTool (reached from a self-scope mcp add on the pi runtime) generates tool.yaml/cli.sh/SKILL.md into the shared repo's tools/<name>/ and commits — the same class of write that tool wrap-mcp gates operator-only when done deliberately. Not conclusively a bug (it's derived from an already-curated manifest and never overwrites an existing tool), but an asymmetry worth a second opinion.

Bugs

Subsessions don't inherit a Foundry/custom endpoint

packaging/payload/scripts/start-subsession.sh sources agent.env without set -a, so ANTHROPIC_BASE_URL/ANTHROPIC_FOUNDRY_* stay unexported shell variables and never reach the claude child process. A Foundry-routed agent's subsessions silently keep billing the direct Anthropic API instead of Azure Foundry, with no error anywhere. The identical bug was already fixed for crons — cron-run.sh has set -a, added specifically to close this failure mode (see that script's header and test/cost_cap_test.sh). Subsessions were missed. One-line fix.

agentctl defaults add tools <mcp> cannot work

tools is a valid deltaCategories entry and loadDefaultsOverlay parses a tools: block, but defaultsCatalogEntries() has no "tools" case — it returns nil, so validateDefaultsTarget rejects every item with exit 1. Meanwhile the code's own comments and concepts/config-layering.md both describe host-overlay MCP defaults as a real, supported path. Currently reachable only by hand-editing defaults.yaml. One-line fix: case "tools": return mcpCatalogNames().

Cron log directory is never rotated

Nothing rotates ~/.local/state/agentctl/log/cron-*.log. The shipped logrotate rule (host_content.go) only covers /var/log/agentctl/*.log. Confirmed by tracing every reference to the per-agent log dir: created in provision_native.go / provision_pi_native.go, written by cron-run.sh, read by the failure alerter and the watchdog — no pruning anywhere in that chain.

The cron failure alerter can't find an ad-hoc cron's log

agentctl-notify-failure.sh gates its job-log lookup on the unit pattern agentctl-*@*.service. Ad-hoc usercrons are named agentctl-usercron-<agent>-<label>.service — no @ — so a failure alert for one of these always reports "(no per-agent job log found)".

Documentation vs. code, corrected in place

These were caught and fixed as part of authoring the pages that made the claim — listed here for visibility, not because they're still wrong.

  • reference/runtimes.md described pi's irreversible-action floor as a "hard-block floor". It has been an ask-gate since irreversible-floor.ts was rewritten; no shipped rule uses block.
  • The informal "deb > overlay > localhost" precedence ladder doesn't hold as a single ordering. Three independent mechanisms govern config layering — resolution (full override), membership (delta algebra, no override), and ownership (symlink pruning) — see Config layering.
  • /api/messages was listed in an earlier draft of the webview API inventory as a cockpit route. It isn't — it's the Teams gateway's Bot Framework webhook on :3978, path-routed by Caddy.

Stale comments and dead references (not fixed, out of scope for docs)

  • packaging/payload/units/agent/terminal@.service and packaging/payload/units/host/host-units/agentctl-host-shell.service both still say the cron reaper "is not built yet" (agentctl-shell-reap.timer does not exist in this tree). It exists: agentctl shell reap (internal/cli/shell_reap.go) plus the shipped agentctl-shell-reap.{service,timer} units.
  • internal/cli/terminal_proxy.go tells an operator to use agentctl ssh when an agent is on another host with no dial-in registry. There is no ssh verb in the command tree. A test (terminal_proxy_test.go) pins the string, so fixing it is a two-file change.
  • internal/cli/notify.go still references a deleted currentAdmin function in a comment — admin became a set, the comment didn't follow.
  • Two registered CLI Usage strings are stale against their actual flag parsers: agents add (cli.go) omits --channel, --user-email, --owner-aad/--owner-email; cron add (cli.go) omits --agent, --run-on, --script-stdin.
  • concepts/architecture.md lists the default host-cron seed as job-watchdog, load-monitor, runaway-reaperdefaultHostCron (agentlists.go) actually seeds four: fleet-usage, job-watchdog, load-monitor, runaway-reaper.