Skip to content

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 apply exits 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.