Build from source¶
./build.sh # docker: go build + fetch+pin sops/rtk + nfpm → ./dist/*.deb
Everything runs in throwaway docker run --rm containers (Go build, checksum-verified
sops/rtk downloads, nfpm). Go is never installed on the build host. The vendored
payload is static (packaging/payload/), so what is in the tree is what ships.
What the .deb installs¶
| Path | What |
|---|---|
/usr/bin/agentctl |
the CLI, the only surface anyone sees |
/usr/bin/agentd |
the root daemon: socket + authz boundary + reconcile (enabled by provision) |
/usr/lib/agentctl/bin/sops |
vendored, pinned sops (not apt-installable on the target distro; arch-dependent → /usr/lib) |
/usr/lib/agentctl/bin/rtk |
vendored, pinned CLI-output token-optimizer; host-base puts it on PATH |
/usr/lib/agentctl/bin/mcptools |
vendored, pinned generic MCP-over-stdio CLI that pi wrap-mcp tools shell out to; host-base symlinks it onto PATH |
/lib/systemd/system/agentd.service |
daemon unit (Type=notify; enabled by provision) |
/usr/share/agentctl/helpers/ |
vendored login/pairing helpers (PKCE + OAuth flows, prime-bot) the native converge/login/plugin paths shell |
/usr/share/agentctl/units/ |
systemd unit files the native converge reconciles onto the host (units/agent/ per-agent templates, units/host/ host units + slices) |
/usr/share/agentctl/templates/ |
the claude-agent systemd override template the native converge renders |
/usr/share/agentctl/telegram-plugin/ |
pinned channel plugin source (channel + self-login) |
/usr/share/agentctl/hooks/ |
Claude Code lifecycle hooks (forward-to-plugin, session-start-memory, kb-reminder, compact-notify, send-telegram, narrate-cron-skip) + hooks/registry.yaml |
/usr/share/agentctl/scripts/ |
agent runtime scripts (start-agent, plugin-pin, watcher handlers, narrate, agent-notify) |
/usr/share/agentctl/mcp/ |
the MCP catalog: one self-contained, relocatable dir per MCP (mcp.yaml + bridge/server + vendored deps); the one operational catalog (pinned/vendored). Runtime-added items live under /var/lib/agentctl/catalog |
/usr/share/agentctl/cron/ |
per-user cron scripts + a cron-run.sh wrapper; cron/units/ holds the agentctl-<job>@.{service,timer} templates (host-scope cron units ship under the host-base role's host-units/) |
/usr/share/agentctl/skills/, /usr/share/agentctl/agents/ |
shipped skills + subagents (drop-to-add; symlinked per agent, not a catalog, no pinning) |
/usr/share/agentctl/defaults/ |
canonical default file set for a fresh agent (CLAUDE.md, settings.json, the ~/workspace zk notebook scaffold); installed non-destructively (scaffold-once) by agent-add |
/usr/share/agentctl/docs/ |
reference docs consumed by skills (steering-model.md drives the housekeeper + dream-cycle convergence rules) |
/usr/share/agentctl/tools/ |
the pi CLI tool catalog: one self-contained tools/<tool>/ dir (tool.yaml + cli.sh + SKILL.md + vendored deps); pi's MCP replacement, overlaid repo-first. Runtime/generated tools live under /var/lib/agentctl/tools |
/usr/share/agentctl/runtime/pi/ |
the pi runtime payload: the telegram↔pi bridge (TS + package.json), pi-run.sh, pi-cron-run, and the lifecycle extensions (extensions/*.ts); staged per-agent + bun install by the native pi provision |
/lib/systemd/system/pi-agent@.service |
the pi agent template unit (no PTY; runs the SDK-embedding bridge) |
Install is non-destructive: no users created, no units enabled, no reconcile run. The
first state-changing step is always an explicit agentctl provision.
apt dependencies¶
The depends: block in packaging/nfpm.yaml is the authoritative list, and it is the only
place base packages are declared — the converge no longer re-asserts them with its own
apt-get install. Four of them are worth knowing about because their absence is a hard
failure rather than a missing feature:
nftables— the per-agent port guard.agentctl applyexits 1 without it rather than reporting complete on an unguarded host.tmux— the PTY behind both interactive shells, so a shell survives a page reload.ffmpeg— the voice-note transcode, and the media path generally.libreoffice-*+pandoc+ocrmypdf— the Office and document-intake paths.
sops is deliberately not an apt dependency: the target distro does not package it, so
it is vendored on-disk and pinned. The same goes for rtk and mcptools.
Repository layout¶
cmd/agentctl/ CLI entrypoint (thin client → socket for mutations)
cmd/agentd/ root daemon entrypoint (socket, peercred authz, serialized reconcile)
internal/cli/ stdlib-only command tree (provision/agents/mcp/secret/login/apply/status/…)
internal/daemon/ agentd core: listener, fifo lock, job registry
internal/proto/ socket wire protocol (Request + streamed Frames)
internal/client/ CLI-side dispatch: route mutations through the socket, run reads locally
packaging/
nfpm.yaml .deb spec (deps, file map, maint scripts)
agentd.service systemd unit (disabled)
postinst.sh daemon-reload only (non-destructive)
prerm.sh stop/disable on removal (safety net)
payload/ the /usr/share/agentctl/ payload (helpers, units, templates, hooks
+ hooks/registry.yaml, scripts, mcp, cron, skills, agents, tools, channel plugin)
repo/ the distribution server image (signed apt + yum repos, mac binaries)
docs/ the documentation site image (this site)
build-image.sh build + tag + push one release container image
build.sh containerized build (binaries + vendored sops/rtk + .deb)
ci-local.sh the gate; .github/workflows/ci.yml runs this same script
test/ end-to-end validation
docs/ this site (mkdocs.yml at the repo root)
Runtime layout, after agentctl init¶
/opt/agentctl/ the GitOps repo (desired state) — git, push/clone-able
agents.yaml fleet ledger: host block + host keys
agents.d/<name>.yaml per-agent shards (one file per agent — concurrency-safe)
secrets.d/*.sops.yaml sops/age-encrypted per-owner store (committed; OAuth excluded)
mcp/ skills/ agents/ hooks/ tools/ catalog overlays (overlay the shipped defaults)
hooks/registry.yaml hook overlay registry
/etc/agentctl/ host-local only — age.key (identity) + env/ (rendered, plaintext)
/usr/share/agentctl/ shipped payload (read-only defaults)
Building the docs site¶
./packaging/docs/build-docs.sh # → dist/docs-site
uvx --with mkdocs-material mkdocs serve # live preview on :8000
build-docs.sh pins the mkdocs versions and passes --strict, so a broken internal link
fails the build. It is what ci-local.sh, the CI workflow and the container's build stage
all run.