agent-containers 0.1.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. agent_containers/__init__.py +1 -0
  2. agent_containers/__main__.py +6 -0
  3. agent_containers/_assets/agent-images/adal/Dockerfile +103 -0
  4. agent_containers/_assets/agent-images/adal/examples/egress-allowlist.txt +10 -0
  5. agent_containers/_assets/agent-images/adal/packages-apt.txt +13 -0
  6. agent_containers/_assets/agent-images/adal/packages-npm.txt +2 -0
  7. agent_containers/_assets/agent-images/adal/packages-uv.txt +6 -0
  8. agent_containers/_assets/agent-images/adal/tools-uv.txt +4 -0
  9. agent_containers/_assets/agent-images/agent-gateway/Dockerfile +50 -0
  10. agent_containers/_assets/agent-images/agent-gateway/entrypoint.sh +28 -0
  11. agent_containers/_assets/agent-images/agent-gateway/sshd_config.agent-gateway +11 -0
  12. agent_containers/_assets/agent-images/aider/Dockerfile +114 -0
  13. agent_containers/_assets/agent-images/aider/examples/egress-allowlist.txt +16 -0
  14. agent_containers/_assets/agent-images/aider/packages-apt.txt +16 -0
  15. agent_containers/_assets/agent-images/aider/packages-npm.txt +7 -0
  16. agent_containers/_assets/agent-images/aider/packages-uv.txt +9 -0
  17. agent_containers/_assets/agent-images/aider/tools-uv.txt +9 -0
  18. agent_containers/_assets/agent-images/claude-code/Dockerfile +171 -0
  19. agent_containers/_assets/agent-images/claude-code/entrypoint.sh +22 -0
  20. agent_containers/_assets/agent-images/claude-code/examples/egress-allowlist.txt +14 -0
  21. agent_containers/_assets/agent-images/claude-code/examples/settings.local-model.json +7 -0
  22. agent_containers/_assets/agent-images/claude-code/packages-apt.txt +16 -0
  23. agent_containers/_assets/agent-images/claude-code/packages-npm.txt +7 -0
  24. agent_containers/_assets/agent-images/claude-code/packages-uv.txt +9 -0
  25. agent_containers/_assets/agent-images/claude-code/plugin-marketplaces.txt +23 -0
  26. agent_containers/_assets/agent-images/claude-code/plugins.txt +13 -0
  27. agent_containers/_assets/agent-images/claude-code/proxy-ca.d/.keep +1 -0
  28. agent_containers/_assets/agent-images/claude-code/tools-uv.txt +9 -0
  29. agent_containers/_assets/agent-images/codex/Dockerfile +147 -0
  30. agent_containers/_assets/agent-images/codex/codex-wrapper.sh +13 -0
  31. agent_containers/_assets/agent-images/codex/examples/egress-allowlist.txt +10 -0
  32. agent_containers/_assets/agent-images/codex/packages-apt.txt +13 -0
  33. agent_containers/_assets/agent-images/codex/packages-npm.txt +2 -0
  34. agent_containers/_assets/agent-images/codex/packages-uv.txt +6 -0
  35. agent_containers/_assets/agent-images/codex/plugin-marketplaces.txt +2 -0
  36. agent_containers/_assets/agent-images/codex/plugins.txt +2 -0
  37. agent_containers/_assets/agent-images/codex/proxy-ca.d/.keep +1 -0
  38. agent_containers/_assets/agent-images/codex/tools-uv.txt +4 -0
  39. agent_containers/_assets/agent-images/hermes/Dockerfile +122 -0
  40. agent_containers/_assets/agent-images/hermes/entrypoint.sh +49 -0
  41. agent_containers/_assets/agent-images/hermes/packages-apt.txt +16 -0
  42. agent_containers/_assets/agent-images/hermes/packages-npm.txt +7 -0
  43. agent_containers/_assets/agent-images/hermes/packages-uv.txt +9 -0
  44. agent_containers/_assets/agent-images/hermes/proxy-ca.d/.keep +1 -0
  45. agent_containers/_assets/agent-images/hermes/tools-uv.txt +9 -0
  46. agent_containers/_assets/agent-images/kilo-code/Dockerfile +101 -0
  47. agent_containers/_assets/agent-images/kilo-code/examples/egress-allowlist.txt +7 -0
  48. agent_containers/_assets/agent-images/kilo-code/packages-apt.txt +5 -0
  49. agent_containers/_assets/agent-images/kilo-code/packages-npm.txt +1 -0
  50. agent_containers/_assets/agent-images/kilo-code/packages-uv.txt +4 -0
  51. agent_containers/_assets/agent-images/kilo-code/tools-uv.txt +4 -0
  52. agent_containers/_assets/agent-images/opencode/Dockerfile +126 -0
  53. agent_containers/_assets/agent-images/opencode/examples/egress-allowlist.txt +6 -0
  54. agent_containers/_assets/agent-images/opencode/packages-apt.txt +13 -0
  55. agent_containers/_assets/agent-images/opencode/packages-npm.txt +2 -0
  56. agent_containers/_assets/agent-images/opencode/packages-uv.txt +6 -0
  57. agent_containers/_assets/agent-images/opencode/proxy-ca.d/.keep +1 -0
  58. agent_containers/_assets/agent-images/opencode/tools-uv.txt +4 -0
  59. agent_containers/_assets/agent-images/qwen-code/Dockerfile +104 -0
  60. agent_containers/_assets/agent-images/qwen-code/examples/egress-allowlist.txt +10 -0
  61. agent_containers/_assets/agent-images/qwen-code/packages-apt.txt +13 -0
  62. agent_containers/_assets/agent-images/qwen-code/packages-npm.txt +2 -0
  63. agent_containers/_assets/agent-images/qwen-code/packages-uv.txt +6 -0
  64. agent_containers/_assets/agent-images/qwen-code/tools-uv.txt +4 -0
  65. agent_containers/_assets/agent-images/shared/egress-allowlist.sh +138 -0
  66. agent_containers/_assets/agent-images/shared/install-additional-packages.sh +79 -0
  67. agent_containers/_assets/agent-images/shared/install-workload-base.sh +46 -0
  68. agent_containers/_assets/agent-images/shared/workload-entrypoint.sh +82 -0
  69. agent_containers/_assets/agent-images/volume-bridge/Dockerfile +37 -0
  70. agent_containers/_assets/agent-images/volume-bridge/entrypoint.sh +29 -0
  71. agent_containers/_assets/agent-images/volume-bridge/serve.sh +66 -0
  72. agent_containers/assets.py +38 -0
  73. agent_containers/build_context.py +116 -0
  74. agent_containers/cli.py +212 -0
  75. agent_containers/creation.py +177 -0
  76. agent_containers/docker.py +562 -0
  77. agent_containers/lifecycle.py +325 -0
  78. agent_containers/planner.py +99 -0
  79. agent_containers/profile.py +377 -0
  80. agent_containers/shortcuts.py +159 -0
  81. agent_containers/state.py +154 -0
  82. agent_containers-0.1.0.dev0.dist-info/METADATA +195 -0
  83. agent_containers-0.1.0.dev0.dist-info/RECORD +86 -0
  84. agent_containers-0.1.0.dev0.dist-info/WHEEL +4 -0
  85. agent_containers-0.1.0.dev0.dist-info/entry_points.txt +3 -0
  86. agent_containers-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ """Deterministic onboarding and maintenance for hardened agent containers."""
@@ -0,0 +1,6 @@
1
+ """Support python -m agent_containers."""
2
+
3
+ from agent_containers.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,103 @@
1
+ FROM python:3.14-slim-trixie AS base
2
+
3
+ ARG TARGETARCH
4
+
5
+ # Shared Node.js, containment, gateway-client, and cloudflared baseline for
6
+ # the first-party Python-and-Node workload images.
7
+ COPY --from=shared install-workload-base.sh /usr/local/lib/agent/install-workload-base.sh
8
+ RUN TARGETARCH="$TARGETARCH" bash /usr/local/lib/agent/install-workload-base.sh
9
+
10
+ COPY --from=shared install-additional-packages.sh /usr/local/lib/agent/install-additional-packages.sh
11
+ COPY packages-apt.txt /tmp/packages-apt.txt
12
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
13
+ && install_apt_packages /tmp/packages-apt.txt' \
14
+ && rm -f /tmp/packages-apt.txt
15
+
16
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
17
+
18
+ # packages-uv.txt — general Python libraries (not CLI tools; see
19
+ # tools-uv.txt below), installed via `uv pip install --system` into this
20
+ # system Python. Runs as root, before the adal user exists, since it writes
21
+ # into the image's system site-packages rather than $HOME.
22
+ COPY packages-uv.txt /tmp/packages-uv.txt
23
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
24
+ && install_uv_packages /tmp/packages-uv.txt' \
25
+ && rm -f /tmp/packages-uv.txt
26
+
27
+ # The official npm fallback ships the platform-specific AdaL runtime as an
28
+ # optional dependency. Pin it so rebuilding does not silently change the CLI.
29
+ RUN npm install -g @sylphai/adal-cli@1.7.1
30
+
31
+ # The adal account is created here, at a fixed placeholder UID/GID
32
+ # (1000:1000), purely so the root-only installers below (npm, uv tool) have
33
+ # a real non-root account to run as. `final` remaps this account to the
34
+ # operator's actual host UID/GID instead of recreating it, so a rebuild for
35
+ # a different host user reuses everything installed below unchanged rather
36
+ # than reinstalling it over the network.
37
+ RUN groupadd -g 1000 adal \
38
+ && useradd -m -u 1000 -g 1000 -s /bin/bash adal \
39
+ && mkdir -p /home/adal/.adal /workspace \
40
+ && chown -R adal:adal /home/adal /workspace
41
+
42
+ ENV HOME=/home/adal
43
+ ENV npm_config_prefix=/home/adal/.npm-global
44
+ ENV PATH="/home/adal/.local/bin:/home/adal/.npm-global/bin:${PATH}"
45
+
46
+ # $HOME (uv's default cache location) and /workspace are deliberately
47
+ # separate mounts in this containment model, so uv's hardlink/reflink
48
+ # optimisation can never apply between them — declare the copy fallback
49
+ # explicit rather than have uv warn about it on every install.
50
+ ENV UV_LINK_MODE=copy
51
+
52
+ COPY packages-npm.txt /tmp/packages-npm.txt
53
+ COPY tools-uv.txt /tmp/tools-uv.txt
54
+ RUN gosu adal bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
55
+ && install_npm_packages /tmp/packages-npm.txt \
56
+ && install_uv_tools /tmp/tools-uv.txt' \
57
+ && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt
58
+
59
+ # Everything above this line is UID/GID-independent and layer-cacheable
60
+ # across hosts: the adal account exists at the fixed placeholder UID/GID
61
+ # only so the installers above have a non-root account to run as. From here
62
+ # down, `final` remaps that account to the operator's actual host UID/GID
63
+ # with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base stage
64
+ # above is reused unchanged from cache, and nothing installed above is
65
+ # reinstalled, rather than rebuilding per host user.
66
+ FROM base AS final
67
+ ARG UID=1000
68
+ ARG GID=1000
69
+
70
+ # Only remap when the caller supplied a different UID/GID than the fixed
71
+ # placeholder baked into `base` above — usermod/groupmod plus a chown -R
72
+ # walk over already-installed content is far cheaper than rerunning the
73
+ # npm/uv-tool installs above, but it's skippable entirely for the common
74
+ # case where the caller never overrides UID/GID. Deliberately no -o:
75
+ # without it, usermod/groupmod refuse to reuse an ID already assigned to
76
+ # another account, so a caller can never remap adal onto UID/GID 0 (root)
77
+ # or collide it with another in-image identity — the build fails instead of
78
+ # silently succeeding.
79
+ RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
80
+ groupmod -g "$GID" adal \
81
+ && usermod -u "$UID" -g "$GID" adal \
82
+ && chown -R "$UID":"$GID" /home/adal /workspace; \
83
+ fi
84
+
85
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
86
+ COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
87
+ RUN chmod +x /usr/local/lib/agent/workload-entrypoint.sh
88
+
89
+ # Strip setuid/setgid bits from the base image and every package installed
90
+ # above — required infra (install-workload-base.sh), packages-apt.txt,
91
+ # packages-npm.txt, packages-uv.txt, and tools-uv.txt — run last so nothing
92
+ # installed by any of those is missed. NoNewPrivs=1 (set by every documented
93
+ # run invocation) already makes these bits inert on execve, but that's an
94
+ # argument for stripping them, not against: if any single control ever
95
+ # regresses, a setuid-root `mount` becomes a live escalation primitive. None
96
+ # of these binaries are needed at runtime, so none should ship
97
+ # exploitable-shaped.
98
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
99
+
100
+ WORKDIR /workspace
101
+
102
+ ENTRYPOINT ["/usr/local/lib/agent/workload-entrypoint.sh", "adal", "adal"]
103
+ CMD ["adal"]
@@ -0,0 +1,10 @@
1
+ # Hosts/IPs the container is allowed to reach outbound, one per line.
2
+ # Blank lines and lines starting with '#' are ignored. Mount this file at
3
+ # /etc/agent/egress-allowlist.txt; it takes precedence over
4
+ # $AGENT_ALLOWED_EGRESS. If neither is set, outbound traffic is denied.
5
+ #
6
+ # AdaL's browser-based sign-in starts at this documented service host. Add
7
+ # only the further hosts required by the features you intentionally enable:
8
+ # direct BYOAK providers, an MCP server, a plugin marketplace, a source host,
9
+ # or a local-model endpoint. Do not use this file to grant broad egress.
10
+ adal.sylph.ai
@@ -0,0 +1,13 @@
1
+ # Optional apt packages installed at build time, one per line.
2
+ # Blank lines and lines starting with '#' are ignored.
3
+ gh
4
+ jq
5
+ ripgrep
6
+ fd-find
7
+ tree
8
+ unzip
9
+ less
10
+ file
11
+ lsof
12
+ yq
13
+ miller
@@ -0,0 +1,2 @@
1
+ # Optional global npm packages installed at build time, one per line.
2
+ # Blank lines and lines starting with '#' are ignored.
@@ -0,0 +1,6 @@
1
+ # Optional Python libraries installed into the system Python at build time
2
+ # via `uv pip install --system`, one per line. Blank lines and lines
3
+ # starting with '#' are ignored. Unlike tools-uv.txt, these land in the one
4
+ # shared system Python rather than an isolated venv, so they're importable
5
+ # by whatever runs inside the image — a CLI-only package with no importable
6
+ # module of its own belongs in tools-uv.txt instead.
@@ -0,0 +1,4 @@
1
+ # Optional Python CLI tools installed at build time via `uv tool install`,
2
+ # each into its own isolated venv, one per line. Blank lines and lines
3
+ # starting with '#' are ignored. A plain importable library (no
4
+ # console-script entry point of its own) belongs in packages-uv.txt instead.
@@ -0,0 +1,50 @@
1
+ FROM debian:trixie-slim
2
+
3
+ ARG TARGETARCH
4
+
5
+ RUN apt-get update \
6
+ && apt-get upgrade -y \
7
+ && apt-get install -y --no-install-recommends \
8
+ ca-certificates \
9
+ iptables \
10
+ ipset \
11
+ dnsmasq \
12
+ dnsutils \
13
+ openssh-server \
14
+ python3 \
15
+ curl \
16
+ && curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${TARGETARCH}" \
17
+ -o /usr/local/bin/cloudflared \
18
+ && chmod +x /usr/local/bin/cloudflared \
19
+ && rm -f /etc/ssh/ssh_host_* \
20
+ && rm -rf /var/lib/apt/lists/*
21
+ # python3 isn't used by anything in this image — it's what sshuttle execs
22
+ # remotely over the SSH session from the claude-code side. The entrypoint
23
+ # copies the mounted public key to a root-owned runtime path under /run, so
24
+ # it works with a read-only root filesystem without CAP_CHOWN. cloudflared
25
+ # is always installed but only ever invoked (see entrypoint.sh) when
26
+ # CLOUDFLARE_TUNNEL_TOKEN is set — the direct-TCP path (Task 4) needs no
27
+ # Cloudflare account and stays completely unaffected by its presence.
28
+ # TARGETARCH is supplied automatically by Buildx and matches cloudflared's
29
+ # own release-asset naming (amd64/arm64/386/...).
30
+
31
+ RUN useradd -m -s /bin/bash -d /home/tunnel tunnel \
32
+ && passwd -l tunnel
33
+
34
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
35
+ COPY sshd_config.agent-gateway /etc/ssh/sshd_config.d/agent-gateway.conf
36
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
37
+ RUN chmod +x /usr/local/bin/entrypoint.sh
38
+
39
+ # Strip setuid/setgid bits from the base image and packages above (mount,
40
+ # umount, passwd, chfn, gpasswd, newgrp, ...) — none of them are this image's
41
+ # own privilege mechanism. sshd runs as real root throughout (not via a
42
+ # setuid binary) and dnsmasq drops root->its own user via the CAP_SETUID/
43
+ # CAP_SETGID this container is granted directly, not through any setuid-bit
44
+ # binary, so both keep working unaffected. The build's own `passwd -l
45
+ # tunnel` step above already ran as root before this point, so it isn't
46
+ # affected either.
47
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
48
+
49
+ EXPOSE 2222
50
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ AUTHORIZED_KEYS_DIR=/run/agent-gateway
5
+ mkdir -p "$AUTHORIZED_KEYS_DIR"
6
+ chmod 755 "$AUTHORIZED_KEYS_DIR"
7
+ # This is a public key, so root ownership and mode 0644 are safe. Keeping it
8
+ # under /run makes the authorization state ephemeral while preserving a
9
+ # read-only image root filesystem.
10
+ install -m 644 /etc/agent/gateway-key.pub "$AUTHORIZED_KEYS_DIR/authorized_keys"
11
+
12
+ HOST_KEY_DIR=/etc/ssh/keys
13
+ mkdir -p "$HOST_KEY_DIR"
14
+ if [ ! -f "$HOST_KEY_DIR/ssh_host_ed25519_key" ]; then
15
+ echo "[entrypoint] Generating gateway SSH host key ..." >&2
16
+ ssh-keygen -q -t ed25519 -f "$HOST_KEY_DIR/ssh_host_ed25519_key" -N ""
17
+ fi
18
+
19
+ source /usr/local/lib/agent/egress-allowlist.sh
20
+ configure_egress_allowlist
21
+
22
+ if [ -n "${CLOUDFLARE_TUNNEL_TOKEN:-}" ]; then
23
+ echo "[entrypoint] Starting cloudflared tunnel (no inbound port needed)." >&2
24
+ cloudflared tunnel run --token "$CLOUDFLARE_TUNNEL_TOKEN" &
25
+ fi
26
+
27
+ mkdir -p /run/sshd
28
+ exec /usr/sbin/sshd -D -e
@@ -0,0 +1,11 @@
1
+ Port 2222
2
+ HostKey /etc/ssh/keys/ssh_host_ed25519_key
3
+ PermitRootLogin no
4
+ PasswordAuthentication no
5
+ KbdInteractiveAuthentication no
6
+ AllowUsers tunnel
7
+ AuthorizedKeysFile /run/agent-gateway/authorized_keys
8
+ AllowTcpForwarding yes
9
+ X11Forwarding no
10
+ AllowAgentForwarding no
11
+ PrintMotd no
@@ -0,0 +1,114 @@
1
+ FROM python:3.12-slim-trixie AS base
2
+
3
+ ARG TARGETARCH
4
+
5
+ # Reuse the hardened workload baseline: egress enforcement, gateway-client
6
+ # support, and the non-root handoff. Node.js and uv remain available for
7
+ # project tooling and optional user-selected packages.
8
+ COPY --from=shared install-workload-base.sh /usr/local/lib/agent/install-workload-base.sh
9
+ RUN TARGETARCH="$TARGETARCH" bash /usr/local/lib/agent/install-workload-base.sh
10
+
11
+ COPY --from=shared install-additional-packages.sh /usr/local/lib/agent/install-additional-packages.sh
12
+ COPY packages-apt.txt /tmp/packages-apt.txt
13
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
14
+ && install_apt_packages /tmp/packages-apt.txt' \
15
+ && rm -f /tmp/packages-apt.txt
16
+
17
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
18
+
19
+ # packages-uv.txt — general Python libraries (not CLI tools; see
20
+ # tools-uv.txt below), installed via `uv pip install --system` into this
21
+ # system Python. Runs as root, before the aider user exists, since it
22
+ # writes into the image's system site-packages rather than $HOME.
23
+ COPY packages-uv.txt /tmp/packages-uv.txt
24
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
25
+ && install_uv_packages /tmp/packages-uv.txt' \
26
+ && rm -f /tmp/packages-uv.txt
27
+
28
+ # Aider 0.86.2 supports Python 3.10-3.12. Pin the CLI so a rebuild does not
29
+ # silently change the agent or its LLM-provider dependency graph. The default
30
+ # package deliberately excludes browser/Playwright extras: installing browser
31
+ # binaries at runtime is incompatible with the read-only-rootfs profile.
32
+ RUN python -m pip install --no-cache-dir 'aider-chat==0.86.2'
33
+
34
+ # The aider account is created here, at a fixed placeholder UID/GID
35
+ # (1000:1000), purely so the root-only installers below (npm, uv tool) have
36
+ # a real non-root account to run as. `final` remaps this account to the
37
+ # operator's actual host UID/GID instead of recreating it, so a rebuild for
38
+ # a different host user reuses everything installed below unchanged rather
39
+ # than reinstalling it over the network.
40
+ RUN groupadd -g 1000 aider \
41
+ && useradd -m -u 1000 -g 1000 -s /bin/bash aider \
42
+ && mkdir -p /workspace \
43
+ && chown -R aider:aider /home/aider /workspace
44
+
45
+ ENV HOME=/home/aider
46
+ ENV npm_config_prefix=/home/aider/.npm-global
47
+ ENV PATH="/home/aider/.local/bin:/home/aider/.npm-global/bin:${PATH}"
48
+
49
+ # $HOME (uv's default cache location) and /workspace are deliberately
50
+ # separate mounts in this containment model, so uv's hardlink/reflink
51
+ # optimisation can never apply between them — declare the copy fallback
52
+ # explicit rather than have uv warn about it on every install.
53
+ ENV UV_LINK_MODE=copy
54
+
55
+ # Aider would otherwise check for updates at startup and may offer to install
56
+ # Playwright dynamically. Both expand egress/write requirements without being
57
+ # necessary for the core terminal workflow. Analytics is disabled explicitly,
58
+ # rather than relying on Aider's prompt-driven opt-in behavior.
59
+ ENV AIDER_CHECK_UPDATE=false
60
+ ENV AIDER_ANALYTICS=false
61
+ ENV AIDER_DISABLE_PLAYWRIGHT=true
62
+
63
+ COPY packages-npm.txt /tmp/packages-npm.txt
64
+ COPY tools-uv.txt /tmp/tools-uv.txt
65
+ RUN gosu aider bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
66
+ && install_npm_packages /tmp/packages-npm.txt \
67
+ && install_uv_tools /tmp/tools-uv.txt' \
68
+ && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt
69
+
70
+ # Everything above this line is UID/GID-independent and layer-cacheable
71
+ # across hosts: the aider account exists at the fixed placeholder UID/GID
72
+ # only so the installers above have a non-root account to run as. From here
73
+ # down, `final` remaps that account to the operator's actual host UID/GID
74
+ # with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base stage
75
+ # above is reused unchanged from cache, and nothing installed above is
76
+ # reinstalled, rather than rebuilding per host user.
77
+ FROM base AS final
78
+ ARG UID=1000
79
+ ARG GID=1000
80
+
81
+ # Only remap when the caller supplied a different UID/GID than the fixed
82
+ # placeholder baked into `base` above — usermod/groupmod plus a chown -R
83
+ # walk over already-installed content is far cheaper than rerunning the
84
+ # npm/uv-tool installs above, but it's skippable entirely for the common
85
+ # case where the caller never overrides UID/GID. Deliberately no -o:
86
+ # without it, usermod/groupmod refuse to reuse an ID already assigned to
87
+ # another account, so a caller can never remap aider onto UID/GID 0 (root)
88
+ # or collide it with another in-image identity — the build fails instead of
89
+ # silently succeeding.
90
+ RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
91
+ groupmod -g "$GID" aider \
92
+ && usermod -u "$UID" -g "$GID" aider \
93
+ && chown -R "$UID":"$GID" /home/aider /workspace; \
94
+ fi
95
+
96
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
97
+ COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
98
+ RUN chmod +x /usr/local/lib/agent/workload-entrypoint.sh
99
+
100
+ # Strip setuid/setgid bits from the base image and every package installed
101
+ # above — required infra (install-workload-base.sh), packages-apt.txt,
102
+ # packages-npm.txt, packages-uv.txt, and tools-uv.txt — run last so nothing
103
+ # installed by any of those is missed. NoNewPrivs=1 (set by every documented
104
+ # run invocation) already makes these bits inert on execve, but that's an
105
+ # argument for stripping them, not against: if any single control ever
106
+ # regresses, a setuid-root `mount` becomes a live escalation primitive. None
107
+ # of these binaries are needed at runtime, so none should ship
108
+ # exploitable-shaped.
109
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
110
+
111
+ WORKDIR /workspace
112
+
113
+ ENTRYPOINT ["/usr/local/lib/agent/workload-entrypoint.sh", "aider", "aider"]
114
+ CMD ["aider"]
@@ -0,0 +1,16 @@
1
+ # Hosts/IPs the container is allowed to reach outbound, one per line.
2
+ # Blank lines and lines starting with '#' are ignored.
3
+ #
4
+ # Accepted forms of each entry:
5
+ # example.com Hostname (resolved via dnsmasq, both A/AAAA)
6
+ # 203.0.113.10 IPv4 address or CIDR (e.g. 203.0.113.0/24)
7
+ # 2001:db8::1 IPv6 address or CIDR
8
+ #
9
+ # Mount this file to /etc/agent/egress-allowlist.txt in the container.
10
+ # A file mount here takes precedence over $AGENT_ALLOWED_EGRESS.
11
+ # If neither is set, all outbound traffic is denied by default.
12
+ #
13
+ # This is only an example. Retain just the API provider(s) you selected and
14
+ # add a source-control, package-registry, or MCP host only when you need it.
15
+ api.anthropic.com
16
+ api.openai.com
@@ -0,0 +1,16 @@
1
+ # Default/optional apt packages installed at build time, one per line.
2
+ # Blank lines and lines starting with '#' are ignored. Edit freely and
3
+ # rebuild the image — this is separate from the Dockerfile's own
4
+ # required-infra apt list (git, iptables, gosu, etc.), which this file
5
+ # cannot remove packages from.
6
+ gh
7
+ jq
8
+ ripgrep
9
+ fd-find
10
+ tree
11
+ unzip
12
+ less
13
+ file
14
+ lsof
15
+ yq
16
+ miller
@@ -0,0 +1,7 @@
1
+ # Optional global npm packages installed at build time (via `npm install -g`),
2
+ # one per line. Blank lines and lines starting with '#' are ignored. Empty by
3
+ # default — there's no "everyone wants this" global npm CLI the way there is
4
+ # for apt utilities.
5
+ #
6
+ # Example:
7
+ # @some-scope/some-cli
@@ -0,0 +1,9 @@
1
+ # Optional Python libraries installed into the system Python at build time
2
+ # (via `uv pip install --system`), one per line. Blank lines and lines
3
+ # starting with '#' are ignored. Empty by default. Unlike tools-uv.txt, these
4
+ # land in the one shared system Python rather than an isolated venv, so
5
+ # they're importable by whatever runs inside the image — a CLI-only package
6
+ # with no importable module of its own belongs in tools-uv.txt instead.
7
+ #
8
+ # Example:
9
+ # langfuse
@@ -0,0 +1,9 @@
1
+ # Optional Python CLI tools installed at build time (via `uv tool install`),
2
+ # one per line, each into its own isolated venv. Blank lines and lines
3
+ # starting with '#' are ignored. Empty by default — there's no "everyone
4
+ # wants this" uv-installed CLI the way there is for apt utilities. A plain
5
+ # importable library (no console-script entry point of its own) belongs in
6
+ # packages-uv.txt instead.
7
+ #
8
+ # Example:
9
+ # ruff
@@ -0,0 +1,171 @@
1
+ FROM python:3.14-slim-trixie AS base
2
+
3
+ ARG TARGETARCH
4
+
5
+ # Shared Node.js, containment, gateway-client, and cloudflared baseline for
6
+ # the first-party Python-and-Node workload images.
7
+ COPY --from=shared install-workload-base.sh /usr/local/lib/agent/install-workload-base.sh
8
+ RUN TARGETARCH="$TARGETARCH" bash /usr/local/lib/agent/install-workload-base.sh
9
+
10
+ # Optional profile-managed organisational CAs. The canonical context carries
11
+ # only a marker; onboarding replaces this directory with the selected file or
12
+ # certificate directory before building. The merged system bundle is then
13
+ # available to curl/git/OpenSSL clients on the read-only runtime rootfs.
14
+ COPY proxy-ca.d/ /tmp/agent-containers-proxy-ca/
15
+ RUN set -eu; \
16
+ mkdir -p /usr/local/share/ca-certificates/custom; \
17
+ found=0; \
18
+ for cert in /tmp/agent-containers-proxy-ca/*; do \
19
+ [ -f "$cert" ] || continue; \
20
+ name="$(basename "$cert")"; \
21
+ [ "$name" = ".keep" ] && continue; \
22
+ case "$name" in *.crt) ;; *) name="${name%.*}.crt" ;; esac; \
23
+ cp "$cert" "/usr/local/share/ca-certificates/custom/$name"; \
24
+ found=1; \
25
+ done; \
26
+ if [ "$found" -eq 1 ]; then update-ca-certificates; fi; \
27
+ rm -rf /tmp/agent-containers-proxy-ca
28
+
29
+ # User-configurable default software (gh, jq, ripgrep, ...) — see
30
+ # packages-apt.txt. Kept as a separate RUN from the baseline block above so
31
+ # the required-infra list stays untouched by editing that file.
32
+ COPY --from=shared install-additional-packages.sh /usr/local/lib/agent/install-additional-packages.sh
33
+ COPY packages-apt.txt /tmp/packages-apt.txt
34
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
35
+ && install_apt_packages /tmp/packages-apt.txt' \
36
+ && rm -f /tmp/packages-apt.txt
37
+
38
+ # uv/uvx (Python package & venv manager), copied in rather than switching
39
+ # the base image — uv manages venvs against this image's existing system
40
+ # Python just as well as against a uv-provisioned one, without disturbing
41
+ # the apt/Node.js setup above.
42
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
43
+
44
+ # packages-uv.txt — general Python libraries (not CLI tools; see
45
+ # tools-uv.txt below), installed via `uv pip install --system` into this
46
+ # system Python. Runs as root, before the claude user exists, since it
47
+ # writes into the image's system site-packages rather than $HOME.
48
+ COPY packages-uv.txt /tmp/packages-uv.txt
49
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
50
+ && install_uv_packages /tmp/packages-uv.txt' \
51
+ && rm -f /tmp/packages-uv.txt
52
+
53
+ RUN npm install -g @anthropic-ai/claude-code
54
+
55
+ # The claude account is created here, at a fixed placeholder UID/GID
56
+ # (1000:1000), purely so the root-only installers below (npm, uv tool,
57
+ # claude plugin) have a real non-root account to run as. `final` remaps
58
+ # this account to the operator's actual host UID/GID instead of recreating
59
+ # it, so a rebuild for a different host user reuses everything installed
60
+ # below unchanged rather than reinstalling it over the network.
61
+ RUN groupadd -g 1000 claude \
62
+ && useradd -m -u 1000 -g 1000 -s /bin/bash claude \
63
+ && mkdir -p /home/claude/.claude /workspace \
64
+ && chown -R claude:claude /home/claude /workspace
65
+
66
+ # gosu switches UID/GID but, unlike a login shell, does not reset $HOME —
67
+ # without this, plugin install (below) and the runtime `claude` process would
68
+ # both try to use root's $HOME instead of /home/claude.
69
+ ENV HOME=/home/claude
70
+
71
+ # npm's default global-install prefix (/usr/local) sits on the read-only
72
+ # rootfs at runtime; repoint it to a location under the claude user's home
73
+ # so `npm install -g` works there instead. This doesn't affect the
74
+ # `npm install -g @anthropic-ai/claude-code` above, which already ran as
75
+ # root before this user/prefix existed. Set via ENV (not `npm config set`,
76
+ # which would only write ~/.npmrc into the image — that file wouldn't reach
77
+ # a volume that already has content the first time it's mounted here, since
78
+ # Docker's volume copy-up only seeds genuinely empty volumes) so the prefix
79
+ # applies regardless of what's already in the mounted $HOME volume.
80
+ ENV npm_config_prefix=/home/claude/.npm-global
81
+ # Home-managed installations intentionally precede profile-managed tools. The
82
+ # latter are image-owned under /opt so a populated home volume cannot hide a
83
+ # rebuilt profile image's requested versions.
84
+ ENV PATH="/home/claude/.local/bin:/home/claude/.npm-global/bin:/opt/agent-tools/bin:/opt/agent-tools/npm/bin:${PATH}"
85
+
86
+ # $HOME (uv's default cache location) and /workspace are deliberately
87
+ # separate mounts in this containment model, so uv's hardlink/reflink
88
+ # optimisation can never apply between them — declare the copy fallback
89
+ # explicit rather than have uv warn about it on every install.
90
+ ENV UV_LINK_MODE=copy
91
+
92
+ # packages-npm.txt / tools-uv.txt — see packages-apt.txt above. Profile-managed
93
+ # tools live outside the persistent home, so changing an image changes the
94
+ # visible managed version even when a populated home volume is mounted.
95
+ COPY packages-npm.txt /tmp/packages-npm.txt
96
+ COPY tools-uv.txt /tmp/tools-uv.txt
97
+ RUN mkdir -p /opt/agent-tools/npm /opt/agent-tools/uv-tools /opt/agent-tools/bin \
98
+ && chown -R claude:claude /opt/agent-tools \
99
+ && gosu claude env \
100
+ npm_config_prefix=/opt/agent-tools/npm \
101
+ UV_TOOL_DIR=/opt/agent-tools/uv-tools \
102
+ UV_TOOL_BIN_DIR=/opt/agent-tools/bin \
103
+ bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
104
+ && install_npm_packages /tmp/packages-npm.txt \
105
+ && install_uv_tools /tmp/tools-uv.txt' \
106
+ && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt
107
+
108
+ COPY plugin-marketplaces.txt /tmp/plugin-marketplaces.txt
109
+ COPY plugins.txt /tmp/plugins.txt
110
+
111
+ # The official marketplace is always available; plugin-marketplaces.txt lists
112
+ # any additional (non-official) marketplaces to add before installing plugins,
113
+ # since plugins.txt entries may reference any of them via "<plugin>@<marketplace>".
114
+ RUN gosu claude claude plugin marketplace add anthropics/claude-plugins-official \
115
+ && while IFS= read -r marketplace; do \
116
+ case "$marketplace" in ''|'#'*) continue ;; esac; \
117
+ gosu claude claude plugin marketplace add "${marketplace}"; \
118
+ done < /tmp/plugin-marketplaces.txt \
119
+ && while IFS= read -r plugin; do \
120
+ case "$plugin" in ''|'#'*) continue ;; esac; \
121
+ gosu claude claude plugin install "${plugin}" --scope user; \
122
+ done < /tmp/plugins.txt \
123
+ && rm /tmp/plugin-marketplaces.txt /tmp/plugins.txt
124
+
125
+ # Everything above this line is UID/GID-independent and layer-cacheable
126
+ # across hosts: the claude account exists at the fixed placeholder UID/GID
127
+ # only so the installers above have a non-root account to run as. From here
128
+ # down, `final` remaps that account to the operator's actual host UID/GID
129
+ # with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base stage
130
+ # above is reused unchanged from cache, and nothing installed above is
131
+ # reinstalled, rather than rebuilding per host user.
132
+ FROM base AS final
133
+ ARG UID=1000
134
+ ARG GID=1000
135
+
136
+ # Only remap when the caller supplied a different UID/GID than the fixed
137
+ # placeholder baked into `base` above — usermod/groupmod plus a chown -R
138
+ # walk over already-installed content is far cheaper than rerunning the
139
+ # npm/uv-tool/plugin installs above, but it's skippable entirely for the
140
+ # common case where the caller never overrides UID/GID. Deliberately no -o:
141
+ # without it, usermod/groupmod refuse to reuse an ID already assigned to
142
+ # another account, so a caller can never remap claude onto UID/GID 0 (root)
143
+ # or collide it with another in-image identity — the build fails instead of
144
+ # silently succeeding.
145
+ RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
146
+ groupmod -g "$GID" claude \
147
+ && usermod -u "$UID" -g "$GID" claude \
148
+ && chown -R "$UID":"$GID" /home/claude /workspace /opt/agent-tools; \
149
+ fi
150
+
151
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
152
+ COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
153
+
154
+ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
155
+ RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
156
+
157
+ # Strip setuid/setgid bits from the base image and every package installed
158
+ # above — required infra (install-workload-base.sh), packages-apt.txt,
159
+ # packages-npm.txt, packages-uv.txt, tools-uv.txt, and installed plugins —
160
+ # run last so nothing installed by any of those is missed. NoNewPrivs=1 (set
161
+ # by every documented run invocation) already makes these bits inert on
162
+ # execve, but that's an argument for stripping them, not against: if any
163
+ # single control ever regresses, a setuid-root `mount` becomes a live
164
+ # escalation primitive. None of these binaries are needed at runtime, so
165
+ # none should ship exploitable-shaped.
166
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
167
+
168
+ WORKDIR /workspace
169
+
170
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
171
+ CMD ["claude"]
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Older images mounted the persistent volume directly at /home/claude/.claude;
5
+ # it's now mounted at /home/claude so uv/npm state under $HOME persists too.
6
+ # A volume from before this change has its old .claude contents sitting at
7
+ # its own root, so /home/claude/.claude won't exist yet — nest them one
8
+ # level down to match where Claude Code still expects them. Both the check
9
+ # and the action run as the claude user (via gosu): /home/claude is
10
+ # 0700-owned by claude, and root lacks CAP_DAC_OVERRIDE under --cap-drop=ALL,
11
+ # so root can't even stat into it — checking as root would misreport
12
+ # "missing" on every single start (not just genuinely legacy volumes) and
13
+ # re-run the sweep repeatedly, corrupting a volume that's already migrated.
14
+ if ! gosu claude test -d /home/claude/.claude; then
15
+ echo "[entrypoint] Migrating legacy claude-home volume layout into ~/.claude ..." >&2
16
+ gosu claude mkdir -p /home/claude/.claude
17
+ gosu claude find /home/claude -mindepth 1 -maxdepth 1 ! -name .claude -exec mv -t /home/claude/.claude -- {} +
18
+ fi
19
+
20
+ # Claude-specific state migration stays here; all containment setup and the
21
+ # privilege drop are shared with the other first-party Node.js workload images.
22
+ exec /usr/local/lib/agent/workload-entrypoint.sh claude claude "$@"
@@ -0,0 +1,14 @@
1
+ # Hosts/IPs the container is allowed to reach outbound, one per line.
2
+ # Blank lines and lines starting with '#' are ignored.
3
+ #
4
+ # Accepted forms of each entry:
5
+ # example.com Hostname (resolved via dnsmasq, both A/AAAA)
6
+ # 203.0.113.10 IPv4 address or CIDR (e.g. 203.0.113.0/24)
7
+ # 2001:db8::1 IPv6 address or CIDR
8
+ #
9
+ # Mount this file to /etc/agent/egress-allowlist.txt in the container.
10
+ # A file mount here takes precedence over $AGENT_ALLOWED_EGRESS.
11
+ # If neither is set, all outbound traffic is denied by default.
12
+ #
13
+ # Example:
14
+ api.anthropic.com
@@ -0,0 +1,7 @@
1
+ {
2
+ "env": {
3
+ "ANTHROPIC_BASE_URL": "https://your-local-model.example.com",
4
+ "ANTHROPIC_AUTH_TOKEN": "replace-with-your-token",
5
+ "ANTHROPIC_MODEL": "your-local-model-name"
6
+ }
7
+ }