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,5 @@
1
+ # One package name per line. These optional packages install at build time.
2
+ file
3
+ lsof
4
+ yq
5
+ miller
@@ -0,0 +1 @@
1
+ # One npm package specifier per line. These optional packages install at build time.
@@ -0,0 +1,4 @@
1
+ # One uv package specifier per line. These optional Python libraries install
2
+ # at build time via `uv pip install --system` into the system Python, so
3
+ # they're importable by whatever runs inside the image — a CLI-only package
4
+ # with no importable module of its own belongs in tools-uv.txt instead.
@@ -0,0 +1,4 @@
1
+ # One uv CLI tool specifier per line. These optional tools install at build
2
+ # time via `uv tool install`, each into its own isolated venv. A plain
3
+ # importable library (no console-script entry point of its own) belongs in
4
+ # packages-uv.txt instead.
@@ -0,0 +1,126 @@
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; see claude-code/Dockerfile.
11
+ COPY proxy-ca.d/ /tmp/agent-containers-proxy-ca/
12
+ RUN set -eu; \
13
+ mkdir -p /usr/local/share/ca-certificates/custom; \
14
+ found=0; \
15
+ for cert in /tmp/agent-containers-proxy-ca/*; do \
16
+ [ -f "$cert" ] || continue; \
17
+ name="$(basename "$cert")"; \
18
+ [ "$name" = ".keep" ] && continue; \
19
+ case "$name" in *.crt) ;; *) name="${name%.*}.crt" ;; esac; \
20
+ cp "$cert" "/usr/local/share/ca-certificates/custom/$name"; \
21
+ found=1; \
22
+ done; \
23
+ if [ "$found" -eq 1 ]; then update-ca-certificates; fi; \
24
+ rm -rf /tmp/agent-containers-proxy-ca
25
+
26
+ COPY --from=shared install-additional-packages.sh /usr/local/lib/agent/install-additional-packages.sh
27
+ COPY packages-apt.txt /tmp/packages-apt.txt
28
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
29
+ && install_apt_packages /tmp/packages-apt.txt' \
30
+ && rm -f /tmp/packages-apt.txt
31
+
32
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
33
+
34
+ # packages-uv.txt — general Python libraries (not CLI tools; see
35
+ # tools-uv.txt below), installed via `uv pip install --system` into this
36
+ # system Python. Runs as root, before the opencode user exists, since it
37
+ # writes into the image's system site-packages rather than $HOME.
38
+ COPY packages-uv.txt /tmp/packages-uv.txt
39
+ RUN bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
40
+ && install_uv_packages /tmp/packages-uv.txt' \
41
+ && rm -f /tmp/packages-uv.txt
42
+
43
+ RUN npm install -g opencode-ai
44
+
45
+ # The opencode account is created here, at a fixed placeholder UID/GID
46
+ # (1000:1000), purely so the root-only installers below (npm, uv tool) have
47
+ # a real non-root account to run as. `final` remaps this account to the
48
+ # operator's actual host UID/GID instead of recreating it, so a rebuild for
49
+ # a different host user reuses everything installed below unchanged rather
50
+ # than reinstalling it over the network.
51
+ RUN groupadd -g 1000 opencode \
52
+ && useradd -m -u 1000 -g 1000 -s /bin/bash opencode \
53
+ && mkdir -p /home/opencode/.config/opencode /home/opencode/.local/share/opencode /workspace \
54
+ && chown -R opencode:opencode /home/opencode /workspace
55
+
56
+ ENV HOME=/home/opencode
57
+ ENV npm_config_prefix=/home/opencode/.npm-global
58
+ # Home-managed installations intentionally precede profile-managed tools. The
59
+ # latter are image-owned under /opt so a populated home volume cannot hide a
60
+ # rebuilt profile image's requested versions.
61
+ ENV PATH="/home/opencode/.local/bin:/home/opencode/.npm-global/bin:/opt/agent-tools/bin:/opt/agent-tools/npm/bin:${PATH}"
62
+
63
+ # $HOME (uv's default cache location) and /workspace are deliberately
64
+ # separate mounts in this containment model, so uv's hardlink/reflink
65
+ # optimisation can never apply between them — declare the copy fallback
66
+ # explicit rather than have uv warn about it on every install.
67
+ ENV UV_LINK_MODE=copy
68
+
69
+ COPY packages-npm.txt /tmp/packages-npm.txt
70
+ COPY tools-uv.txt /tmp/tools-uv.txt
71
+ RUN mkdir -p /opt/agent-tools/npm /opt/agent-tools/uv-tools /opt/agent-tools/bin \
72
+ && chown -R opencode:opencode /opt/agent-tools \
73
+ && gosu opencode env \
74
+ npm_config_prefix=/opt/agent-tools/npm \
75
+ UV_TOOL_DIR=/opt/agent-tools/uv-tools \
76
+ UV_TOOL_BIN_DIR=/opt/agent-tools/bin \
77
+ bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
78
+ && install_npm_packages /tmp/packages-npm.txt \
79
+ && install_uv_tools /tmp/tools-uv.txt' \
80
+ && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt
81
+
82
+ # Everything above this line is UID/GID-independent and layer-cacheable
83
+ # across hosts: the opencode account exists at the fixed placeholder
84
+ # UID/GID only so the installers above have a non-root account to run as.
85
+ # From here down, `final` remaps that account to the operator's actual host
86
+ # UID/GID with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base
87
+ # stage above is reused unchanged from cache, and nothing installed above
88
+ # is reinstalled, rather than rebuilding per host user.
89
+ FROM base AS final
90
+ ARG UID=1000
91
+ ARG GID=1000
92
+
93
+ # Only remap when the caller supplied a different UID/GID than the fixed
94
+ # placeholder baked into `base` above — usermod/groupmod plus a chown -R
95
+ # walk over already-installed content is far cheaper than rerunning the
96
+ # npm/uv-tool installs above, but it's skippable entirely for the common
97
+ # case where the caller never overrides UID/GID. Deliberately no -o:
98
+ # without it, usermod/groupmod refuse to reuse an ID already assigned to
99
+ # another account, so a caller can never remap opencode onto UID/GID 0
100
+ # (root) or collide it with another in-image identity — the build fails
101
+ # instead of silently succeeding.
102
+ RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
103
+ groupmod -g "$GID" opencode \
104
+ && usermod -u "$UID" -g "$GID" opencode \
105
+ && chown -R "$UID":"$GID" /home/opencode /workspace /opt/agent-tools; \
106
+ fi
107
+
108
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
109
+ COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
110
+ RUN chmod +x /usr/local/lib/agent/workload-entrypoint.sh
111
+
112
+ # Strip setuid/setgid bits from the base image and every package installed
113
+ # above — required infra (install-workload-base.sh), packages-apt.txt,
114
+ # packages-npm.txt, packages-uv.txt, and tools-uv.txt — run last so nothing
115
+ # installed by any of those is missed. NoNewPrivs=1 (set by every documented
116
+ # run invocation) already makes these bits inert on execve, but that's an
117
+ # argument for stripping them, not against: if any single control ever
118
+ # regresses, a setuid-root `mount` becomes a live escalation primitive. None
119
+ # of these binaries are needed at runtime, so none should ship
120
+ # exploitable-shaped.
121
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
122
+
123
+ WORKDIR /workspace
124
+
125
+ ENTRYPOINT ["/usr/local/lib/agent/workload-entrypoint.sh", "opencode", "opencode"]
126
+ CMD ["opencode"]
@@ -0,0 +1,6 @@
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
+ opencode.ai
6
+ models.dev
@@ -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 @@
1
+ # Profile-specific CA certificates are injected into this directory by the onboarding CLI.
@@ -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,104 @@
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 qwen 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
+ RUN npm install -g @qwen-code/qwen-code
28
+
29
+ # The qwen account is created here, at a fixed placeholder UID/GID
30
+ # (1000:1000), purely so the root-only installers below (npm, uv tool) have
31
+ # a real non-root account to run as. `final` remaps this account to the
32
+ # operator's actual host UID/GID instead of recreating it, so a rebuild for
33
+ # a different host user reuses everything installed below unchanged rather
34
+ # than reinstalling it over the network.
35
+ RUN groupadd -g 1000 qwen \
36
+ && useradd -m -u 1000 -g 1000 -s /bin/bash qwen \
37
+ && mkdir -p /home/qwen/.qwen /workspace \
38
+ && chown -R qwen:qwen /home/qwen /workspace
39
+
40
+ ENV HOME=/home/qwen
41
+ ENV npm_config_prefix=/home/qwen/.npm-global
42
+ ENV PATH="/home/qwen/.local/bin:/home/qwen/.npm-global/bin:${PATH}"
43
+
44
+ # $HOME (uv's default cache location) and /workspace are deliberately
45
+ # separate mounts in this containment model, so uv's hardlink/reflink
46
+ # optimisation can never apply between them — declare the copy fallback
47
+ # explicit rather than have uv warn about it on every install.
48
+ ENV UV_LINK_MODE=copy
49
+ # The outer container is the sandbox. Never let Qwen Code start a nested
50
+ # Docker/Podman sandbox, which would require access to a container runtime.
51
+ ENV QWEN_SANDBOX=false
52
+
53
+ COPY packages-npm.txt /tmp/packages-npm.txt
54
+ COPY tools-uv.txt /tmp/tools-uv.txt
55
+ RUN gosu qwen bash -c '. /usr/local/lib/agent/install-additional-packages.sh \
56
+ && install_npm_packages /tmp/packages-npm.txt \
57
+ && install_uv_tools /tmp/tools-uv.txt' \
58
+ && rm -f /tmp/packages-npm.txt /tmp/tools-uv.txt
59
+
60
+ # Everything above this line is UID/GID-independent and layer-cacheable
61
+ # across hosts: the qwen account exists at the fixed placeholder UID/GID
62
+ # only so the installers above have a non-root account to run as. From here
63
+ # down, `final` remaps that account to the operator's actual host UID/GID
64
+ # with --build-arg UID=$(id -u) --build-arg GID=$(id -g) — the base stage
65
+ # above is reused unchanged from cache, and nothing installed above is
66
+ # reinstalled, rather than rebuilding per host user.
67
+ FROM base AS final
68
+ ARG UID=1000
69
+ ARG GID=1000
70
+
71
+ # Only remap when the caller supplied a different UID/GID than the fixed
72
+ # placeholder baked into `base` above — usermod/groupmod plus a chown -R
73
+ # walk over already-installed content is far cheaper than rerunning the
74
+ # npm/uv-tool installs above, but it's skippable entirely for the common
75
+ # case where the caller never overrides UID/GID. Deliberately no -o:
76
+ # without it, usermod/groupmod refuse to reuse an ID already assigned to
77
+ # another account, so a caller can never remap qwen onto UID/GID 0 (root)
78
+ # or collide it with another in-image identity — the build fails instead of
79
+ # silently succeeding.
80
+ RUN if [ "$UID" != "1000" ] || [ "$GID" != "1000" ]; then \
81
+ groupmod -g "$GID" qwen \
82
+ && usermod -u "$UID" -g "$GID" qwen \
83
+ && chown -R "$UID":"$GID" /home/qwen /workspace; \
84
+ fi
85
+
86
+ COPY --from=shared egress-allowlist.sh /usr/local/lib/agent/egress-allowlist.sh
87
+ COPY --from=shared workload-entrypoint.sh /usr/local/lib/agent/workload-entrypoint.sh
88
+ RUN chmod +x /usr/local/lib/agent/workload-entrypoint.sh
89
+
90
+ # Strip setuid/setgid bits from the base image and every package installed
91
+ # above — required infra (install-workload-base.sh), packages-apt.txt,
92
+ # packages-npm.txt, packages-uv.txt, and tools-uv.txt — run last so nothing
93
+ # installed by any of those is missed. NoNewPrivs=1 (set by every documented
94
+ # run invocation) already makes these bits inert on execve, but that's an
95
+ # argument for stripping them, not against: if any single control ever
96
+ # regresses, a setuid-root `mount` becomes a live escalation primitive. None
97
+ # of these binaries are needed at runtime, so none should ship
98
+ # exploitable-shaped.
99
+ RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + || true
100
+
101
+ WORKDIR /workspace
102
+
103
+ ENTRYPOINT ["/usr/local/lib/agent/workload-entrypoint.sh", "qwen", "qwen"]
104
+ CMD ["qwen"]
@@ -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
+ # These are the DashScope-compatible official Qwen API endpoints. Use only the
7
+ # endpoint appropriate for your account, and add every separately configured
8
+ # model provider, MCP server, source-control host, or package registry.
9
+ dashscope.aliyuncs.com
10
+ dashscope-intl.aliyuncs.com
@@ -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,138 @@
1
+ #!/bin/bash
2
+ # Shared by the workload and agent-gateway entrypoints. Configures this
3
+ # container's own OUTPUT chain to allow only the hosts/IPs in AGENT_ALLOWED_EGRESS /
4
+ # /etc/agent/egress-allowlist.txt (file mount takes precedence over the env
5
+ # var), defaulting to deny-all when neither is set. Must be sourced and
6
+ # called as root, before any privilege drop, since it installs iptables
7
+ # rules and starts dnsmasq.
8
+
9
+ configure_egress_allowlist() {
10
+ local allowlist_file="/etc/agent/egress-allowlist.txt"
11
+ local ipset_name="agent_allowed"
12
+ local ipset6_name="agent_allowed6"
13
+ local -a allowlist
14
+
15
+ # File mount takes precedence over the env var; neither set means deny-all.
16
+ if [ -f "$allowlist_file" ]; then
17
+ mapfile -t allowlist < <(grep -v '^\s*#' "$allowlist_file" | grep -v '^\s*$')
18
+ elif [ -n "${AGENT_ALLOWED_EGRESS:-}" ]; then
19
+ IFS=',' read -ra allowlist <<< "$AGENT_ALLOWED_EGRESS"
20
+ else
21
+ allowlist=()
22
+ fi
23
+
24
+ _egress_is_ipv4() { [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}(/[0-9]{1,2})?$ ]]; }
25
+ _egress_is_ipv6() { [[ "$1" == *:* ]]; }
26
+
27
+ if [ "${#allowlist[@]}" -eq 1 ] && [ "${allowlist[0]}" = "*" ]; then
28
+ echo "[egress-allowlist] AGENT_ALLOWED_EGRESS=* — no egress restrictions applied (IPv4 and IPv6)." >&2
29
+ return 0
30
+ fi
31
+
32
+ if [ "${#allowlist[@]}" -eq 0 ]; then
33
+ echo "[egress-allowlist] No allowlist configured (\$AGENT_ALLOWED_EGRESS unset, no $allowlist_file mount)." >&2
34
+ echo "[egress-allowlist] Defaulting to deny-all outbound traffic (IPv4 and IPv6). Set AGENT_ALLOWED_EGRESS=host1,host2 (or '*' for unrestricted) to change this." >&2
35
+ iptables -P OUTPUT DROP
36
+ iptables -A OUTPUT -o lo -j ACCEPT
37
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
38
+ ip6tables -P OUTPUT DROP
39
+ ip6tables -A OUTPUT -o lo -j ACCEPT
40
+ ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
41
+ return 0
42
+ fi
43
+
44
+ echo "[egress-allowlist] Restricting egress to: ${allowlist[*]}" >&2
45
+
46
+ # Preserve whatever resolvers were in place before we take over DNS.
47
+ local upstream_dns
48
+ upstream_dns=$(grep '^nameserver' /etc/resolv.conf | awk '{print $2}')
49
+
50
+ ipset create "$ipset_name" hash:ip family inet -exist
51
+ ipset create "$ipset6_name" hash:ip family inet6 -exist
52
+
53
+ # /tmp rather than /etc: entrypoint runs before rootfs is guaranteed
54
+ # writable (a --read-only container only gets /tmp and /run as tmpfs).
55
+ local dnsmasq_conf=/tmp/dnsmasq.agent.conf
56
+ {
57
+ echo "no-resolv"
58
+ echo "listen-address=127.0.0.1"
59
+ echo "bind-interfaces"
60
+ # Pin dnsmasq to a known, unprivileged UID so the NAT rules below can
61
+ # tell its own upstream forwarding apart from every other process's
62
+ # DNS traffic (both otherwise share the same destination: $upstream_dns).
63
+ echo "user=dnsmasq"
64
+ # Deliberately no unqualified "server=$ns" default here. Only the
65
+ # per-domain "server=/entry/$ns" lines added below (one per
66
+ # allowlisted name) are allowed to forward upstream; a query for
67
+ # anything else matches no server line, so dnsmasq answers it
68
+ # locally (NXDOMAIN) instead of forwarding it. Without this, every
69
+ # domain — allowlisted or not — would resolve via the blanket
70
+ # default, and only the *connection* to the resolved address would
71
+ # be filtered, leaving DNS itself as an unfiltered exfiltration
72
+ # channel (arbitrary data encoded in queried names, or in the
73
+ # queried names of an attacker-controlled zone, still reaches the
74
+ # upstream resolver even though the matching IP egress is blocked).
75
+ } > "$dnsmasq_conf"
76
+
77
+ local entry
78
+ for entry in "${allowlist[@]}"; do
79
+ entry="$(echo "$entry" | xargs)" # trim whitespace
80
+ if _egress_is_ipv6 "$entry"; then
81
+ ipset add "$ipset6_name" "$entry" -exist
82
+ elif _egress_is_ipv4 "$entry"; then
83
+ ipset add "$ipset_name" "$entry" -exist
84
+ else
85
+ # dnsmasq routes A records into the first set and AAAA records
86
+ # into the second, based on each set's own address family.
87
+ echo "ipset=/$entry/$ipset_name,$ipset6_name" >> "$dnsmasq_conf"
88
+ local ns
89
+ for ns in $upstream_dns; do
90
+ echo "server=/$entry/$ns" >> "$dnsmasq_conf"
91
+ done
92
+ fi
93
+ done
94
+
95
+ dnsmasq --conf-file="$dnsmasq_conf"
96
+
97
+ iptables -P OUTPUT DROP
98
+ iptables -A OUTPUT -d 127.0.0.0/8 -j ACCEPT
99
+ iptables -A OUTPUT -o lo -j ACCEPT
100
+ iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
101
+ ip6tables -P OUTPUT DROP
102
+ ip6tables -A OUTPUT -d ::1/128 -j ACCEPT
103
+ ip6tables -A OUTPUT -o lo -j ACCEPT
104
+ ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
105
+ for ns in $upstream_dns; do
106
+ if _egress_is_ipv6 "$ns"; then
107
+ ip6tables -A OUTPUT -p udp -d "$ns" --dport 53 -j ACCEPT
108
+ ip6tables -A OUTPUT -p tcp -d "$ns" --dport 53 -j ACCEPT
109
+ else
110
+ iptables -A OUTPUT -p udp -d "$ns" --dport 53 -j ACCEPT
111
+ iptables -A OUTPUT -p tcp -d "$ns" --dport 53 -j ACCEPT
112
+ fi
113
+ done
114
+ iptables -A OUTPUT -m set --match-set "$ipset_name" dst -j ACCEPT
115
+ ip6tables -A OUTPUT -m set --match-set "$ipset6_name" dst -j ACCEPT
116
+
117
+ # Force every process's DNS traffic to the local filtering dnsmasq
118
+ # instance via NAT instead of rewriting /etc/resolv.conf, which isn't
119
+ # writable under --read-only. dnsmasq's own upstream forwarding (to the
120
+ # same $upstream_dns) must not be redirected back to itself, so it's
121
+ # excluded by UID via a negated owner match on the same rule, rather than
122
+ # a separate RETURN rule ahead of a blanket REDIRECT — a plain append
123
+ # (-A) here loses a race on Docker user-defined networks: Docker inserts
124
+ # its own embedded-DNS NAT rule (destination 127.0.0.11) ahead of
125
+ # whatever this script appends, and that rule is a terminating DNAT, so
126
+ # a RETURN-then-REDIRECT pair appended afterward never even runs for
127
+ # traffic to 127.0.0.11 — dnsmasq is silently bypassed, its ipset never
128
+ # gets populated, and domain-based allowlist entries stay permanently
129
+ # (and confusingly) blocked. Inserting (-I ... 1) a single rule with a
130
+ # negated owner match wins that race: dnsmasq's own query still falls
131
+ # through untouched to Docker's rule below it, while every other
132
+ # process's query is redirected to the local dnsmasq before Docker's
133
+ # rule ever sees it.
134
+ iptables -t nat -I OUTPUT 1 -p tcp --dport 53 -m owner ! --uid-owner dnsmasq -j REDIRECT --to-ports 53
135
+ iptables -t nat -I OUTPUT 1 -p udp --dport 53 -m owner ! --uid-owner dnsmasq -j REDIRECT --to-ports 53
136
+ ip6tables -t nat -I OUTPUT 1 -p tcp --dport 53 -m owner ! --uid-owner dnsmasq -j REDIRECT --to-ports 53
137
+ ip6tables -t nat -I OUTPUT 1 -p udp --dport 53 -m owner ! --uid-owner dnsmasq -j REDIRECT --to-ports 53
138
+ }
@@ -0,0 +1,79 @@
1
+ #!/bin/bash
2
+ # Shared support for installing additional packages in workload-image
3
+ # Dockerfiles. Build-time only, unlike
4
+ # egress-allowlist.sh: invoked directly during `docker build`, never sourced
5
+ # at runtime. Installs each image's
6
+ # user-configurable default software from plain package-list files (one
7
+ # entry per line, '#' comments, blank lines ignored) — same convention as
8
+ # plugins.txt. Each function no-ops if its file is missing or has no
9
+ # non-comment entries, so an image can ship empty lists at zero cost.
10
+ #
11
+ # Two distinct uv-backed installers exist because `uv tool install` and
12
+ # `uv pip install --system` put packages in fundamentally incompatible
13
+ # places: install_uv_tools gives each entry its own isolated venv (only that
14
+ # entry's own console-script wrapper ends up on PATH — nothing importable
15
+ # lands anywhere shared), while install_uv_packages installs into the one
16
+ # shared system Python so entries can `import` each other and whatever else
17
+ # runs in that interpreter. A library with no console-script entry point
18
+ # (e.g. a plain SDK) has no wrapper to run in isolation, so it belongs in
19
+ # packages-uv.txt/install_uv_packages, not tools-uv.txt/install_uv_tools.
20
+
21
+ # Inherited by whatever sources this file, so a failed apt-get/npm/uv install
22
+ # aborts the build immediately instead of being masked by a later command
23
+ # (e.g. `rm -rf /var/lib/apt/lists/*`) that succeeds regardless.
24
+ set -e
25
+
26
+ install_apt_packages() {
27
+ local file="$1"
28
+ [ -s "$file" ] || return 0
29
+ local -a packages
30
+ mapfile -t packages < <(grep -v '^[[:space:]]*#' "$file" \
31
+ | grep -v '^[[:space:]]*$' \
32
+ | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
33
+ [ "${#packages[@]}" -eq 0 ] && return 0
34
+ apt-get update
35
+ apt-get install -y --no-install-recommends "${packages[@]}"
36
+ rm -rf /var/lib/apt/lists/*
37
+ }
38
+
39
+ install_npm_packages() {
40
+ local file="$1"
41
+ [ -s "$file" ] || return 0
42
+ local line package
43
+ while IFS= read -r line; do
44
+ # sed, not xargs, to trim -- xargs parses its input for shell-style
45
+ # quoting, which breaks on ordinary text like "there's" or "'#'" in
46
+ # this file's own comment lines.
47
+ package="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
48
+ case "$package" in ''|'#'*) continue ;; esac
49
+ npm install -g "$package"
50
+ done < "$file"
51
+ }
52
+
53
+ install_uv_tools() {
54
+ local file="$1"
55
+ [ -s "$file" ] || return 0
56
+ local line package
57
+ # uv doesn't batch multi-package installs the way apt/npm do — each
58
+ # package gets its own isolated venv, so this must be one call per line.
59
+ while IFS= read -r line; do
60
+ package="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
61
+ case "$package" in ''|'#'*) continue ;; esac
62
+ uv tool install "$package"
63
+ done < "$file"
64
+ }
65
+
66
+ install_uv_packages() {
67
+ local file="$1"
68
+ [ -s "$file" ] || return 0
69
+ local -a packages
70
+ mapfile -t packages < <(grep -v '^[[:space:]]*#' "$file" \
71
+ | grep -v '^[[:space:]]*$' \
72
+ | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
73
+ [ "${#packages[@]}" -eq 0 ] && return 0
74
+ # --system, unlike uv tool install: lands in the one shared system Python
75
+ # so entries can import each other and whatever else runs in that
76
+ # interpreter. Batched in one resolve/install, same as apt/npm, since
77
+ # there's no per-entry isolation to force one-call-per-line here.
78
+ uv pip install --system "${packages[@]}"
79
+ }
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Shared build-time baseline for the first-party Python-and-Node workload
3
+ # images: adal, claude-code, codex, kilo-code, opencode, and qwen-code. Keep this
4
+ # separate from install-additional-packages.sh: this file owns required
5
+ # containment and runtime infrastructure, whereas that script consumes each
6
+ # image's optional user-selected package lists.
7
+
8
+ set -euo pipefail
9
+
10
+ : "${TARGETARCH:?TARGETARCH must be set by the Dockerfile}"
11
+
12
+ # Node.js installs each image's CLI. The remaining packages support the
13
+ # in-container egress allowlist, the optional gateway tunnel, and gosu's
14
+ # privilege drop after root installs those firewall rules.
15
+ apt-get update
16
+ apt-get upgrade -y
17
+ apt-get install -y --no-install-recommends \
18
+ ca-certificates \
19
+ curl \
20
+ git \
21
+ gnupg \
22
+ iptables \
23
+ ipset \
24
+ dnsmasq \
25
+ dnsutils \
26
+ gosu \
27
+ openssh-client \
28
+ sshuttle
29
+ curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
30
+ apt-get install -y --no-install-recommends nodejs
31
+ rm -rf /var/lib/apt/lists/*
32
+
33
+ # cloudflared is only invoked when AGENT_GATEWAY_ACCESS_HOSTNAME is set, but
34
+ # installing it in every workload keeps the gateway-client contract uniform.
35
+ # TARGETARCH is supplied automatically by Buildx and matches Cloudflare's
36
+ # release-asset naming (amd64/arm64/386/...).
37
+ curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${TARGETARCH}" \
38
+ -o /usr/local/bin/cloudflared
39
+ chmod +x /usr/local/bin/cloudflared
40
+
41
+ # Deliberately no setuid/setgid strip here: this script runs before each
42
+ # consuming Dockerfile's optional packages-apt.txt install, so stripping at
43
+ # this point would miss any setuid/setgid binary a user-added apt package
44
+ # ships. Each Dockerfile that sources this script instead runs its own strip
45
+ # as its last build step, after every package list (required and optional)
46
+ # has been installed. See claude-code/Dockerfile for the reference comment.