feat: pull private images without a hand docker login; surface the pull error

Design merged first: monky-design-docs !225 (doc 24 §4a). Pairs with monky-tenancy!40, which
copies the estate-wide read-only Harbor robot into each environment's own prefix and marks the
manifest entry `use: registry-auth`.

- That entry is not an env var (it would otherwise land in .env and therefore in every container's
  environment). The agent parses it — JSON, or `username:password` with the new `registry_host` —
  and writes `<state_dir>/docker/config.json` 0600 in a directory it owns, with an explicit
  DOCKER_CONFIG pointing the docker CLI at it. The unit runs as monky-deployd, so a `docker login`
  by a human or by root is invisible to the agent: that is what made env-dev-01 look like it had no
  credential at all after the operator had just logged in.
- `compose pull` failures now carry the registry's own message ("no basic auth credentials",
  "manifest unknown", DNS) into the journal and the report instead of `rc=1`.
- Tests: both credential shapes, the refusals, 0600/0700 modes, idempotent rewrite, and that the
  runner never silently falls back to a human's $HOME.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
This commit is contained in:
2026-09-08 15:45:22 +00:00
parent 35949a9246
commit b25c6b3b8b
9 changed files with 205 additions and 9 deletions
+10
View File
@@ -175,6 +175,9 @@ class Config:
healthy_timeout_s: int = 300
compose_project: str = ""
docker_bin: str = "docker"
# doc 24 §4a: the registry the bundle pulls from, used when the seeded credential is a bare
# `username:password` (a JSON credential names its own registry).
registry_host: str = "harbor.tikali.net"
log_level: str = "INFO"
path: str = DEFAULT_CONFIG_PATH
@@ -191,6 +194,13 @@ class Config:
def lock_path(self) -> Path:
return Path(self.state_dir) / "lock"
@property
def docker_config_dir(self) -> Path:
"""Where the agent keeps its OWN registry credentials (`DOCKER_CONFIG`). Not `$HOME`: the
unit runs as `monky-deployd`, and a human's or root's `docker login` must not be what the
agent depends on (doc 24 §4a)."""
return Path(self.state_dir) / "docker"
@property
def is_prod(self) -> bool:
if self.prod is not None: