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.
- agent_containers/__init__.py +1 -0
- agent_containers/__main__.py +6 -0
- agent_containers/_assets/agent-images/adal/Dockerfile +103 -0
- agent_containers/_assets/agent-images/adal/examples/egress-allowlist.txt +10 -0
- agent_containers/_assets/agent-images/adal/packages-apt.txt +13 -0
- agent_containers/_assets/agent-images/adal/packages-npm.txt +2 -0
- agent_containers/_assets/agent-images/adal/packages-uv.txt +6 -0
- agent_containers/_assets/agent-images/adal/tools-uv.txt +4 -0
- agent_containers/_assets/agent-images/agent-gateway/Dockerfile +50 -0
- agent_containers/_assets/agent-images/agent-gateway/entrypoint.sh +28 -0
- agent_containers/_assets/agent-images/agent-gateway/sshd_config.agent-gateway +11 -0
- agent_containers/_assets/agent-images/aider/Dockerfile +114 -0
- agent_containers/_assets/agent-images/aider/examples/egress-allowlist.txt +16 -0
- agent_containers/_assets/agent-images/aider/packages-apt.txt +16 -0
- agent_containers/_assets/agent-images/aider/packages-npm.txt +7 -0
- agent_containers/_assets/agent-images/aider/packages-uv.txt +9 -0
- agent_containers/_assets/agent-images/aider/tools-uv.txt +9 -0
- agent_containers/_assets/agent-images/claude-code/Dockerfile +171 -0
- agent_containers/_assets/agent-images/claude-code/entrypoint.sh +22 -0
- agent_containers/_assets/agent-images/claude-code/examples/egress-allowlist.txt +14 -0
- agent_containers/_assets/agent-images/claude-code/examples/settings.local-model.json +7 -0
- agent_containers/_assets/agent-images/claude-code/packages-apt.txt +16 -0
- agent_containers/_assets/agent-images/claude-code/packages-npm.txt +7 -0
- agent_containers/_assets/agent-images/claude-code/packages-uv.txt +9 -0
- agent_containers/_assets/agent-images/claude-code/plugin-marketplaces.txt +23 -0
- agent_containers/_assets/agent-images/claude-code/plugins.txt +13 -0
- agent_containers/_assets/agent-images/claude-code/proxy-ca.d/.keep +1 -0
- agent_containers/_assets/agent-images/claude-code/tools-uv.txt +9 -0
- agent_containers/_assets/agent-images/codex/Dockerfile +147 -0
- agent_containers/_assets/agent-images/codex/codex-wrapper.sh +13 -0
- agent_containers/_assets/agent-images/codex/examples/egress-allowlist.txt +10 -0
- agent_containers/_assets/agent-images/codex/packages-apt.txt +13 -0
- agent_containers/_assets/agent-images/codex/packages-npm.txt +2 -0
- agent_containers/_assets/agent-images/codex/packages-uv.txt +6 -0
- agent_containers/_assets/agent-images/codex/plugin-marketplaces.txt +2 -0
- agent_containers/_assets/agent-images/codex/plugins.txt +2 -0
- agent_containers/_assets/agent-images/codex/proxy-ca.d/.keep +1 -0
- agent_containers/_assets/agent-images/codex/tools-uv.txt +4 -0
- agent_containers/_assets/agent-images/hermes/Dockerfile +122 -0
- agent_containers/_assets/agent-images/hermes/entrypoint.sh +49 -0
- agent_containers/_assets/agent-images/hermes/packages-apt.txt +16 -0
- agent_containers/_assets/agent-images/hermes/packages-npm.txt +7 -0
- agent_containers/_assets/agent-images/hermes/packages-uv.txt +9 -0
- agent_containers/_assets/agent-images/hermes/proxy-ca.d/.keep +1 -0
- agent_containers/_assets/agent-images/hermes/tools-uv.txt +9 -0
- agent_containers/_assets/agent-images/kilo-code/Dockerfile +101 -0
- agent_containers/_assets/agent-images/kilo-code/examples/egress-allowlist.txt +7 -0
- agent_containers/_assets/agent-images/kilo-code/packages-apt.txt +5 -0
- agent_containers/_assets/agent-images/kilo-code/packages-npm.txt +1 -0
- agent_containers/_assets/agent-images/kilo-code/packages-uv.txt +4 -0
- agent_containers/_assets/agent-images/kilo-code/tools-uv.txt +4 -0
- agent_containers/_assets/agent-images/opencode/Dockerfile +126 -0
- agent_containers/_assets/agent-images/opencode/examples/egress-allowlist.txt +6 -0
- agent_containers/_assets/agent-images/opencode/packages-apt.txt +13 -0
- agent_containers/_assets/agent-images/opencode/packages-npm.txt +2 -0
- agent_containers/_assets/agent-images/opencode/packages-uv.txt +6 -0
- agent_containers/_assets/agent-images/opencode/proxy-ca.d/.keep +1 -0
- agent_containers/_assets/agent-images/opencode/tools-uv.txt +4 -0
- agent_containers/_assets/agent-images/qwen-code/Dockerfile +104 -0
- agent_containers/_assets/agent-images/qwen-code/examples/egress-allowlist.txt +10 -0
- agent_containers/_assets/agent-images/qwen-code/packages-apt.txt +13 -0
- agent_containers/_assets/agent-images/qwen-code/packages-npm.txt +2 -0
- agent_containers/_assets/agent-images/qwen-code/packages-uv.txt +6 -0
- agent_containers/_assets/agent-images/qwen-code/tools-uv.txt +4 -0
- agent_containers/_assets/agent-images/shared/egress-allowlist.sh +138 -0
- agent_containers/_assets/agent-images/shared/install-additional-packages.sh +79 -0
- agent_containers/_assets/agent-images/shared/install-workload-base.sh +46 -0
- agent_containers/_assets/agent-images/shared/workload-entrypoint.sh +82 -0
- agent_containers/_assets/agent-images/volume-bridge/Dockerfile +37 -0
- agent_containers/_assets/agent-images/volume-bridge/entrypoint.sh +29 -0
- agent_containers/_assets/agent-images/volume-bridge/serve.sh +66 -0
- agent_containers/assets.py +38 -0
- agent_containers/build_context.py +116 -0
- agent_containers/cli.py +212 -0
- agent_containers/creation.py +177 -0
- agent_containers/docker.py +562 -0
- agent_containers/lifecycle.py +325 -0
- agent_containers/planner.py +99 -0
- agent_containers/profile.py +377 -0
- agent_containers/shortcuts.py +159 -0
- agent_containers/state.py +154 -0
- agent_containers-0.1.0.dev0.dist-info/METADATA +195 -0
- agent_containers-0.1.0.dev0.dist-info/RECORD +86 -0
- agent_containers-0.1.0.dev0.dist-info/WHEEL +4 -0
- agent_containers-0.1.0.dev0.dist-info/entry_points.txt +3 -0
- agent_containers-0.1.0.dev0.dist-info/licenses/LICENSE +21 -0
|
@@ -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,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,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,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.
|