Files
monky-deployd/docs/PROTOCOL.md
T
Claude-Docs-Manager 05724edde2 docs(protocol): the AppRole divergence is resolved history; the lease example and fake follow tenancy's AgentLeaseOut
DD-0524 — PROTOCOL.md §Divergences bullets 1-2 said monky-tenancy `main`
"still implements the AppRole lease and install kit" and that "the kit's
generated config uses tenancy.base_url". Both were false when the page was
published: tenancy !17 (288df791, merged 07:48Z) shipped AgentLeaseOut{env_id,
login_jwt, ttl_s, mount, role, addr}, the ES256 grant signer and the JWKS
sixteen minutes before v0.1.0 was tagged, and !22 (61bd0281) made the kit run
install.sh with flags instead of writing a config. The two bullets are now
dated "Resolved" notes; bullets 3-4 (report `detail`, X-Bundle-Sha) stand.

DD-0526 — the lease example sent `reason` and received a nested `vault{}`;
tenancy's AgentLease is `{env_id}` and AgentLeaseOut carries `addr` at the top
level (no vault object). The example now shows tenancy's shapes (with a note
that the agent still sends `reason` and tenancy ignores unknown fields), the
checkin example gains `auth_mount`/`auth_role` so it is the full AgentVaultOut,
tests/fakes.py emits `addr` the way tenancy does, and tenancy.py's docstring
and lease() read `addr` first (the `vault{}` fallback is kept so an older fake
or tenancy still leases). Verified against monky-tenancy app/schemas_backends.py
at 1fd51454 (AgentLease 296-297, AgentLeaseOut 300-309, AgentVaultOut 278-283).

Gates (local, py3.12): ruff format --check, ruff check, pytest 50 passed,
bash -n packaging/install.sh.

Doc-Drift: DD-0524 fixed
Doc-Drift: DD-0526 fixed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AW3QqEpwLV69KHn24Re45Q
2026-09-06 23:13:09 -07:00

9.8 KiB
Raw Blame History

monky-deployd protocol

The agent side of the backend lifecycle agent protocol — MONKY-ADR-0028 §2/§3 (amendment 2026-09-05) and design doc 24 §2.1a, §3.3, §4. The tenancy side is monky-tenancy app/agent_main.py / app/api/agent.py / app/schemas_backends.py; its shapes are the contract, this page is how the agent uses them.

Where and how

  • Where: tenancy's agent entrypoint — a second container (app.agent_main, 127.0.0.1:8081) bound to the ziti service monky.tenancy.deploy by a ziti-edge-tunnel run-host sidecar. Unreachable from the public ingress. Only host identities carrying #monky-deploy-agent may dial (the broker adds the attr when the identity is created at kit reveal).
  • Transport: plain HTTP inside the mesh (transport: sdk dials by service name; proxy127.0.0.1:18443). OpenBao is the existing openbao ziti service (#openbao-client), dialled as https://bao.cbs.tikali.net:8200 (an intercept name), TLS validated against the openbao-ca certificate (bao.ca_bundle).
  • Auth: Authorization: Bearer <the agent's OpenBao token>. Tenancy verifies it with auth/token/lookup (cached 60 s), pins the request's env_id to the token's meta.env_id (403 AGENT_ENV_MISMATCH → exit 78, never retried), and compares meta.grant_jti against backend_leases (401 AGENT_UNAUTHENTICATED when the grant was superseded or revoked).
  • Rate buckets per env: checkin 6/min, bundle 10/min, report 30/min, lease 5/h (429 LEASE_RATE_LIMITED, Retry-After) → the agent treats 429 as temporary (exit 75).
  • Errors are {code, detail} JSON; the agent maps them to TenancyError subclasses.

Bootstrap → lease → token (the grant flow)

kit reveal (admin, once)     tenancy signs a bootstrap DEPLOY GRANT: ES256 JWT
                              iss <tenancy issuer>, aud openbao-see-env, sub agent:<env_id>,
                              env_id, kind deploy-grant, jti (single-use, recorded), exp now+1h
install.sh                   stages it at /etc/monky-deployd/bootstrap.jwt (0600)
first tick                   POST https://bao…/v1/auth/jwt-tenancy/login {"role":"see-env","jwt":<grant>}
                              → auth.client_token (ttl 24 h, max 30 d, policy see-env,
                                 metadata {env_id, grant_jti} from the mount's claim_mappings)
                             token → /var/lib/monky-deployd/bao.token (0600); grant deleted
every tick                   Bearer <token> → checkin / bundle / lease / report
apply                        POST /v1/agent/lease → {login_jwt,…} → jwt-tenancy login → NEW token
                              (becomes the bearer; the old one is revoke-self'd) → KV reads
upkeep (finally)             lookup-self; ttl < 12 h → renew-self; age > 30 d  2 d → re-lease

OpenBao side (Terraform, tikali/services/security/openbao): mount jwt-tenancy (jwks_url = tenancy's GET /.well-known/agent-jwks.json, in-cluster), role see-env (role_type=jwt, user_claim=env_id ⇒ one alias/entity per env, bound_audiences=["openbao-see-env"], bound_claims={"kind":"deploy-grant"}, claim_mappings={"env_id":"env_id","jti":"grant_jti"}, token_ttl=86400, token_max_ttl=2592000, token_no_default_policy=true), policy see-env = monky/data/{{identity.entity.aliases.<accessor>.metadata.env_id}}/see/* read + metadata list + auth/token/{renew-self,lookup-self}. Gate 1 v2 passed 2026-09-05 07:40Z (distinct entity per env).

Second-reveal semantics. A kit re-reveal (or a retire) supersedes every earlier grant of the backend and revokes the token accessors tenancy knows. The old kit's grant fails at login (unknown/used jti), and a token already minted from it is refused at the next call with 401 AGENT_UNAUTHENTICATED (its meta.grant_jti is superseded) — not AGENT_ENV_MISMATCH. The agent then deletes its token; if a fresh bootstrap.jwt is on disk it bootstraps again in the same tick, otherwise it exits 1 and says "re-run the install kit".

The four calls

POST /v1/agent/checkin

{"env_id": "env-qa-02", "agent_version": "0.1.0", "applied_sha": "3f9c…",
 "host": {"hostname": "env-qa-02", "os": "ubuntu-26.04", "kernel": "7.0.0-30-generic",
          "docker": "28.3.0", "compose": "2.32.0", "free_bytes": 61234567890,
          "agent_version": "0.1.0", "transport": "sdk", "site": "cbs", "laptop_mode": false},
 "containers": [{"name": "see-backend", "state": "running", "health": "healthy"}]}
{"env_id": "env-qa-02", "desired_sha": "7a10…", "action": "apply", "purge_volumes": false,
 "bundle_url": "/v1/agent/bundle/env-qa-02/7a10…", "checkin_interval_s": 60,
 "vault": {"addr": "https://bao.cbs.tikali.net:8200", "mount": "monky", "prefix": "env-qa-02/see",
           "auth_mount": "jwt-tenancy", "auth_role": "see-env"}}

action: apply (desired ≠ applied), none (converged → heartbeat), down (retire; purge_volumes is only meaningful here). vault.mount is the KV mount; the agent adopts it if it differs from config. vault.addr is informational — the transport decides how OpenBao is reached.

GET /v1/agent/bundle/{env_id}/{sha}

application/x-tar, Cache-Control: no-store, X-Bundle-Sha (tenancy) / X-Bundle-Sha256 (doc 24; both accepted). Members: docker-compose.yml (or compose.yaml), .env.template, secrets.manifest.json, bundle.json, optionally .env.example. No secret material — the agent refuses a manifest entry carrying a value. The agent recomputes

sha256( for name in sorted(files): name + "\0" + content + "\0" )

and refuses (BUNDLE_SHA_MISMATCH) unless it equals desired_sha. 404 BUNDLE_NOT_FOUND when the sha is not published.

secrets.manifest.json (paths + versions, never values):

{"vault_mode": "openbao",
 "entries": [{"var": "GEMINI_API_KEY",    "path": "monky/env-qa-02/see/gemini_api_key", "kind": "supplied",  "version": 2},
             {"var": "POSTGRES_PASSWORD", "path": "monky/env-qa-02/see/pg_password",    "kind": "generated", "version": 1},
             {"var": "SEE_ADMIN_TOKEN",   "path": "monky/env-qa-02/see/admin_token",    "kind": "generated", "version": 1}]}

Path forms monky/<env>/see/<name> (tenancy) and monky/data/<env>/see/<name> (doc 24) both map to GET /v1/monky/data/<env>/see/<name>?version=N; the <env> segment must equal the agent's env_id. version: null = latest. The secret document is {"value": "…"}.

bundle.json (monky-deploy render_bundle_json): env_id, tier, site, bundle, target, target_class, address, pull, renderer, images_policy, secrets_provider, agent{…}, files[]. The agent honours allow_privileged, allow_rollback, disk_need_bytes (top level or under agent), refuses a renderer other than compose, and treats tier == "prod" as prod.

POST /v1/agent/lease

{"env_id": "env-qa-02"}

(The agent also sends "reason": "apply" | "renew" for its own logs; tenancy's AgentLease is {env_id} and ignores unknown fields.)

{"env_id": "env-qa-02", "login_jwt": "eyJ…", "ttl_s": 3600, "mount": "jwt-tenancy", "role": "see-env",
 "addr": "https://bao.cbs.tikali.net:8200"}

addr is the OpenBao address for the login below, top-level (tenancy AgentLeaseOut); the KV mount/prefix come from checkin's vault, not from the lease. Then POST /v1/auth/{mount}/login {"role": "{role}", "jwt": "{login_jwt}"} on OpenBao. A body with wrapping_token / role_id (the pre-Gate-1 AppRole lease) is refused with LEASE_SHAPE → report failed. 429 LEASE_RATE_LIMITED → exit 75.

POST /v1/agent/report

{"env_id": "env-qa-02", "sha": "7a10…", "result": "applied",
 "log_tail": "INFO checkin: action=apply …\nINFO applied 7a10c0ffee11 (2 container(s) healthy)",
 "containers": [{"name": "see-backend", "state": "running", "health": "healthy"}],
 "detail": "ENV_INCOMPLETE: unresolved: SEE_ADMIN_TOKEN"}         // detail only on failed

result: applied (sets applied_sha, health ok — also the heartbeat on none), failed (health degraded; detail = code + names), down (clears applied_sha). log_tail ≤ 16 KiB, the first 2 KiB land in the audit log — it has been through the redactor.

Divergences (2026-09-05)

  • Resolved 2026-09-05 (before v0.1.0 was tagged): monky-tenancy !17 (288df791) landed the tenancy side of the Gate 1 RESULT / ADR-0028 amendment — AgentLeaseOut{env_id, login_jwt, ttl_s, mount, role, addr}, the ES256 deploy-grant signer (app/agent_keys.py), GET /.well-known/agent-jwks.json, no AppRole and no response wrapping anywhere in tenancy. The LEASE_SHAPE refusal of a wrapping_token/role_id body stays in this agent as a guard against a stale tenancy, not as a description of main.
  • Resolved 2026-09-05: the kit no longer generates a config file. Since monky-tenancy !22 (61bd0281) the one-time install script stages the bootstrap grant and runs install.sh --env … --site … --version … --bootstrap-file …, and install.sh writes /etc/monky-deployd/config.yaml (tenancy service name, OpenBao address and jwt-tenancy/see-env). tenancy.base_url: remains an accepted alias for tenancy.{scheme,host,port} (config.py) for hand-written configs.
  • report gains an optional detail (doc 24 §3.3); tenancy's AgentReport ignores unknown fields today — if strict bodies land, detail folds into log_tail.
  • Bundle sha header: tenancy sends X-Bundle-Sha, doc 24 says X-Bundle-Sha256; the agent reads either and trusts only its own computation.