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

92 lines
5.2 KiB
Markdown

<!-- xlate:verbatim-fences -->
# 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
```sh
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.