The GitLab project is private (its parent groups are private, so it cannot be made public): the v0.1.1 one-liner answered 401 anonymously. install.sh gains --token / MONKY_DEPLOYD_TOKEN and sends `DEPLOY-TOKEN: <token>` (a GitLab deploy token, scope read_package_registry only, revocable) on every registry download, the script itself included; the token goes through a 0600 curl -K file (never the command line, the log or an xtrace). The grant is taken via --bootstrap-file when the script is piped (stdin IS the script). Ansible: monky_deployd_download_token (vaulted) -> DEPLOY-TOKEN header, no_log. Docs explain why, the token's scope and the --source gitea alternative (split-horizon Gitea, cbs/iac#102). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
5.3 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 isopenziti(transportsdk), loaded lazily insidetransport.SdkTransport. No PyYAML:config.parse_yaml_subsetreads 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 loudLEASE_SHAPEfailure, 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=75in the unit. - Prod never purges.
-vis 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 acurrentsymlink = 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 generic package registry + release the installer downloads from (PRIVATE
project — the installer sends a read-only deploy token as DEPLOY-TOKEN; the Gitea name is
split-horizon inside the estate, cbs/iac#102) and — with GITEA_TOKEN —
the Gitea release (install.sh --source gitea, off-estate; 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
openzitiat 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.