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.mddescribed pi's irreversible-action floor as a "hard-block floor". It has been an ask-gate sinceirreversible-floor.tswas rewritten; no shipped rule usesblock.- 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/messageswas 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@.serviceandpackaging/payload/units/host/host-units/agentctl-host-shell.serviceboth 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 shippedagentctl-shell-reap.{service,timer}units.internal/cli/terminal_proxy.gotells an operator touse agentctl sshwhen an agent is on another host with no dial-in registry. There is nosshverb in the command tree. A test (terminal_proxy_test.go) pins the string, so fixing it is a two-file change.internal/cli/notify.gostill references a deletedcurrentAdminfunction in a comment — admin became a set, the comment didn't follow.- Two registered CLI
Usagestrings 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.mdlists the default host-cron seed asjob-watchdog, load-monitor, runaway-reaper—defaultHostCron(agentlists.go) actually seeds four:fleet-usage, job-watchdog, load-monitor, runaway-reaper.