feat(docker): opt-in gh + az CLIs with git credential helpers so Clone Repo and Docker cases can reach private repos - #472
Conversation
…vate repos Add Case -> Clone Repo could only reach public repositories in the Docker deployment. This lets a deployment opt in to the GitHub CLI and the Azure CLI (+ azure-devops extension) as git credential helpers. Codeman itself still collects no credentials. - server.Dockerfile / agent.Dockerfile: CODEMAN_INSTALL_GH / CODEMAN_INSTALL_AZ build args (0 or 1, default 0; anything else stops the build). Off leaves no apt repository, package, extension, helper script or credential entry, so a default build is unchanged. On installs from the vendors' apt repositories and configures system gitconfig helpers: github.com / gist.github.com -> `gh auth git-credential`, dev.azure.com / *.visualstudio.com -> new docker/git-credential-azure-cli (an Entra ID token from `az account get-access-token`, or AZURE_DEVOPS_EXT_PAT). A helper whose CLI is not signed in prints nothing, so a private clone still fails fast. - The extension lives in AZURE_EXTENSION_DIR outside HOME (/opt/codeman-az-extensions, runtime-owned; /opt/az-extensions, gid-0 group-writable in the agent image). - Hosts turn them on in docker-compose.override.yml: `build: args:` for the server image, `environment:` CODEMAN_AGENT_IMAGE_INSTALL_GH / _AZ for the agent image. build-agent-image.mjs and the in-app auto-build share one env -> ARG table (pinned by the parity test) and pass nothing when unset. docker-compose.yaml is untouched; .env.example only gains a comment, so the self-updater's environment gate sees no new keys. - Docker cases seed the gh sign-in (~/.config/gh/hosts.yml, config.yml) and the az sign-in files from ~/.azure per file, read-only, like pi/grok. - The Clone Repo AUTH_REQUIRED message says how to sign the server's git in instead of claiming private repositories cannot be cloned. - Docs: docker/README.md "Private repositories", docker-compose.md, docker-cases.md, the Quick-Start / Core-Concepts / Docker-Cases wiki pages, security-architecture.md, architecture-invariants.md, changeset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167CiuzLrmjYWxwKp3rMWjw
63e7290 to
5cf5a45
Compare
|
Thanks a lot for this, @opticon454. It adds opt-in gh and az CLIs with git credential helpers to the Docker images so Clone Repo and Docker cases can reach private GitHub and Azure DevOps repos, and the Dockerfile work, the validated build args and the parity tests are really careful. Two things need to change before merge:
Smaller points:
No need to pin gh/az for now, your reasoning there is fine. Once the two points above are in, this is ready to merge. |
…for non-admin clones Addresses the review on Ark0N#472. - CRED_STORES: `.config/gh` and `.azure` now carry `enabledByEnv` (CODEMAN_AGENT_IMAGE_INSTALL_GH / _AZ), and resolveDockerCredentialArtifacts skips a store unless that variable is exactly `1`, read at container create. A host that merely has ~/.config/gh/hosts.yml or a plaintext MSAL cache no longer copies them into every case container. Tests: the default environment seeds neither even with the files present, and each store follows only its own switch. - Multi-user mode: a non-admin's Clone Repo clone and preflight run with `git -c credential.helper=` (GIT_NO_CREDENTIAL_HELPERS, placed before the subcommand), so the server account's helpers are never lent to them. Verified against a real private repo that it also clears the URL-scoped credential.<url>.helper entries, and that public clones still work. Tests: the argv in test/git-clone.test.ts, and the route decision (non-admin cleared; admin and single-user kept) in test/routes/case-clone-credential-helpers.test.ts. - Docs: recreate the case container to pick up seeds (docker/README.md, Docker-Cases wiki, docker-cases.md); the multi-user behaviour in docker/README.md and security-architecture.md; "functionally unchanged" instead of "unchanged" for an image built with both switches off (server.Dockerfile comment, README, changeset). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167CiuzLrmjYWxwKp3rMWjw
|
Thanks @Ark0N, both are fixed in 02e40f5 (a follow-up commit on top of the squashed one, so the delta is easy to review; happy to squash-merge). 1. gh/az seeding is now behind the opt-in.
The docs and changeset now say the seeds are switch-gated, and I dropped the "inert without the CLIs" wording, which was wrong for exactly the reason you gave. 2. Multi-user non-admins no longer inherit the server's git sign-in. A non-admin's clone and preflight run with
Documented in Smaller points
Verification on 02e40f5: full gate ( |
#472) Clone Repo clears the credential helpers for a non-admin, but a non-admin's Docker case with credential seeding on still receives a copy of the server account's gh/az sign-in when the agent-image switches are on, the same as the Claude and Codex credentials. Say so in the multi-user notes so the docs do not read as a stronger guarantee than they are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merged, thanks @opticon454! This ships in 1.32.1. Both points from my review came back fixed properly: gh/az seeding only happens behind the agent-image switches, with tests, and a non-admin's clone and preflight run with the credential helpers cleared, pinned in both the argv and the route tests. Having everything opt-in with "switch off leaves nothing behind" as the rule made this easy to take. One addition at merge: the multi-user notes in |
Summary
Add Case → Clone Repo only works for public repositories in the Docker deployment: the container has no way to authenticate to GitHub or Azure DevOps, so any private URL fails with
AUTH_REQUIREDand the message sends the user off to clone it by hand and use Link Existing.src/git-clone.tsalready inheritsHOMEand git's own config on purpose ("a user whose own credential helper already works keeps working"). This PR lets a Docker deployment opt in to credential helpers for that path to inherit. Codeman itself still collects nothing.Opt-in, off by default.
docker/server.Dockerfileanddocker/agent.DockerfiletakeCODEMAN_INSTALL_GH/CODEMAN_INSTALL_AZbuild args (0/1, default0; anything else stops the build). With neither set, the rebuilt images are functionally unchanged: no repository, package, extension or credential entry. What remains is theAZURE_EXTENSION_DIRvariable, an empty directory and one COPY+rm layer. A host turns them on indocker-compose.override.yml:docker-compose.yamlis untouched, and.env.examplegains only a comment pointing at the override file. A new.env.examplekey would make the updater's environment gate refuse every existing install until its.envgained it.What an enabled switch installs. The GitHub CLI and/or the Azure CLI with the
azure-devopsextension, from their vendors' apt repositories: the same repos the documented one-liners configure.deb_install.shisn't piped into the build, and apt reads the.asckey directly, sognupgstays out. Plus system gitconfig credential helpers, per host:https://github.com,https://gist.github.com→!/usr/bin/gh auth git-credential(whatgh auth setup-gitwrites).https://dev.azure.com,https://*.visualstudio.com→ newdocker/git-credential-azure-cli, which returns an Entra ID token fromaz account get-access-token --resource 499b84ac-…(the Azure DevOps resource id), orAZURE_DEVOPS_EXT_PATwhen set.useHttpPath=truefor both.Extension location. It goes into
AZURE_EXTENSION_DIR, outsideHOME.HOMEis the app-data bind mount on the server image and a seeded directory on the agent image, and either would hide or drop a build-time install. On the server it's/opt/codeman-az-extensions, chowned to the runtime account next to/opt/codeman-cli; on the agent it's/opt/az-extensions, gid 0 and group-writable. Nothing that runs as root executes from either.Agent image plumbing.
scripts/build-agent-image.mjsand the in-app auto-build both pass the switches fromCODEMAN_AGENT_IMAGE_INSTALL_GH/_AZ, and pass nothing when unset, so a default build's argv is unchanged. They share one env → ARG table, whichtest/agent-image-build-args-parity.test.tspins for every combination. A malformed value is refused on both sides; the auto-build reports it as a failed build.Docker-case seeding, behind the same switches.
CRED_STORESinsrc/docker-hosts.tsgains two per-file seeds, read-only and copied once at launch like pi/grok. Each is gated byenabledByEnvand seeded only when its switch is exactly1at container create (.config/ghneedsCODEMAN_AGENT_IMAGE_INSTALL_GH,.azureneedsCODEMAN_AGENT_IMAGE_INSTALL_AZ), never merely because the files exist:.config/gh→hosts.yml,config.yml.azure→azureProfile.json,msal_token_cache.json,service_principal_entries.json,clouds.config,config~/.azure's logs, command index and extensions are never seeded. Seeds are create-time mounts, so an existing case container must be recreated to pick them up. With a switch on, they hand a GitHub token and an Azure sign-in to seeded case containers, the same trust already extended to Claude/Codex/gcloud credentials; a sealed case (seeding off) gets none. A token in a desktop keyring, or in az's encrypted MSAL cache on Windows/macOS, isn't in those files and doesn't carry. That's documented.Signing in. The user signs in once from a Terminal / Shell session (
gh auth login,az login --use-device-code). It lands in~/.config/ghand~/.azureon the bind mount, so it survives rebuilds.Multi-user mode: no shared git sign-in for non-admins. Every user's git runs as the one server account, so a non-admin's Clone Repo clone and preflight run with
git -c credential.helper=(GIT_NO_CREDENTIAL_HELPERS, before the subcommand; decided bycloneWithoutCredentialHelpers(req)). That empties the helper list including the URL-scoped entries (verified against a real private repo), while public repos still clone. Admins and single-user mode keep the helpers. This closes the Clone Repo path only: the account's SSH keys and a non-admin's own agent sessions are unchanged, as documented insecurity-architecture.md.Fail-fast is unchanged. A helper whose CLI isn't signed in prints nothing, so git falls through to its normal
terminal prompts disabledfailure (measured: about 0.5 s).Error message.
AUTH_REQUIRED's message now says how to sign the server's git in, instead of saying private repositories can't be cloned.classifyGitFailureand its codes are unchanged.Docs:
docker/README.md: "Private repositories" with "Turning them on", "Signing in" and "Versions", includinggh skill install cli/cli gh --scope user.docs/docker-compose.mdanddocs/docker-cases.md.security-architecture.md, andarchitecture-invariants.md.docker-cases.mdand the Docker-Cases wiki.gh, az and the extension are not version-pinned, unlike the four agent CLIs: nothing in Codeman depends on a particular gh or az behaviour. Each build takes the current release, and the layer cache keeps it until a
--no-cacherebuild; the README says so. Happy to pin them if you'd rather.This changes
server.Dockerfile, so the updater's environment gate will (correctly) ask Compose users to runStart-Codeman.shinstead of applying it in place; with no switches set, the rebuilt image is functionally unchanged. The changeset is markedminor; adjust as you see fit.Commits
Verification
Built both Dockerfiles locally (Docker 29.5.3) and checked them as the unprivileged runtime account (server uid 1000; agent
--user 12345:0).Default (no switches). Server and agent images contain no
gh, noaz, no helper script, no extra apt source and nocredential.*entry.build-agent-image.mjspasses noCODEMAN_INSTALL_*args.Both on (
=1).gh version 2.101.0,azure-cli 2.90.0,azure-devops 1.0.8, and the six credential entries.az extension update --name azure-devopsworks as the runtime user (the directory is writable).Mixed and invalid.
az.CODEMAN_INSTALL_AZ=yes: the build stops withCODEMAN_INSTALL_AZ must be 0 or 1.build-agent-image.mjs: withCODEMAN_AGENT_IMAGE_INSTALL_*=0the script passed both args, and the image has neither CLI.Authentication.
git ls-remoteagainst a private github.com URL and a dev.azure.com URL both fail in about 0.5 s withterminal prompts disabled.git-credential-azure-cli getprints nothing and exits 0.GH_TOKEN):git ls-remoteof a real private repository returns its refs throughgh auth git-credential.hosts.ymlstaged at the seed path and copied by the launch's own[ -e to ] || cp from toline lets an agent-image user list a private repo.AZURE_DEVOPS_EXT_PATpath makes the helper emitusername=pat/password=<pat>.az loginwas NOT verified end to end. The account I had available needs an interactive MFA re-login. The token-as-basic-auth-password scheme is the one Git Credential Manager uses for Azure Repos, but it hasn't been exercised against dev.azure.com here.Tests. New unit tests:
test/docker-hosts.test.ts: the gh/az seeds are per file, read-only, excludelogs//cliextensions/, are NOT seeded in the default environment even when the host files exist, and each follows only its own switch.test/git-clone.test.ts: the-c credential.helper=argv, before the subcommand, for both clone and ls-remote.test/routes/case-clone-credential-helpers.test.ts: non-admin in multi-user mode cleared; admin and single-user kept.The switch parity, validation and
ARG ...=0defaults are covered intest/agent-image-build-args-parity.test.ts. Full gate on02e40f50, full gate (npm test, the CI config) in a Linux container: 415 files / 7840 tests passed, 12 skipped, 0 failed;tsc --noEmitand lint clean.Image size (server image). Neither 3.67 GB (none of this layer content; I did not build an upstream-master image to compare against), gh only 3.71 GB, both 4.34 GB. The Azure CLI is about 630 MB of the roughly 670 MB the pair adds.
🤖 Generated with Claude Code
https://claude.ai/code/session_0167CiuzLrmjYWxwKp3rMWjw