Files
monky-deployd/CLAUDE.md
T
mdella c966450d8e fix(install): fetch from scm.tikali.ai (public project) — Gitea name is split-horizon inside the estate
Inside the estate gitea.cbs.tikali.net resolves to jump1's RED EIP (10.10.0.175),
which has no HTTP ingress, so backend boxes could not download the install
artefacts from the Gitea mirror (cbs/iac#102). scm.tikali.ai is reachable from
those boxes and the project is now public, so the GitLab generic package
registry becomes the PRIMARY source:

- packaging/install.sh: default source = scm.tikali.ai generic package registry
  (projects/69/packages/generic/monky-deployd/<ver>/...); `--source gitea` /
  MONKY_DEPLOYD_SOURCE=gitea keeps the Gitea release as the off-estate
  alternative; --base-url / MONKY_DEPLOYD_BASE_URL still override the base.
- ansible role defaults: monky_deployd_base_url/_deb_url point at the registry,
  Gitea layout kept as a commented alternative.
- README / docs/OPERATIONS.md / CLAUDE.md / CI comments + release description:
  both locations keep being published (release + release:gitea).
- Version 0.1.1 (the tag gate refuses v* tags whose version != __version__);
  tests compare against __version__ instead of a literal. No agent change.

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

5.2 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 generic package registry + release the installer downloads from (public project; 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 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.