From 57785a96c2fe4f57e0697dea0d9cd8f91f0132b7 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 17:06:30 +0000 Subject: [PATCH 1/9] feat(agent-images): update Debian and add Podman --- bot_bottle/contrib/claude/Dockerfile | 11 ++++- bot_bottle/contrib/codex/Dockerfile | 10 ++++- bot_bottle/contrib/pi/Dockerfile | 3 +- ...prd-new-modernize-built-in-agent-images.md | 43 +++++++++++++++++++ tests/unit/test_builtin_agent_images.py | 34 +++++++++++++++ 5 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 docs/prds/prd-new-modernize-built-in-agent-images.md create mode 100644 tests/unit/test_builtin_agent_images.py diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index 4834dd3..4c30481 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -10,7 +10,7 @@ # Current Node LTS; slim variant keeps the image small while still # providing apt-get for any future additions. -FROM node:22-slim +FROM node:22-trixie-slim # Install runtime system deps. claude-code shells out to git for several # features (status checks, commits, PR creation) — without git in the @@ -21,7 +21,14 @@ FROM node:22-slim # to it) works against egress's bumped TLS without the agent needing # local DNS. RUN apt-get update \ - && apt-get install -y --no-install-recommends git ca-certificates curl ripgrep iproute2 dnsutils \ + && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + podman \ + ripgrep \ + iproute2 \ + dnsutils \ && rm -rf /var/lib/apt/lists/* # App-specific deps. Python isn't required by claude-code itself diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index c966b8d..36d9507 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -3,10 +3,16 @@ # Mirrors the default Claude image shape: Node LTS, git/network tooling, # non-root node user, and the provider CLI installed for that user. -FROM node:22-slim +FROM node:22-trixie-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends git ca-certificates curl procps ripgrep \ + && apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + podman \ + procps \ + ripgrep \ && rm -rf /var/lib/apt/lists/* # App-specific deps. Python isn't required by codex itself diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index 20a5f29..9036c92 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -2,7 +2,7 @@ # # Node LTS, git/network tooling, and the Pi coding-agent CLI installed globally. -FROM node:22-slim +FROM node:22-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -10,6 +10,7 @@ RUN apt-get update \ ca-certificates \ curl \ fd-find \ + podman \ ripgrep \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ && rm -rf /var/lib/apt/lists/* diff --git a/docs/prds/prd-new-modernize-built-in-agent-images.md b/docs/prds/prd-new-modernize-built-in-agent-images.md new file mode 100644 index 0000000..4a5d3fd --- /dev/null +++ b/docs/prds/prd-new-modernize-built-in-agent-images.md @@ -0,0 +1,43 @@ +# PRD prd-new: Modernize built-in agent images + +- **Status:** Draft +- **Author:** Codex +- **Created:** 2026-07-21 +- **Issue:** #451 + +## Summary + +Keep every built-in agent provider on Debian's current stable release and make +Podman available inside each image. This gives agents a consistent, modern +userspace and an OCI container tool without requiring per-project setup. + +## Problem + +The Claude, Codex, and Pi images inherit the generic `node:22-slim` tag. That +tag does not state which Debian release the project supports and currently +leaves the images on the older Bookworm release. None of the built-in images +installs Podman, so tasks that need to inspect or build OCI images must first +modify the bottle or cannot run at all. + +## Goals / success criteria + +- Every Dockerfile under `bot_bottle/contrib/*/Dockerfile` explicitly inherits + `node:22-trixie-slim`, based on Debian 13 (the current stable release). +- Every built-in agent image installs Podman from Debian stable. +- A shared test enforces both requirements for current and future built-in + providers. + +## Non-goals + +- Configuring privileged or nested-container execution for bottles. +- Pinning Podman outside Debian's stable package repository. +- Changing the Node.js or agent CLI release policy. + +## Design + +Use the explicit `node:22-trixie-slim` base rather than the floating `slim` +variant. Install the `podman` package with each image's existing `apt-get` +dependency layer, so package metadata and caches are still removed in the same +layer. Treat Debian stable as the Podman stability and update channel; this +keeps the images stdlib/distribution-first and avoids adding a third-party +package repository. diff --git a/tests/unit/test_builtin_agent_images.py b/tests/unit/test_builtin_agent_images.py new file mode 100644 index 0000000..45c5024 --- /dev/null +++ b/tests/unit/test_builtin_agent_images.py @@ -0,0 +1,34 @@ +"""Unit contracts shared by all built-in agent images.""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + + +_CONTRIB_DIR = Path(__file__).resolve().parents[2] / "bot_bottle/contrib" +_AGENT_DOCKERFILES = tuple(sorted(_CONTRIB_DIR.glob("*/Dockerfile"))) + + +class TestBuiltinAgentImages(unittest.TestCase): + def test_all_use_debian_trixie_stable(self): + self.assertTrue(_AGENT_DOCKERFILES) + for dockerfile in _AGENT_DOCKERFILES: + with self.subTest(provider=dockerfile.parent.name): + self.assertRegex( + dockerfile.read_text(), + r"(?m)^FROM node:22-trixie-slim\s*$", + ) + + def test_all_install_podman(self): + for dockerfile in _AGENT_DOCKERFILES: + with self.subTest(provider=dockerfile.parent.name): + self.assertRegex( + dockerfile.read_text(), + re.compile(r"(?m)^\s*podman(?:\s|\\|$)"), + ) + + +if __name__ == "__main__": + unittest.main() -- 2.52.0 From 1aeeb55b540678837a0569ce1e46828e3fb96e52 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 17:25:11 +0000 Subject: [PATCH 2/9] fix(agent-images): retain SSH client --- bot_bottle/contrib/claude/Dockerfile | 1 + bot_bottle/contrib/codex/Dockerfile | 1 + bot_bottle/contrib/pi/Dockerfile | 1 + docs/prds/prd-new-modernize-built-in-agent-images.md | 1 + tests/integration/test_sandbox_escape.py | 2 +- tests/unit/test_builtin_agent_images.py | 8 ++++++++ 6 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index 4c30481..4abc514 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -25,6 +25,7 @@ RUN apt-get update \ git \ ca-certificates \ curl \ + openssh-client \ podman \ ripgrep \ iproute2 \ diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index 36d9507..16414c2 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -10,6 +10,7 @@ RUN apt-get update \ git \ ca-certificates \ curl \ + openssh-client \ podman \ procps \ ripgrep \ diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index 9036c92..aa24671 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -10,6 +10,7 @@ RUN apt-get update \ ca-certificates \ curl \ fd-find \ + openssh-client \ podman \ ripgrep \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ diff --git a/docs/prds/prd-new-modernize-built-in-agent-images.md b/docs/prds/prd-new-modernize-built-in-agent-images.md index 4a5d3fd..788b552 100644 --- a/docs/prds/prd-new-modernize-built-in-agent-images.md +++ b/docs/prds/prd-new-modernize-built-in-agent-images.md @@ -24,6 +24,7 @@ modify the bottle or cannot run at all. - Every Dockerfile under `bot_bottle/contrib/*/Dockerfile` explicitly inherits `node:22-trixie-slim`, based on Debian 13 (the current stable release). - Every built-in agent image installs Podman from Debian stable. +- Every built-in agent image retains an SSH client for Git-over-SSH workflows. - A shared test enforces both requirements for current and future built-in providers. diff --git a/tests/integration/test_sandbox_escape.py b/tests/integration/test_sandbox_escape.py index 29b7608..de52e5f 100644 --- a/tests/integration/test_sandbox_escape.py +++ b/tests/integration/test_sandbox_escape.py @@ -172,7 +172,7 @@ class TestSandboxEscape(unittest.TestCase): # base image without producing five confusing # command-not-found failures down the suite. missing: list[str] = [] - for tool in ("curl", "git", "dig"): + for tool in ("curl", "git", "dig", "ssh"): r = cls._bottle.exec(f"command -v {tool} >/dev/null 2>&1") if r.returncode != 0: missing.append(tool) diff --git a/tests/unit/test_builtin_agent_images.py b/tests/unit/test_builtin_agent_images.py index 45c5024..df9ab68 100644 --- a/tests/unit/test_builtin_agent_images.py +++ b/tests/unit/test_builtin_agent_images.py @@ -29,6 +29,14 @@ class TestBuiltinAgentImages(unittest.TestCase): re.compile(r"(?m)^\s*podman(?:\s|\\|$)"), ) + def test_all_install_ssh_client(self): + for dockerfile in _AGENT_DOCKERFILES: + with self.subTest(provider=dockerfile.parent.name): + self.assertRegex( + dockerfile.read_text(), + re.compile(r"(?m)^\s*openssh-client(?:\s|\\|$)"), + ) + if __name__ == "__main__": unittest.main() -- 2.52.0 From 7e5ffd96749f29bc1fa591b81673d84a2cb1c69c Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 17:38:04 +0000 Subject: [PATCH 3/9] fix(agent-images): own Git config directory --- bot_bottle/contrib/claude/Dockerfile | 5 +++++ bot_bottle/contrib/codex/Dockerfile | 2 ++ bot_bottle/contrib/pi/Dockerfile | 3 ++- docs/prds/prd-new-modernize-built-in-agent-images.md | 3 +++ tests/unit/test_builtin_agent_images.py | 7 +++++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index 4abc514..fd8520d 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -47,6 +47,11 @@ RUN apt-get update \ RUN npm install -g --no-fund --no-audit @anthropic-ai/claude-code@2.1.172 \ && npm cache clean --force +# Git reads both ~/.gitconfig and ~/.config/git/config. Keep its XDG config +# path traversable by the non-root runtime user so permission errors do not +# suppress bot-bottle's git-gate insteadOf rules. +RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git + # Run as a non-root user. The node image already provides a `node` user # (uid 1000) with a home directory, which is where claude-code will write # its session state. diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index 16414c2..56216c2 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -24,6 +24,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* +RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git + USER node WORKDIR /home/node diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index aa24671..02564fd 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -23,7 +23,8 @@ RUN apt-get update \ RUN npm install -g --ignore-scripts --no-fund --no-audit @earendil-works/pi-coding-agent \ && npm cache clean --force -RUN mkdir -p /home/node/.pi/agent \ +RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git \ + && mkdir -p /home/node/.pi/agent \ /home/node/.pi/context-mode/sessions \ /tmp/pi-subagents-uid-1000 \ && chown -R node:node /home/node/.pi /tmp \ diff --git a/docs/prds/prd-new-modernize-built-in-agent-images.md b/docs/prds/prd-new-modernize-built-in-agent-images.md index 788b552..a0c4843 100644 --- a/docs/prds/prd-new-modernize-built-in-agent-images.md +++ b/docs/prds/prd-new-modernize-built-in-agent-images.md @@ -25,6 +25,9 @@ modify the bottle or cannot run at all. `node:22-trixie-slim`, based on Debian 13 (the current stable release). - Every built-in agent image installs Podman from Debian stable. - Every built-in agent image retains an SSH client for Git-over-SSH workflows. +- The non-root agent user owns a traversable XDG Git configuration directory, + so Git can load bot-bottle's global git-gate rewrites without permission + errors. - A shared test enforces both requirements for current and future built-in providers. diff --git a/tests/unit/test_builtin_agent_images.py b/tests/unit/test_builtin_agent_images.py index df9ab68..61fa961 100644 --- a/tests/unit/test_builtin_agent_images.py +++ b/tests/unit/test_builtin_agent_images.py @@ -37,6 +37,13 @@ class TestBuiltinAgentImages(unittest.TestCase): re.compile(r"(?m)^\s*openssh-client(?:\s|\\|$)"), ) + def test_all_prepare_node_git_config_directory(self): + for dockerfile in _AGENT_DOCKERFILES: + with self.subTest(provider=dockerfile.parent.name): + dockerfile_text = dockerfile.read_text() + self.assertIn("install -d -o node -g node", dockerfile_text) + self.assertIn("/home/node/.config/git", dockerfile_text) + if __name__ == "__main__": unittest.main() -- 2.52.0 From 51b82f80d15c0b6dc31f49204f5394a55c7bc666 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 17:40:18 +0000 Subject: [PATCH 4/9] refactor(agent-images): use explicit Debian base --- bot_bottle/contrib/claude/Dockerfile | 20 +++++++++------ bot_bottle/contrib/codex/Dockerfile | 17 +++++++------ bot_bottle/contrib/pi/Dockerfile | 9 +++++-- ...prd-new-modernize-built-in-agent-images.md | 25 +++++++++++-------- tests/unit/test_builtin_agent_images.py | 13 +++++++++- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index fd8520d..8628b58 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -8,15 +8,15 @@ # Layer ordering is deliberate: the npm install lives in its own layer so # changes to the rest of the repo (or to the CMD) don't bust it. -# Current Node LTS; slim variant keeps the image small while still -# providing apt-get for any future additions. -FROM node:22-trixie-slim +# Debian stable is the supported userspace. The provider's Node.js runtime is +# installed explicitly below rather than inherited from a language image. +FROM debian:trixie-slim # Install runtime system deps. claude-code shells out to git for several # features (status checks, commits, PR creation) — without git in the # image, those features fail in surprising ways once the user does any -# real work. ca-certificates is already in the slim base; listed for -# clarity in case the base ever drops it. curl is here so any +# real work. ca-certificates is installed explicitly for TLS clients. curl is +# here so any # HTTPS_PROXY-aware tool (curl itself, plus anything that shells out # to it) works against egress's bumped TLS without the agent needing # local DNS. @@ -30,8 +30,13 @@ RUN apt-get update \ ripgrep \ iproute2 \ dnsutils \ + nodejs \ + npm \ && rm -rf /var/lib/apt/lists/* +RUN groupadd --gid 1000 node \ + && useradd --uid 1000 --gid node --shell /bin/bash --create-home node + # App-specific deps. Python isn't required by claude-code itself # (claude-code is a Node CLI), but is convenient for the agent to # shell out to for ad-hoc scripts. Kept on its own layer so it can @@ -52,9 +57,8 @@ RUN npm install -g --no-fund --no-audit @anthropic-ai/claude-code@2.1.172 \ # suppress bot-bottle's git-gate insteadOf rules. RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git -# Run as a non-root user. The node image already provides a `node` user -# (uid 1000) with a home directory, which is where claude-code will write -# its session state. +# Run as the explicitly-created non-root user. Claude Code writes its session +# state under this user's home directory. USER node WORKDIR /home/node diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index 56216c2..b25cd91 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -1,9 +1,9 @@ # bot-bottle Codex provider image. # -# Mirrors the default Claude image shape: Node LTS, git/network tooling, -# non-root node user, and the provider CLI installed for that user. +# Mirrors the default Claude image shape: Debian stable, git/network tooling, +# a non-root runtime user, and the provider CLI installed for that user. -FROM node:22-trixie-slim +FROM debian:trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -16,8 +16,11 @@ RUN apt-get update \ ripgrep \ && rm -rf /var/lib/apt/lists/* -# App-specific deps. Python isn't required by codex itself -# (codex is a Node CLI), but is convenient for the agent to shell +RUN groupadd --gid 1000 node \ + && useradd --uid 1000 --gid node --shell /bin/bash --create-home node + +# App-specific deps. Python isn't required by Codex itself, but is convenient +# for the agent to shell # out to for ad-hoc scripts. Kept on its own layer so it can be # moved to a downstream image if the base ever needs to shrink. RUN apt-get update \ @@ -32,8 +35,8 @@ WORKDIR /home/node ENV PATH="/home/node/.local/bin:${PATH}" # Remote-control support requires the standalone Codex install layout -# under ~/.codex/packages/standalone/current. The npm package can run -# the TUI, but remote-control commands expect this installer-owned path. +# under ~/.codex/packages/standalone/current. Remote-control commands expect +# this installer-owned path. RUN mkdir -p /home/node/.codex \ && curl -fsSL https://chatgpt.com/codex/install.sh | sh diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index 02564fd..9c7fcde 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -1,8 +1,8 @@ # bot-bottle Pi provider image. # -# Node LTS, git/network tooling, and the Pi coding-agent CLI installed globally. +# Debian stable, git/network tooling, and the Pi coding-agent CLI. -FROM node:22-trixie-slim +FROM debian:trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -13,9 +13,14 @@ RUN apt-get update \ openssh-client \ podman \ ripgrep \ + nodejs \ + npm \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ && rm -rf /var/lib/apt/lists/* +RUN groupadd --gid 1000 node \ + && useradd --uid 1000 --gid node --shell /bin/bash --create-home node + RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* diff --git a/docs/prds/prd-new-modernize-built-in-agent-images.md b/docs/prds/prd-new-modernize-built-in-agent-images.md index a0c4843..b9eb7c4 100644 --- a/docs/prds/prd-new-modernize-built-in-agent-images.md +++ b/docs/prds/prd-new-modernize-built-in-agent-images.md @@ -13,16 +13,18 @@ userspace and an OCI container tool without requiring per-project setup. ## Problem -The Claude, Codex, and Pi images inherit the generic `node:22-slim` tag. That -tag does not state which Debian release the project supports and currently -leaves the images on the older Bookworm release. None of the built-in images -installs Podman, so tasks that need to inspect or build OCI images must first -modify the bottle or cannot run at all. +The Claude, Codex, and Pi images inherited the generic `node:22-slim` tag. That +tag did not make Debian the explicit supported base and left the images on the +older Bookworm release. None of the built-in images installed Podman, so tasks +that need to inspect or build OCI images first had to modify the bottle or +could not run at all. ## Goals / success criteria - Every Dockerfile under `bot_bottle/contrib/*/Dockerfile` explicitly inherits - `node:22-trixie-slim`, based on Debian 13 (the current stable release). + `debian:trixie-slim`, Debian 13 (the current stable release). +- Every image explicitly creates the non-root `node` runtime user with UID and + GID 1000 instead of relying on a language image to provide it. - Every built-in agent image installs Podman from Debian stable. - Every built-in agent image retains an SSH client for Git-over-SSH workflows. - The non-root agent user owns a traversable XDG Git configuration directory, @@ -39,9 +41,10 @@ modify the bottle or cannot run at all. ## Design -Use the explicit `node:22-trixie-slim` base rather than the floating `slim` -variant. Install the `podman` package with each image's existing `apt-get` +Use the explicit `debian:trixie-slim` base. Install Node.js and npm from Debian +stable where a provider needs them, and create the common `node` runtime user +explicitly. Install the `podman` package with each image's existing `apt-get` dependency layer, so package metadata and caches are still removed in the same -layer. Treat Debian stable as the Podman stability and update channel; this -keeps the images stdlib/distribution-first and avoids adding a third-party -package repository. +layer. Treat Debian stable as the Node.js and Podman stability and update +channel; this keeps the images distribution-first and avoids adding a +third-party package repository. diff --git a/tests/unit/test_builtin_agent_images.py b/tests/unit/test_builtin_agent_images.py index 61fa961..dde6809 100644 --- a/tests/unit/test_builtin_agent_images.py +++ b/tests/unit/test_builtin_agent_images.py @@ -18,7 +18,18 @@ class TestBuiltinAgentImages(unittest.TestCase): with self.subTest(provider=dockerfile.parent.name): self.assertRegex( dockerfile.read_text(), - r"(?m)^FROM node:22-trixie-slim\s*$", + r"(?m)^FROM debian:trixie-slim\s*$", + ) + + def test_all_create_node_runtime_user(self): + for dockerfile in _AGENT_DOCKERFILES: + with self.subTest(provider=dockerfile.parent.name): + dockerfile_text = dockerfile.read_text() + self.assertIn("groupadd --gid 1000 node", dockerfile_text) + self.assertIn( + "useradd --uid 1000 --gid node --shell /bin/bash " + "--create-home node", + dockerfile_text, ) def test_all_install_podman(self): -- 2.52.0 From 735d8ed5f361000a80df9cdef7c0d42a33b804a8 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 17:41:04 +0000 Subject: [PATCH 5/9] Revert "refactor(agent-images): use explicit Debian base" This reverts commit 51b82f80d15c0b6dc31f49204f5394a55c7bc666. --- bot_bottle/contrib/claude/Dockerfile | 20 ++++++--------- bot_bottle/contrib/codex/Dockerfile | 17 ++++++------- bot_bottle/contrib/pi/Dockerfile | 9 ++----- ...prd-new-modernize-built-in-agent-images.md | 25 ++++++++----------- tests/unit/test_builtin_agent_images.py | 13 +--------- 5 files changed, 29 insertions(+), 55 deletions(-) diff --git a/bot_bottle/contrib/claude/Dockerfile b/bot_bottle/contrib/claude/Dockerfile index 8628b58..fd8520d 100644 --- a/bot_bottle/contrib/claude/Dockerfile +++ b/bot_bottle/contrib/claude/Dockerfile @@ -8,15 +8,15 @@ # Layer ordering is deliberate: the npm install lives in its own layer so # changes to the rest of the repo (or to the CMD) don't bust it. -# Debian stable is the supported userspace. The provider's Node.js runtime is -# installed explicitly below rather than inherited from a language image. -FROM debian:trixie-slim +# Current Node LTS; slim variant keeps the image small while still +# providing apt-get for any future additions. +FROM node:22-trixie-slim # Install runtime system deps. claude-code shells out to git for several # features (status checks, commits, PR creation) — without git in the # image, those features fail in surprising ways once the user does any -# real work. ca-certificates is installed explicitly for TLS clients. curl is -# here so any +# real work. ca-certificates is already in the slim base; listed for +# clarity in case the base ever drops it. curl is here so any # HTTPS_PROXY-aware tool (curl itself, plus anything that shells out # to it) works against egress's bumped TLS without the agent needing # local DNS. @@ -30,13 +30,8 @@ RUN apt-get update \ ripgrep \ iproute2 \ dnsutils \ - nodejs \ - npm \ && rm -rf /var/lib/apt/lists/* -RUN groupadd --gid 1000 node \ - && useradd --uid 1000 --gid node --shell /bin/bash --create-home node - # App-specific deps. Python isn't required by claude-code itself # (claude-code is a Node CLI), but is convenient for the agent to # shell out to for ad-hoc scripts. Kept on its own layer so it can @@ -57,8 +52,9 @@ RUN npm install -g --no-fund --no-audit @anthropic-ai/claude-code@2.1.172 \ # suppress bot-bottle's git-gate insteadOf rules. RUN install -d -o node -g node -m 755 /home/node/.config /home/node/.config/git -# Run as the explicitly-created non-root user. Claude Code writes its session -# state under this user's home directory. +# Run as a non-root user. The node image already provides a `node` user +# (uid 1000) with a home directory, which is where claude-code will write +# its session state. USER node WORKDIR /home/node diff --git a/bot_bottle/contrib/codex/Dockerfile b/bot_bottle/contrib/codex/Dockerfile index b25cd91..56216c2 100644 --- a/bot_bottle/contrib/codex/Dockerfile +++ b/bot_bottle/contrib/codex/Dockerfile @@ -1,9 +1,9 @@ # bot-bottle Codex provider image. # -# Mirrors the default Claude image shape: Debian stable, git/network tooling, -# a non-root runtime user, and the provider CLI installed for that user. +# Mirrors the default Claude image shape: Node LTS, git/network tooling, +# non-root node user, and the provider CLI installed for that user. -FROM debian:trixie-slim +FROM node:22-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -16,11 +16,8 @@ RUN apt-get update \ ripgrep \ && rm -rf /var/lib/apt/lists/* -RUN groupadd --gid 1000 node \ - && useradd --uid 1000 --gid node --shell /bin/bash --create-home node - -# App-specific deps. Python isn't required by Codex itself, but is convenient -# for the agent to shell +# App-specific deps. Python isn't required by codex itself +# (codex is a Node CLI), but is convenient for the agent to shell # out to for ad-hoc scripts. Kept on its own layer so it can be # moved to a downstream image if the base ever needs to shrink. RUN apt-get update \ @@ -35,8 +32,8 @@ WORKDIR /home/node ENV PATH="/home/node/.local/bin:${PATH}" # Remote-control support requires the standalone Codex install layout -# under ~/.codex/packages/standalone/current. Remote-control commands expect -# this installer-owned path. +# under ~/.codex/packages/standalone/current. The npm package can run +# the TUI, but remote-control commands expect this installer-owned path. RUN mkdir -p /home/node/.codex \ && curl -fsSL https://chatgpt.com/codex/install.sh | sh diff --git a/bot_bottle/contrib/pi/Dockerfile b/bot_bottle/contrib/pi/Dockerfile index 9c7fcde..02564fd 100644 --- a/bot_bottle/contrib/pi/Dockerfile +++ b/bot_bottle/contrib/pi/Dockerfile @@ -1,8 +1,8 @@ # bot-bottle Pi provider image. # -# Debian stable, git/network tooling, and the Pi coding-agent CLI. +# Node LTS, git/network tooling, and the Pi coding-agent CLI installed globally. -FROM debian:trixie-slim +FROM node:22-trixie-slim RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -13,14 +13,9 @@ RUN apt-get update \ openssh-client \ podman \ ripgrep \ - nodejs \ - npm \ && ln -s /usr/bin/fdfind /usr/local/bin/fd \ && rm -rf /var/lib/apt/lists/* -RUN groupadd --gid 1000 node \ - && useradd --uid 1000 --gid node --shell /bin/bash --create-home node - RUN apt-get update \ && apt-get install -y --no-install-recommends python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* diff --git a/docs/prds/prd-new-modernize-built-in-agent-images.md b/docs/prds/prd-new-modernize-built-in-agent-images.md index b9eb7c4..a0c4843 100644 --- a/docs/prds/prd-new-modernize-built-in-agent-images.md +++ b/docs/prds/prd-new-modernize-built-in-agent-images.md @@ -13,18 +13,16 @@ userspace and an OCI container tool without requiring per-project setup. ## Problem -The Claude, Codex, and Pi images inherited the generic `node:22-slim` tag. That -tag did not make Debian the explicit supported base and left the images on the -older Bookworm release. None of the built-in images installed Podman, so tasks -that need to inspect or build OCI images first had to modify the bottle or -could not run at all. +The Claude, Codex, and Pi images inherit the generic `node:22-slim` tag. That +tag does not state which Debian release the project supports and currently +leaves the images on the older Bookworm release. None of the built-in images +installs Podman, so tasks that need to inspect or build OCI images must first +modify the bottle or cannot run at all. ## Goals / success criteria - Every Dockerfile under `bot_bottle/contrib/*/Dockerfile` explicitly inherits - `debian:trixie-slim`, Debian 13 (the current stable release). -- Every image explicitly creates the non-root `node` runtime user with UID and - GID 1000 instead of relying on a language image to provide it. + `node:22-trixie-slim`, based on Debian 13 (the current stable release). - Every built-in agent image installs Podman from Debian stable. - Every built-in agent image retains an SSH client for Git-over-SSH workflows. - The non-root agent user owns a traversable XDG Git configuration directory, @@ -41,10 +39,9 @@ could not run at all. ## Design -Use the explicit `debian:trixie-slim` base. Install Node.js and npm from Debian -stable where a provider needs them, and create the common `node` runtime user -explicitly. Install the `podman` package with each image's existing `apt-get` +Use the explicit `node:22-trixie-slim` base rather than the floating `slim` +variant. Install the `podman` package with each image's existing `apt-get` dependency layer, so package metadata and caches are still removed in the same -layer. Treat Debian stable as the Node.js and Podman stability and update -channel; this keeps the images distribution-first and avoids adding a -third-party package repository. +layer. Treat Debian stable as the Podman stability and update channel; this +keeps the images stdlib/distribution-first and avoids adding a third-party +package repository. diff --git a/tests/unit/test_builtin_agent_images.py b/tests/unit/test_builtin_agent_images.py index dde6809..61fa961 100644 --- a/tests/unit/test_builtin_agent_images.py +++ b/tests/unit/test_builtin_agent_images.py @@ -18,18 +18,7 @@ class TestBuiltinAgentImages(unittest.TestCase): with self.subTest(provider=dockerfile.parent.name): self.assertRegex( dockerfile.read_text(), - r"(?m)^FROM debian:trixie-slim\s*$", - ) - - def test_all_create_node_runtime_user(self): - for dockerfile in _AGENT_DOCKERFILES: - with self.subTest(provider=dockerfile.parent.name): - dockerfile_text = dockerfile.read_text() - self.assertIn("groupadd --gid 1000 node", dockerfile_text) - self.assertIn( - "useradd --uid 1000 --gid node --shell /bin/bash " - "--create-home node", - dockerfile_text, + r"(?m)^FROM node:22-trixie-slim\s*$", ) def test_all_install_podman(self): -- 2.52.0 From 7512e3179ac1e2059846f2c12452aa1c6f8227e8 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 18:09:27 +0000 Subject: [PATCH 6/9] fix(firecracker): repair runtime Git config ownership --- bot_bottle/agent_provider.py | 15 +++++++++++++++ tests/unit/test_docker_provision_git_user.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 3ef816b..c0b00dc 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -259,6 +259,21 @@ class AgentProvider(ABC): BottleBackend.provision_workspace against the running bottle.""" from .log import info + # Firecracker exports image rootfs files through an unprivileged host + # tar extraction, so image-time ownership of XDG directories is not + # preserved. Git consults ~/.config/git even when the actual config + # is ~/.gitconfig; an unreadable directory there can prevent the + # git-gate insteadOf rules below from taking effect. Repair this at + # runtime, after every backend's copy/export path has completed. + git_xdg_dir = f"{plan.guest_home}/.config/git" + bottle.exec( + f"mkdir -p {shlex.quote(git_xdg_dir)} && " + f"chown -R node:node {shlex.quote(f'{plan.guest_home}/.config')} && " + f"chmod 755 {shlex.quote(f'{plan.guest_home}/.config')} " + f"{shlex.quote(git_xdg_dir)}", + user="root", + ) + manifest_bottle = plan.manifest.bottle if manifest_bottle.git: from .git_gate import GIT_GATE_HOSTNAME, git_gate_render_gitconfig diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index 081bfce..852822b 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -125,6 +125,20 @@ class TestProvisionGitUser(unittest.TestCase): _PROVIDER.provision_git(bottle, _plan(stage_dir=self.stage)) self.assertEqual([], _git_config_exec_calls(bottle)) + def test_repairs_git_xdg_directory_for_runtime_user(self): + bottle = _make_bottle() + _PROVIDER.provision_git(bottle, _plan(stage_dir=self.stage)) + + script, user = next( + (call.args[0], call.kwargs.get("user", "node")) + for call in bottle.exec.call_args_list + if "/home/node/.config/git" in call.args[0] + ) + self.assertEqual("root", user) + self.assertIn("mkdir -p /home/node/.config/git", script) + self.assertIn("chown -R node:node /home/node/.config", script) + self.assertIn("chmod 755 /home/node/.config /home/node/.config/git", script) + def test_sets_name_and_email(self): plan = _plan( git_user={"name": "Eric Bauerfeld", "email": "eric@dideric.is"}, -- 2.52.0 From c2c934899c0232bce2783124aac68ac33b2debf5 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 18:17:29 +0000 Subject: [PATCH 7/9] fix(firecracker): pin the provisioned Git config --- bot_bottle/agent_provider.py | 30 ++++++++++++++++---- bot_bottle/backend/firecracker/launch.py | 5 ++++ tests/unit/test_docker_provision_git_user.py | 2 +- tests/unit/test_firecracker_backend.py | 13 ++++++++- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index c0b00dc..006e4eb 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -257,7 +257,7 @@ class AgentProvider(ABC): Default: Debian/node — writes the git-gate insteadOf gitconfig and sets user.name/email as node. Workspace copy runs through BottleBackend.provision_workspace against the running bottle.""" - from .log import info + from .log import die, info # Firecracker exports image rootfs files through an unprivileged host # tar extraction, so image-time ownership of XDG directories is not @@ -266,13 +266,17 @@ class AgentProvider(ABC): # git-gate insteadOf rules below from taking effect. Repair this at # runtime, after every backend's copy/export path has completed. git_xdg_dir = f"{plan.guest_home}/.config/git" - bottle.exec( + repair = bottle.exec( f"mkdir -p {shlex.quote(git_xdg_dir)} && " f"chown -R node:node {shlex.quote(f'{plan.guest_home}/.config')} && " - f"chmod 755 {shlex.quote(f'{plan.guest_home}/.config')} " - f"{shlex.quote(git_xdg_dir)}", + f"chmod -R u+rwX,go+rX {shlex.quote(f'{plan.guest_home}/.config')}", user="root", ) + if repair.returncode != 0: + die( + "git provisioning: could not make the runtime Git config " + f"directory readable: {(repair.stderr or repair.stdout).strip()}" + ) manifest_bottle = plan.manifest.bottle if manifest_bottle.git: @@ -295,11 +299,27 @@ class AgentProvider(ABC): f"{len(manifest_bottle.git)} insteadOf rule(s)" ) bottle.cp_in(str(config_file), guest_gitconfig) - bottle.exec( + permissions = bottle.exec( f"chown node:node {shlex.quote(guest_gitconfig)} && " f"chmod 644 {shlex.quote(guest_gitconfig)}", user="root", ) + if permissions.returncode != 0: + die( + "git provisioning: could not set ownership on " + f"{guest_gitconfig}: " + f"{(permissions.stderr or permissions.stdout).strip()}" + ) + configured = bottle.exec( + "git config --global --get-regexp '^url\\..*\\.insteadof$'", + user="node", + ) + if configured.returncode != 0: + die( + "git provisioning: the runtime user cannot read the " + f"git-gate insteadOf rules from {guest_gitconfig}: " + f"{(configured.stderr or configured.stdout).strip()}" + ) gu = manifest_bottle.git_user if not gu.is_empty(): diff --git a/bot_bottle/backend/firecracker/launch.py b/bot_bottle/backend/firecracker/launch.py index a5efc00..b87b44b 100644 --- a/bot_bottle/backend/firecracker/launch.py +++ b/bot_bottle/backend/firecracker/launch.py @@ -242,6 +242,11 @@ def _agent_guest_env(plan: FirecrackerBottlePlan, host_ip: str) -> dict[str, str "HTTPS_PROXY": proxy_url, "HTTP_PROXY": proxy_url, "https_proxy": proxy_url, "http_proxy": proxy_url, "NO_PROXY": no_proxy, "no_proxy": no_proxy, + # Rootfs export can leave Git's implicit XDG paths unreadable even + # after the runtime repair. Bypass that discovery and name the + # provisioned global config explicitly so insteadOf can never fall + # through to the credential-bearing upstream URL. + "GIT_CONFIG_GLOBAL": f"{plan.guest_home}/.gitconfig", "NODE_EXTRA_CA_CERTS": AGENT_CA_PATH, "SSL_CERT_FILE": AGENT_CA_BUNDLE, "REQUESTS_CA_BUNDLE": AGENT_CA_BUNDLE, diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index 852822b..c6e8e8d 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -137,7 +137,7 @@ class TestProvisionGitUser(unittest.TestCase): self.assertEqual("root", user) self.assertIn("mkdir -p /home/node/.config/git", script) self.assertIn("chown -R node:node /home/node/.config", script) - self.assertIn("chmod 755 /home/node/.config /home/node/.config/git", script) + self.assertIn("chmod -R u+rwX,go+rX /home/node/.config", script) def test_sets_name_and_email(self): plan = _plan( diff --git a/tests/unit/test_firecracker_backend.py b/tests/unit/test_firecracker_backend.py index e29c972..5da1c77 100644 --- a/tests/unit/test_firecracker_backend.py +++ b/tests/unit/test_firecracker_backend.py @@ -422,10 +422,15 @@ class TestBottlePlanProperties(unittest.TestCase): ap.command = "claude" ap.prompt_mode = "append_file" ap.template = "claude" + ap.guest_home = "/home/node" + ap.guest_env = {} + egress_plan = cast(Any, MagicMock()) + egress_plan.canary = "" + egress_plan.canary_env = "" fields = dict( spec=cast(Any, MagicMock()), manifest=cast(Any, MagicMock()), stage_dir=Path("/stage"), git_gate_plan=cast(Any, MagicMock()), - egress_plan=cast(Any, MagicMock()), supervise_plan=None, + egress_plan=egress_plan, supervise_plan=None, agent_provision=ap, slug="demo-x", forwarded_env={}, ) fields.update(overrides) @@ -451,6 +456,12 @@ class TestBottlePlanProperties(unittest.TestCase): self.assertEqual("10.243.0.0:9420", p.git_gate_insteadof_host) self.assertEqual("http", p.git_gate_insteadof_scheme) + def test_guest_env_pins_global_git_config(self): + from bot_bottle.backend.firecracker.launch import _agent_guest_env + + env = _agent_guest_env(self._plan(), "10.243.0.0") + self.assertEqual("/home/node/.gitconfig", env["GIT_CONFIG_GLOBAL"]) + if __name__ == "__main__": unittest.main() -- 2.52.0 From 9256bce4d42064fad8b2f4acafac99e4cfc6db0d Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 18:31:15 +0000 Subject: [PATCH 8/9] fix(firecracker): restore agent home ownership at boot --- bot_bottle/agent_provider.py | 2 ++ bot_bottle/backend/firecracker/util.py | 6 ++++++ tests/unit/test_docker_provision_git_user.py | 2 ++ tests/unit/test_firecracker_backend.py | 6 ++++++ 4 files changed, 16 insertions(+) diff --git a/bot_bottle/agent_provider.py b/bot_bottle/agent_provider.py index 006e4eb..c3d94d0 100644 --- a/bot_bottle/agent_provider.py +++ b/bot_bottle/agent_provider.py @@ -267,6 +267,8 @@ class AgentProvider(ABC): # runtime, after every backend's copy/export path has completed. git_xdg_dir = f"{plan.guest_home}/.config/git" repair = bottle.exec( + f"chown node:node {shlex.quote(plan.guest_home)} && " + f"chmod 755 {shlex.quote(plan.guest_home)} && " f"mkdir -p {shlex.quote(git_xdg_dir)} && " f"chown -R node:node {shlex.quote(f'{plan.guest_home}/.config')} && " f"chmod -R u+rwX,go+rX {shlex.quote(f'{plan.guest_home}/.config')}", diff --git a/bot_bottle/backend/firecracker/util.py b/bot_bottle/backend/firecracker/util.py index cf30a18..0d9aad8 100644 --- a/bot_bottle/backend/firecracker/util.py +++ b/bot_bottle/backend/firecracker/util.py @@ -373,6 +373,12 @@ mount -o remount,rw / 2>/dev/null # scratch dirs there — git worktrees, build temp, `git init /tmp/...`, etc. mkdir -p /tmp && chmod 1777 /tmp +# Rootfs export also maps the image's original owners to the unprivileged +# host build uid. That uid is not guaranteed to be node's uid in the guest; +# restore the home-directory boundary before any SSH provisioning runs. +chown node:node /home/node 2>/dev/null || true +chmod 755 /home/node 2>/dev/null || true + # Install the per-bottle SSH pubkey from the kernel cmdline. KEY=$(sed -n 's/.*bb_pubkey=\([^ ]*\).*/\1/p' /proc/cmdline | base64 -d 2>/dev/null) if [ -n "$KEY" ]; then diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index c6e8e8d..a889002 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -135,6 +135,8 @@ class TestProvisionGitUser(unittest.TestCase): if "/home/node/.config/git" in call.args[0] ) self.assertEqual("root", user) + self.assertIn("chown node:node /home/node", script) + self.assertIn("chmod 755 /home/node", script) self.assertIn("mkdir -p /home/node/.config/git", script) self.assertIn("chown -R node:node /home/node/.config", script) self.assertIn("chmod -R u+rwX,go+rX /home/node/.config", script) diff --git a/tests/unit/test_firecracker_backend.py b/tests/unit/test_firecracker_backend.py index 5da1c77..d72d795 100644 --- a/tests/unit/test_firecracker_backend.py +++ b/tests/unit/test_firecracker_backend.py @@ -61,6 +61,12 @@ class TestNetpoolSlots(unittest.TestCase): class TestNetpoolRenderers(unittest.TestCase): + def test_guest_init_restores_node_home_boundary(self): + from bot_bottle.backend.firecracker import util + + self.assertIn("chown node:node /home/node", util._GUEST_INIT) + self.assertIn("chmod 755 /home/node", util._GUEST_INIT) + def test_nixos_module_is_non_invasive(self): # The NixOS module must NOT flip the host firewall backend or # hand interfaces to systemd-networkd; it brings the pool up via -- 2.52.0 From 42bafcf7407cc300616b68728db7064c93fafc7d Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 21 Jul 2026 18:40:33 +0000 Subject: [PATCH 9/9] test(git): cover provisioning failures --- tests/unit/test_docker_provision_git_user.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/unit/test_docker_provision_git_user.py b/tests/unit/test_docker_provision_git_user.py index a889002..d193e9f 100644 --- a/tests/unit/test_docker_provision_git_user.py +++ b/tests/unit/test_docker_provision_git_user.py @@ -45,12 +45,15 @@ _PROVIDER = _Provider() def _plan(*, git_user: dict | None = None, # type: ignore + git_repos: dict | None = None, # type: ignore copy_cwd: bool = False, user_cwd: str = "/tmp/x", stage_dir: Path | None = None) -> DockerBottlePlan: bottle_json: dict = {} # type: ignore if git_user is not None: bottle_json["git-gate"] = {"user": git_user} + if git_repos is not None: + bottle_json.setdefault("git-gate", {})["repos"] = git_repos index = ManifestIndex.from_json_obj({ "bottles": {"dev": bottle_json}, "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, @@ -141,6 +144,46 @@ class TestProvisionGitUser(unittest.TestCase): self.assertIn("chown -R node:node /home/node/.config", script) self.assertIn("chmod -R u+rwX,go+rX /home/node/.config", script) + def test_fails_closed_when_home_permissions_cannot_be_repaired(self): + bottle = _make_bottle() + bottle.exec.return_value = ExecResult(1, "", "read-only filesystem") + + with self.assertRaises(SystemExit): + _PROVIDER.provision_git(bottle, _plan(stage_dir=self.stage)) + + def _git_plan(self) -> DockerBottlePlan: + return _plan( + git_repos={ + "repo": { + "url": "ssh://git@example.com/repo.git", + "key": {"provider": "static", "path": "/dev/null"}, + "host_key": "ssh-ed25519 AAAA", + }, + }, + stage_dir=self.stage, + ) + + def test_fails_closed_when_gitconfig_permissions_cannot_be_set(self): + bottle = _make_bottle() + bottle.exec.side_effect = [ + ExecResult(0, "", ""), + ExecResult(1, "", "chown failed"), + ] + + with self.assertRaises(SystemExit): + _PROVIDER.provision_git(bottle, self._git_plan()) + + def test_fails_closed_when_runtime_user_cannot_read_gitconfig(self): + bottle = _make_bottle() + bottle.exec.side_effect = [ + ExecResult(0, "", ""), + ExecResult(0, "", ""), + ExecResult(1, "", "permission denied"), + ] + + with self.assertRaises(SystemExit): + _PROVIDER.provision_git(bottle, self._git_plan()) + def test_sets_name_and_email(self): plan = _plan( git_user={"name": "Eric Bauerfeld", "email": "eric@dideric.is"}, -- 2.52.0