From bf73a8473219c13f1755813bb76546d89a0d3399 Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:27:26 +0800 Subject: [PATCH] fix(docker): address git identity review --- .changeset/static-git-identity.md | 5 ++++ docker/.env.example | 8 +++--- docker/README.md | 4 ++- docker/agent.Dockerfile | 29 +++++++++++----------- docker/docker-compose.yaml | 4 +-- docker/server.Dockerfile | 29 +++++++++++----------- docs/docker-cases.md | 2 ++ scripts/lib/cli-catalog.mjs | 6 ++--- src/docker-hosts.ts | 8 +++--- test/agent-image-build-args-parity.test.ts | 18 +++++++++----- 10 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 .changeset/static-git-identity.md diff --git a/.changeset/static-git-identity.md b/.changeset/static-git-identity.md new file mode 100644 index 00000000..345e124a --- /dev/null +++ b/.changeset/static-git-identity.md @@ -0,0 +1,5 @@ +--- +"aicodeman": minor +--- + +Docker deployments can configure a static Git commit identity for server and Docker-case agent images. diff --git a/docker/.env.example b/docker/.env.example index 8e929697..fbdb1229 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -15,11 +15,11 @@ TZ=Australia/Perth # this value rebuilds the image with a matching account. CODEMAN_RUNTIME_USER=codeman -# Required for Git commits made by Codeman and Docker-case agents. These values +# Optional Git identity for commits made by Codeman and Docker-case agents. These values # are written to each image's system Git configuration when it is rebuilt, so -# they remain available even when the runtime home directory is a fresh mount. -GIT_USER_NAME= -GIT_USER_EMAIL= +# deployments can configure a consistent default. Set both values together. +# GIT_USER_NAME= +# GIT_USER_EMAIL= # Required. Persistent Codeman application data, CLI credentials, and session # state are stored here on the host and mounted at the runtime account's home diff --git a/docker/README.md b/docker/README.md index 0358add4..5328fb65 100644 --- a/docker/README.md +++ b/docker/README.md @@ -80,7 +80,9 @@ Compose passes the values to the Codeman server build, and to the server process when it builds Docker-case agent images. Both images write the pair to Git's system configuration during their build, so commits retain the same identity after a container or agent image is recreated. Set both values together; an -image build with only one value fails rather than using a partial identity. +image build with only one value fails rather than using a partial identity. An +identity already present in `CODEMAN_APPDATA_PATH`'s `~/.gitconfig` overrides +the server image's system-level default. Run `bash docker/Start-Codeman.sh` after changing the server values. Rebuild an existing agent image with `node scripts/build-agent-image.mjs --no-cache` in the diff --git a/docker/agent.Dockerfile b/docker/agent.Dockerfile index 93cb1c3b..afc9c6e1 100644 --- a/docker/agent.Dockerfile +++ b/docker/agent.Dockerfile @@ -12,9 +12,6 @@ # writable even though the uid is not the baked 1000. FROM node:22-bookworm-slim -ARG GIT_USER_EMAIL= -ARG GIT_USER_NAME= - # Base toolchain. `curl` is needed for the hook callbacks (`curl -sk $CODEMAN_API_URL`), # `procps` for `ps`, `tmux` for the durable in-container session. RUN apt-get update \ @@ -30,17 +27,6 @@ RUN apt-get update \ openssh-client \ && rm -rf /var/lib/apt/lists/* -# Docker cases run with a fresh, container-owned home directory. Configure Git -# at the system level during the build so the identity supplied in docker/.env -# remains stable after an agent image rebuild. Refuse an incomplete identity. -RUN set -eux; \ - if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \ - test -n "${GIT_USER_NAME}"; \ - test -n "${GIT_USER_EMAIL}"; \ - git config --system user.name "${GIT_USER_NAME}"; \ - git config --system user.email "${GIT_USER_EMAIL}"; \ - fi - # GitHub CLI and Azure CLI (+ the azure-devops extension) with the same system # git credential helpers as docker/server.Dockerfile, so an agent in a Docker # case can clone and push to private GitHub / Azure DevOps repositories. The @@ -268,6 +254,21 @@ RUN useradd -g 0 -m -d /home/agent -s /bin/bash agent \ && chgrp -R 0 /home/agent \ && chmod -R g=u /home/agent +# Docker cases have a fresh, container-owned home directory. Declare the +# optional identity here so changing it invalidates only this final layer, then +# configure Git's system defaults. A user-level config still takes precedence. +ARG GIT_USER_EMAIL= +ARG GIT_USER_NAME= +RUN set -eux; \ + if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \ + if [ -z "${GIT_USER_NAME}" ] || [ -z "${GIT_USER_EMAIL}" ]; then \ + echo 'Git user name and email must both be set when configuring Git identity' >&2; \ + exit 1; \ + fi; \ + git config --system user.name "${GIT_USER_NAME}"; \ + git config --system user.email "${GIT_USER_EMAIL}"; \ + fi + USER agent WORKDIR /home/agent diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 9b50e8f1..db6e17c3 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -36,8 +36,8 @@ services: CODEMAN_CASES_PATH: ${CODEMAN_CASES_PATH} # Passed through only so Codeman can use the same identity when it builds # the Docker-case agent image. - GIT_USER_EMAIL: ${GIT_USER_EMAIL:-} - GIT_USER_NAME: ${GIT_USER_NAME:-} + CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: ${GIT_USER_EMAIL:-} + CODEMAN_AGENT_IMAGE_GIT_USER_NAME: ${GIT_USER_NAME:-} # Extra Host-header allowlist entries for a reverse-proxied deployment # (docker/README.md, "Reverse-proxy host allowlist"). Optional, so it # defaults to empty rather than requiring a line in every .env. diff --git a/docker/server.Dockerfile b/docker/server.Dockerfile index 10f58ec0..fd2cdf65 100644 --- a/docker/server.Dockerfile +++ b/docker/server.Dockerfile @@ -25,8 +25,6 @@ RUN npm ci \ FROM node:22-bookworm-slim ARG CODEMAN_RUNTIME_USER=codeman -ARG GIT_USER_EMAIL= -ARG GIT_USER_NAME= ARG PUID=1000 ARG PGID=1000 @@ -50,18 +48,6 @@ RUN apt-get update \ tmux \ && rm -rf /var/lib/apt/lists/* -# A runtime home is normally a bind mount, so user-level Git configuration is -# not durable across a fresh deployment. Keep the operator-supplied identity in -# the image's system config instead. Both values are required together to avoid -# producing commits with a misleading partial identity. -RUN set -eux; \ - if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \ - test -n "${GIT_USER_NAME}"; \ - test -n "${GIT_USER_EMAIL}"; \ - git config --system user.name "${GIT_USER_NAME}"; \ - git config --system user.email "${GIT_USER_EMAIL}"; \ - fi - # The Docker CLI, taken from the official image rather than Debian's `docker.io`. # That package is the full ENGINE: with --no-install-recommends it still pulls 15 # packages including containerd, runc, dmsetup and iptables, none of which a @@ -313,6 +299,21 @@ EXPOSE 3000 COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod 0755 /usr/local/bin/entrypoint.sh +# Declare the optional identity immediately before configuring it so a change +# invalidates only this final layer. This is declarative setup: a persisted +# ~/.gitconfig in CODEMAN_APPDATA_PATH still overrides the system-level values. +ARG GIT_USER_EMAIL= +ARG GIT_USER_NAME= +RUN set -eux; \ + if [ -n "${GIT_USER_NAME}" ] || [ -n "${GIT_USER_EMAIL}" ]; then \ + if [ -z "${GIT_USER_NAME}" ] || [ -z "${GIT_USER_EMAIL}" ]; then \ + echo 'Git user name and email must both be set when configuring Git identity' >&2; \ + exit 1; \ + fi; \ + git config --system user.name "${GIT_USER_NAME}"; \ + git config --system user.email "${GIT_USER_EMAIL}"; \ + fi + ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] CMD ["node", "dist/index.js", "web"] diff --git a/docs/docker-cases.md b/docs/docker-cases.md index f8a1459a..4b8bd7bc 100644 --- a/docs/docker-cases.md +++ b/docs/docker-cases.md @@ -83,6 +83,8 @@ Pi's credentials are seeded per-FILE rather than as a whole directory (`auth.jso The image can also carry the GitHub CLI (`gh`) and the Azure CLI (`az` + the `azure-devops` extension, in `AZURE_EXTENSION_DIR=/opt/az-extensions` so it stays out of the seeded HOME), wired into the system git config as credential helpers for github.com and dev.azure.com / *.visualstudio.com, exactly as in `docker/server.Dockerfile`. Their sign-ins are seeded per-FILE like pi's: `~/.config/gh/{hosts.yml,config.yml}` and `~/.azure/{azureProfile.json,msal_token_cache.json,service_principal_entries.json,clouds.config,config}`, never `~/.azure`'s logs, command index or extensions. A token kept in a desktop keyring, or in the encrypted MSAL cache az uses on Windows/macOS, is not in those files and does not carry. None of the three is version-pinned; the `--no-cache` rebuild recommended above is also what refreshes them. Both CLIs are opt-in and OFF by default: `CODEMAN_AGENT_IMAGE_INSTALL_GH=1` / `CODEMAN_AGENT_IMAGE_INSTALL_AZ=1` in the environment of `scripts/build-agent-image.mjs`, or of the Codeman server for its own auto-build (in the Compose deployment, `environment:` in `docker-compose.override.yml`), become the `CODEMAN_INSTALL_GH` / `CODEMAN_INSTALL_AZ` build args and put that CLI, its extension and its helper entry into the image. Unset passes nothing, so a default build's argv is unchanged and the image has neither. The sign-in seeds follow the same switches, read when a case container is created: `.config/gh` only with `CODEMAN_AGENT_IMAGE_INSTALL_GH=1`, `.azure` only with `CODEMAN_AGENT_IMAGE_INSTALL_AZ=1` (`enabledByEnv` in `CRED_STORES`), never merely because the files exist. Seeds are create-time mounts and deliberately not part of the config hash (hashing them would trip the drift gate for every case), so an existing case container picks them up only when it is recreated. +Set `CODEMAN_AGENT_IMAGE_GIT_USER_NAME` and `CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL` together to configure the agent image's Git identity. Rebuild an existing `codeman/agent:base` with `node scripts/build-agent-image.mjs --no-cache`, then recreate Docker-case containers so they use the rebuilt image. + ## Quickest path: one-click "Run in Docker" On the **New case → Create New** tab there's a **🐳 Run in an isolated Docker container** checkbox. Checking it alone is enough: Codeman creates the case folder in `~/codeman-cases/`, spins up a hardened container with sensible defaults (auto-provisioning a shared `default` host), and starts the session inside it. No host/image/network fields to fill in. diff --git a/scripts/lib/cli-catalog.mjs b/scripts/lib/cli-catalog.mjs index 23edb136..fd9c53f6 100644 --- a/scripts/lib/cli-catalog.mjs +++ b/scripts/lib/cli-catalog.mjs @@ -66,8 +66,8 @@ export const GIT_HOST_CLI_BUILD_ARGS = [ /** Environment variables passed through to the agent image's system Git configuration. */ export const GIT_IDENTITY_BUILD_ARGS = [ - ['GIT_USER_NAME', 'GIT_USER_NAME'], - ['GIT_USER_EMAIL', 'GIT_USER_EMAIL'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'], ]; /** @@ -94,7 +94,7 @@ export function gitIdentityBuildArgPairs(env) { const configured = pairs.filter(([, value]) => value !== ''); if (configured.length === 0) return []; if (configured.length !== pairs.length) { - throw new Error('GIT_USER_NAME and GIT_USER_EMAIL must both be set when configuring Git identity'); + throw new Error('Git user name and email must both be set when configuring Git identity'); } return pairs; } diff --git a/src/docker-hosts.ts b/src/docker-hosts.ts index 269dd540..3b6f7744 100644 --- a/src/docker-hosts.ts +++ b/src/docker-hosts.ts @@ -617,8 +617,8 @@ export const GIT_HOST_CLI_BUILD_ARGS: ReadonlyArray = /** Environment variables passed through to the agent image's system Git configuration. */ export const GIT_IDENTITY_BUILD_ARGS: ReadonlyArray = [ - ['GIT_USER_NAME', 'GIT_USER_NAME'], - ['GIT_USER_EMAIL', 'GIT_USER_EMAIL'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'], ]; /** @@ -648,7 +648,7 @@ export function gitIdentityBuildArgPairs(env: NodeJS.ProcessEnv): Array<[string, const configured = pairs.filter(([, value]) => value !== ''); if (configured.length === 0) return []; if (configured.length !== pairs.length) { - throw new Error('GIT_USER_NAME and GIT_USER_EMAIL must both be set when configuring Git identity'); + throw new Error('Git user name and email must both be set when configuring Git identity'); } return pairs; } @@ -1228,7 +1228,7 @@ function buildAgentImage( try { buildArgPairs = agentImageBuildArgPairs(); } catch (err) { - // A malformed CODEMAN_AGENT_IMAGE_INSTALL_* value: report it like any other build failure. + // A malformed CODEMAN_AGENT_IMAGE_* value: report it like any other build failure. return Promise.resolve({ ok: false, built: false, alreadyPresent: false, error: String((err as Error).message) }); } const argv = dockerEngineArgv(docker); diff --git a/test/agent-image-build-args-parity.test.ts b/test/agent-image-build-args-parity.test.ts index 1f29a4a9..3ca358d0 100644 --- a/test/agent-image-build-args-parity.test.ts +++ b/test/agent-image-build-args-parity.test.ts @@ -154,13 +154,16 @@ describe('Git identity in the agent image: both producers pass the same settings it('maps the Git environment variables to matching Dockerfile ARGs', () => { expect(tsGitIdentityArgs).toEqual(mjsGitIdentityArgs); expect(tsGitIdentityArgs).toEqual([ - ['GIT_USER_NAME', 'GIT_USER_NAME'], - ['GIT_USER_EMAIL', 'GIT_USER_EMAIL'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_NAME', 'GIT_USER_NAME'], + ['CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL', 'GIT_USER_EMAIL'], ]); }); it('passes a complete identity and omits an absent identity', () => { - const identity = { GIT_USER_NAME: 'Ada Lovelace', GIT_USER_EMAIL: 'ada@example.com' }; + const identity = { + CODEMAN_AGENT_IMAGE_GIT_USER_NAME: 'Ada Lovelace', + CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: 'ada@example.com', + }; const expected: Array<[string, string]> = [ ['GIT_USER_NAME', 'Ada Lovelace'], ['GIT_USER_EMAIL', 'ada@example.com'], @@ -172,9 +175,12 @@ describe('Git identity in the agent image: both producers pass the same settings }); it('refuses a partial identity in both build paths', () => { - for (const identity of [{ GIT_USER_NAME: 'Ada Lovelace' }, { GIT_USER_EMAIL: 'ada@example.com' }]) { - expect(() => tsGitIdentityPairs(identity)).toThrow(/GIT_USER_NAME and GIT_USER_EMAIL/); - expect(() => mjsGitIdentityPairs(identity)).toThrow(/GIT_USER_NAME and GIT_USER_EMAIL/); + for (const identity of [ + { CODEMAN_AGENT_IMAGE_GIT_USER_NAME: 'Ada Lovelace' }, + { CODEMAN_AGENT_IMAGE_GIT_USER_EMAIL: 'ada@example.com' }, + ]) { + expect(() => tsGitIdentityPairs(identity)).toThrow(/Git user name and email/); + expect(() => mjsGitIdentityPairs(identity)).toThrow(/Git user name and email/); } });