Skip to content

container: give agent runs docker, gh auth and a git identity - #104

Merged
max-tet merged 1 commit into
mainfrom
feat/container-tooling
Sep 21, 2026
Merged

max-tet merged 1 commit into
mainfrom
feat/container-tooling

Conversation

@ClaydeCode

Copy link
Copy Markdown
Owner

Why

A headless run started by the scheduler or the Pebble worker could not commit, could not push, and could not talk to Docker. Verified in the running container on the VM today:

  • gh auth statusnot logged into any GitHub hosts. GH_TOKEN is exported only inside run_cycle() of the Freeshard loop, and that loop is off on the VM (CLAYDE_FS_ENABLED=false), so nothing ever set it. The credential helper is !gh auth git-credential, so pushes fail too.
  • git commit in the container → Author identity unknown. No user.name/user.email is configured anywhere, although CLAYDE_GIT_NAME and CLAYDE_GIT_EMAIL exist in config.
  • No docker binary in the image.

CLAUDE.md already documented the first two as if they were true (lines 5 and 148). This makes them true.

The motivating case is running the app-repository update-apps pass as a weekly scheduled task: update/update.py apply gates every app on docker compose pull --dry-run, then commits, pushes and opens a PR.

What changed

  • config.bootstrap_process_env() exports GH_TOKEN and sets the global git identity from settings. Called from both entry points (orchestrator._run_with_pebble, freeshard.entry.run_loop) instead of happening as a side effect of one loop; the in-cycle export and its test move here.
  • Dockerfile installs docker-ce-cli + docker-compose-plugin from Docker's Debian repo, codename taken from /etc/os-release so a base-image bump does not silently break it.
  • docker-compose.yml mounts /var/run/docker.sock and adds the host's docker group via group_add: ["${DOCKER_GID:-111}"], needed because the container runs as uid 1000.

The socket is the full one. An agent in this container can control every container on the host. That is deliberate and was weighed against running the update pass outside the container; a read-only socket proxy would also work if we later want it.

Verified before opening this PR

Built the Docker-install layer on python:3.13-slim and ran the resulting client against the host daemon as uid 1000:

Check Result
docker --version / docker compose version in the built image 29.8.1 / v5.5.1
docker version as -u 1000:1000 --group-add 111 reaches the daemon (29.2.1)
same, without --group-add permission denied ... unix:///var/run/docker.sock
docker compose pull --dry-run -q on a rendered app template (audiobookshelf:2.36.0) exit 0
same with a bogus tag exit 1, manifest unknown

The last two are the gate update.py depends on, and they discriminate.

uv run pytest: 418 passed.

Deploy note

~/clayde/docker-compose.yml on the VM is maintained by hand and needs the same two edits, plus DOCKER_GID=111 in ~/clayde/.env (this host's getent group docker). The container must be recreated, not just restarted, for the mount to apply.

Recommended reading order

  1. src/clayde/config.py — the new function
  2. src/clayde/orchestrator.py, src/clayde/freeshard/entry.py — call sites
  3. src/clayde/freeshard/loop.py — the removed duplicate
  4. Dockerfile, docker-compose.yml
  5. tests/test_config.py, tests/freeshard/test_loop.py

A headless run started by the scheduler or the Pebble worker had none of
the three. GH_TOKEN was exported only inside the Freeshard cycle, which
is disabled on the VM (CLAYDE_FS_ENABLED=false), so `gh` was
unauthenticated and the `!gh auth git-credential` helper could not push.
No git identity was configured anywhere, so any commit failed with
"Author identity unknown". And the image carried no docker client, which
the app-repository update pass needs for `docker compose pull --dry-run`.

bootstrap_process_env() now does the first two at both entry points
instead of as a side effect of one loop, and the image gets the Docker
CLI plus Compose plugin talking to the host socket through the host's
docker group. CLAUDE.md already described the first two as if they were
already true.

The socket is the full one: an agent in this container can control every
container on the host. Deliberate, weighed against running the update
pass outside the container entirely.
@max-tet
max-tet merged commit b668958 into main Sep 21, 2026
3 checks passed
@max-tet
max-tet deleted the feat/container-tooling branch September 21, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants