Files
monky-deployd/CLAUDE.md
T
mdella 1c42e913a8 feat: monky-deployd v0.1.0 — pull agent over the mesh (ADR-0028)
Stdlib-only Python 3.12 agent for docker VMs and laptops: flock → checkin
(bearer = the agent's OpenBao token, bootstrapped from the install kit's
jwt-tenancy deploy grant) → action apply|none|down → bundle (sha256
verified) → refusal checks (unresolved ${VAR} names only, manifest paths
pinned to monky/data/<env>/see/, privileged/host-network, rollback, disk
need×1.5+headroom) → lease → POST /v1/auth/jwt-tenancy/login → KV reads →
.env 0600 → promote → compose pull/up → wait healthy → report; finally
renew-self / re-lease before max TTL, scrub. Exit 0/75/78/1. Redactor log
filter. Transports sdk (openziti) / proxy (ziti tunnel proxy 18443/18200) /
system. Laptop mode.

Packaging: hardened oneshot + 60 s timer + proxy unit, nfpm .deb with
/opt/monky-deployd/venv, install.sh for Ubuntu 26.04 (Gitea release
download, enrol, ACLs, bootstrap from stdin), ansible role skeleton for
osg1-07. CI: lint/test on every change; wheel (openziti on ubuntu:26.04) and
package (nfpm) allow_failure until runner egress is proven; GitLab release +
release:gitea on v* tags. Docs: README, PROTOCOL, OPERATIONS, CHANGELOG,
CLAUDE/AGENTS.

Divergence noted: monky-tenancy main (MR !15) still ships the AppRole lease
and kit; this agent implements the plan's Gate 1 RESULT (login_jwt, no
unwrap) and refuses an AppRole lease loudly (LEASE_SHAPE).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
2026-09-05 08:01:36 +00:00

5.1 KiB

monky-deployd — CLAUDE.md

What this is

monky-deployd is the on-box pull agent for Monky backend environments on docker VMs and laptops (MONKY-ADR-0028). It dials monky-tenancy over the OpenZiti mesh with the box's own host identity, converges the box to the published bundle (docker compose), reads its secrets from OpenBao itself, and reports — the check-in is also the liveness heartbeat. It is the apply half of the backend lifecycle; the API/DB half is monky-tenancy (/v1/backends*, /v1/agent/*), the composition source is monky-deploy (renderer), the console is monky-mgmt-ui. k8s backends have no agent (ArgoCD applies).

Vocabulary (hard rule)

backend, environment (env_id = env-<tier>-<nn>), bundle, deploy grant, check-in. Check-in returns action ∈ apply|none|down; a report carries result ∈ applied|failed|down. There is no desired_sha="down", no "absent", no "up" result. Never "tenant" (the API repo's name is the one exception).

Non-negotiables

  • Stdlib only in monky_deployd/. The only optional import is openziti (transport sdk), loaded lazily inside transport.SdkTransport. No PyYAML: config.parse_yaml_subset reads the config; keep the install kit / ansible template inside that subset.
  • The agent never receives a secret from tenancy and never logs a value. Every value read from OpenBao (and every token/grant) goes through redact.REDACTOR.add() the moment it exists; refusals and reports name variables, never values. Tests assert the report tail carries none.
  • No AppRole, nothing to unwrap. Auth is POST /v1/auth/jwt-tenancy/login {"role":"see-env", "jwt":<grant>}; the resulting Bao token is the bearer to tenancy. An AppRole-shaped lease is a loud LEASE_SHAPE failure, not a fallback.
  • Refuse before you lease. All bundle checks (sha, env, unresolved vars, manifest paths, privileged, rollback, disk) run before POST /v1/agent/lease, so a refused bundle costs no lease (5/h budget) and no OpenBao login.
  • Exit codes are a contract with the timer and the loop: 0 / 75 (temporary, retry) / 78 (AGENT_ENV_MISMATCH, never retried) / 1. SuccessExitStatus=75 in the unit.
  • Prod never purges. -v is dropped on prod even if tenancy asks; the agent never decides state.
  • Paths: bearer at /var/lib/monky-deployd/bao.token (0600), grant at /etc/monky-deployd/bootstrap.jwt (consumed then deleted/truncated), releases under <deploy_dir>/releases/<sha> with a current symlink = the compose project directory.

Layout

module role
cli.py argparse, journald-friendly logging (<N> priority when JOURNAL_STREAM), run/status/bootstrap/version
agent.py the tick (Agent.run_once), action dispatch, refusal checks, token upkeep, staging/promote
config.py YAML-subset reader + Config dataclasses + validation
transport.py sdk / proxy / system transports + HttpClient (http.client with TLS SNI)
tenancy.py the four calls; error classes Unauthenticated, EnvMismatch, RateLimited, LeaseShapeUnsupported
bao.py login, lookup/renew/revoke-self, KV-v2 read, kv_data_path (env pinning)
bundle.py tar parse, bundle_sha (tenancy's formula), unresolved-var / privileged scans, .env rendering
compose.py docker compose wrapper, ps --format json parsing (NDJSON + array), health wait
state.py state.json, token file, atomic 0600 writes, flock
redact.py the log filter

Tests

pytest is hermetic: tests/fakes.py runs a fake tenancy agent endpoint and a fake OpenBao as real HTTP servers on loopback (shapes of record, incl. env pinning, superseded grants, rate limits, the AppRole shape), and tests/fakebin/docker is a stub on PATH that records every invocation. No network, no docker daemon, no root. Add a test for every new refusal code.

Gates before you push

ruff format --check . || exit 1
ruff check . || exit 1
pytest || exit 1
bash -n packaging/install.sh || exit 1
systemd-analyze verify packaging/systemd/*.service   # when available

Releasing

Bump monky_deployd.__version__ + pyproject.toml + packaging/install.sh DEFAULT_VERSION + CHANGELOG.md, merge to main, tag vX.Y.Z (protected v*). The tag pipeline builds the .deb, publishes the GitLab release and — with GITEA_TOKEN — the Gitea release the installer downloads from (docs/OPERATIONS.md has the manual recipe). lint refuses a tag whose version differs from __version__.

What NOT to do

  • Don't add a dependency to the agent; don't import openziti at module top level.
  • Don't log, print or report a secret value, a token or a grant; don't put values in state.json.
  • Don't add an AppRole/unwrap path "for compatibility".
  • Don't apply when a check fails "with a warning"; refuse, report, exit 1.
  • Don't retry AGENT_ENV_MISMATCH; don't turn 75 into a busy loop (the timer is the retry).
  • Don't edit *.ru.md (doc-translator owns them); keep line 1 <!-- xlate:verbatim-fences -->.
  • Cite decisions as MONKY-ADR-NNNN; don't restate them here.