-
Notifications
You must be signed in to change notification settings - Fork 6
feat: band-python-kit base image, isolated SDK venv, CA wiring [INT-977] #443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
6654609
15b314d
842c038
538dd84
d3086d9
6ee62f1
339966f
a91b9bb
ae6c9f6
114df82
33d4c98
e9d428c
b110269
8ce0046
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| # Fully-specific tag + manifest-list digest (multi-arch: resolves to the | ||
| # correct linux/amd64 or linux/arm64 image automatically). The tag makes the | ||
| # pin human-legible; the digest is what actually gets resolved — re-pin by | ||
| # passing --build-arg to bump either, but always keep both in sync (the tag | ||
| # is documentation, not an independent fallback). | ||
| ARG PYTHON_BASE_IMAGE=python:3.12.13-slim-trixie@sha256:c3d81d25b3154142b0b42eb1e61300024426268edeb5b5a26dd7ddf64d9daf28 | ||
| ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.9.13@sha256:f07d1bf7b1fb4b983eed2b31320e25a2a76625bdf83d5ff0208fe105d4d8d2f5 | ||
| # Shared by both stages so the venv is built at the exact absolute path it | ||
| # will live at in the runtime image — see UV_PROJECT_ENVIRONMENT below. | ||
| ARG BAND_SDK_HOME=/opt/band | ||
|
|
||
| # Named stage so the pinned uv image can be referenced by a fixed alias below | ||
| # — COPY --from doesn't support ARG expansion directly. | ||
| FROM ${UV_IMAGE} AS uv | ||
|
|
||
| # ── Build stage ─────────────────────────────────────────────────────────── | ||
| FROM ${PYTHON_BASE_IMAGE} AS builder | ||
| ARG BAND_SDK_HOME | ||
|
|
||
| ENV DEBIAN_FRONTEND=noninteractive | ||
|
|
||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| git \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # uv, copied as a pinned binary (no pip install). | ||
| COPY --from=uv /uv /uvx /usr/local/bin/ | ||
|
|
||
| WORKDIR /build | ||
| COPY pyproject.toml uv.lock README.md ./ | ||
| COPY src ./src | ||
|
|
||
| # Prefer HTTPS for GitHub URLs to avoid SSH key requirements in container builds. | ||
| RUN git config --global --add url."https://github.com/".insteadOf "git@github.com:" \ | ||
| && git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/" | ||
|
|
||
| # Strip host-specific [tool.uv.sources] overrides so uv resolves from the registry. | ||
| RUN sed -i '/^\[tool\.uv\.sources\]/,/^\[/{ /^\[tool\.uv\.sources\]/d; /^\[/!d; }' pyproject.toml | ||
|
|
||
| # Core-only by default: this venv is baked once and never touched again. | ||
| # Optionally bake exactly one framework extra in at build time (e.g. | ||
| # `--build-arg SDK_EXTRA=langgraph`) — deliberately one, not a fixed v1 set, | ||
| # since some extras (e.g. crewai vs. parlant/pydantic_ai) conflict and can't | ||
| # resolve into the same venv. Any other framework is the customer's own | ||
| # venv, created at sandbox runtime, isolated from this one. | ||
| ARG SDK_EXTRA="" | ||
| # Build the venv directly at its final runtime path (not e.g. ./.venv, then | ||
| # relocated by COPY below): console-script shebangs bake in an absolute | ||
| # interpreter path at install time, so copying an already-built venv to a | ||
| # different path leaves every entry point (band-acp, band-trigger, ...) | ||
| # pointing at the deleted builder path. | ||
| ENV UV_PROJECT_ENVIRONMENT=${BAND_SDK_HOME}/venv | ||
| RUN if [ -n "$SDK_EXTRA" ]; then \ | ||
| uv sync --locked --no-dev --no-editable --extra "$SDK_EXTRA"; \ | ||
| else \ | ||
| uv sync --locked --no-dev --no-editable; \ | ||
| fi | ||
|
|
||
| # ── Runtime stage ───────────────────────────────────────────────────────── | ||
| FROM ${PYTHON_BASE_IMAGE} | ||
| ARG BAND_SDK_HOME | ||
|
|
||
| ENV DEBIAN_FRONTEND=noninteractive | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| # ca-certificates provides the system trust store that the sandbox's | ||
| # session-generated proxy CA is installed into at runtime. No proxy env vars | ||
| # are set here. SSL_CERT_FILE/REQUESTS_CA_BUNDLE point at that same system | ||
| # bundle (not a narrowed, kit-private CA file) so it's additive, not an | ||
| # override: httpx and requests otherwise trust only their vendored certifi | ||
| # bundle and never see certs update-ca-certificates adds at sandbox runtime. | ||
| # git + openssh-client: band.docker.repo_init (always in the baked venv, part | ||
| # of core band-sdk) shells out to git for clone/checkout/rev-parse and to | ||
| # ssh-keygen for SSH host-key verification — both are needed in the base | ||
| # runtime image because repo initialization runs directly inside it. | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| bash \ | ||
| ca-certificates \ | ||
| git \ | ||
| openssh-client \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ | ||
| REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt | ||
|
|
||
| # Immutable SDK venv, outside the customer's writable workspace ($HOME). Copied | ||
| # from the exact same absolute path it was built at (see UV_PROJECT_ENVIRONMENT | ||
| # in the builder stage) — console-script shebangs bake in an absolute | ||
| # interpreter path, so copying to a *different* path would leave every entry | ||
| # point (band-acp, band-trigger, ...) pointing at the deleted builder path. | ||
| ENV BAND_SDK_HOME=${BAND_SDK_HOME} | ||
| COPY --from=builder ${BAND_SDK_HOME}/venv ${BAND_SDK_HOME}/venv | ||
| # Same digest-pinned uv as the build stage, for the sandbox launcher's locked | ||
| # customer-venv sync (`uv sync --locked`) at runtime. Lives under the | ||
| # read-only SDK home and stays off PATH for the same reason as | ||
| # BAND_SDK_PYTHON: the customer's own tooling must never be shadowed. | ||
| COPY --from=uv /uv ${BAND_SDK_HOME}/bin/uv | ||
| RUN chmod -R a-w ${BAND_SDK_HOME} | ||
|
|
||
| # Fixed, absolute interpreter path for launching the SDK. Deliberately not | ||
| # added to PATH, so it can never shadow the customer's own venv at runtime. | ||
| ENV BAND_SDK_PYTHON=${BAND_SDK_HOME}/venv/bin/python | ||
| # Fixed, absolute path to the pinned uv binary; the launcher must use this | ||
| # and never download or upgrade uv at runtime. | ||
| ENV BAND_SDK_UV=${BAND_SDK_HOME}/bin/uv | ||
|
|
||
| # Fixed system path, deliberately NOT under ${BAND_SDK_HOME}: ENTRYPOINT's | ||
| # exec-form JSON array is one of the few Dockerfile instructions Docker does | ||
| # NOT substitute ARG/ENV into, so a literal path is unavoidable here — keep | ||
| # it independent of the configurable SDK home rather than letting a | ||
| # --build-arg BAND_SDK_HOME override silently break container startup. | ||
| COPY docker/band_python_kit/entrypoint.sh /usr/local/bin/band-entrypoint.sh | ||
| RUN chmod +x /usr/local/bin/band-entrypoint.sh | ||
|
|
||
| RUN useradd -m -u 1000 -s /bin/bash agent | ||
| ENV HOME=/home/agent | ||
| WORKDIR /home/agent | ||
|
|
||
| # Container starts as root: the entrypoint decodes the sandbox's per-session | ||
| # proxy CA (only root can update the system trust store), then drops to the | ||
| # non-root "agent" user via setpriv before handing off to CMD. | ||
| ENTRYPOINT ["/usr/local/bin/band-entrypoint.sh"] | ||
| CMD ["bash"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| # band-python-kit base image | ||
|
|
||
| Base image for the Band Docker Sandboxes kit: a customer's existing framework | ||
| agent (LangGraph, CrewAI, Anthropic, etc.) boots inside a sandbox already | ||
| connected to Band. This image provides the immutable Band SDK layer; the | ||
| customer's own framework lives in a separate venv created at sandbox runtime | ||
| (not part of this image). | ||
|
|
||
| The publishing workflow is a separate release deliverable. This README covers | ||
| building and running the image directly. | ||
|
|
||
| ## Build | ||
|
|
||
| Core SDK only, no framework extras: | ||
|
|
||
| ```bash | ||
| docker build -f docker/band_python_kit/Dockerfile -t band-python-kit . | ||
| ``` | ||
|
|
||
| Bake in exactly one framework extra (e.g. for a kit variant dedicated to a | ||
| single framework): | ||
|
|
||
| ```bash | ||
| docker build -f docker/band_python_kit/Dockerfile \ | ||
| --build-arg SDK_EXTRA=langgraph \ | ||
| -t band-python-kit:langgraph . | ||
| ``` | ||
|
|
||
| `SDK_EXTRA` accepts any single extra from `pyproject.toml`'s | ||
| `[project.optional-dependencies]` (`langgraph`, `anthropic`, `claude_sdk`, | ||
| `crewai`, `pydantic_ai`, ...). Only one at a time: some extras conflict and | ||
| cannot resolve into the same venv (`crewai` vs. `parlant`/`pydantic_ai` — see | ||
| the Dependency Conflicts section of the repo's `CLAUDE.md`). Any framework not | ||
| baked in here is installed into the customer's own venv at sandbox runtime, | ||
| isolated from this one. | ||
|
|
||
| Multi-arch (arm64 + x86_64): | ||
|
|
||
| ```bash | ||
| docker buildx build --platform linux/amd64,linux/arm64 \ | ||
| -f docker/band_python_kit/Dockerfile -t band-python-kit . | ||
| ``` | ||
|
|
||
| ## Image layout | ||
|
|
||
| | Path / env var | What it is | | ||
| |---|---| | ||
| | `$BAND_SDK_HOME` (`/opt/band`) | Root of the baked SDK install. Read-only to every user, including root, after build (`chmod -R a-w`). | | ||
| | `$BAND_SDK_PYTHON` (`/opt/band/venv/bin/python`) | Fixed, absolute interpreter for the Band SDK. **Not on `PATH`** — the SDK venv can never shadow the customer's own venv or interpreter. Invoke the SDK only via this path. | | ||
| | `$BAND_SDK_UV` (`/opt/band/bin/uv`) | The build's digest-pinned `uv`, for the sandbox launcher's locked customer-venv sync at runtime. **Not on `PATH`**, read-only like the rest of `$BAND_SDK_HOME`; never downloaded or upgraded at runtime. | | ||
| | `agent` (uid 1000, `$HOME=/home/agent`) | The non-root user every process ends up running as. Matches the sandbox's `$HOME`-mounted workspace convention. | | ||
|
|
||
| ## CA trust | ||
|
|
||
| Docker Sandboxes generates a per-session proxy CA and exposes it to the | ||
| container as base64 in `PROXY_CA_CERT_B64`. The entrypoint: | ||
|
|
||
| 1. Decodes `PROXY_CA_CERT_B64` (if set) into | ||
| `/usr/local/share/ca-certificates/sandbox-proxy-ca.crt` and runs | ||
| `update-ca-certificates` — this needs root, which is why the container | ||
| starts as root before dropping privileges (see below). | ||
| 2. Relies on `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`, baked in as image `ENV` | ||
| and pointed at the system bundle | ||
| (`/etc/ssl/certs/ca-certificates.crt`) — this is what `httpx` needs, | ||
| since it trusts only its vendored `certifi` bundle by default and | ||
| ignores the system trust store otherwise. `websockets` already defaults | ||
| to `ssl.create_default_context()`, so it needs no extra wiring. | ||
|
|
||
| Both are additive (pointed at the *whole* system bundle, not a narrowed, | ||
| kit-private CA file) — never override these to trust only an internal CA; | ||
| that breaks the credential proxy's own TLS termination path. | ||
|
|
||
| If `PROXY_CA_CERT_B64` is unset (plain allowlisted egress, no credential | ||
| injection), the entrypoint skips the install step entirely and every real, | ||
| publicly-trusted upstream cert still verifies normally. | ||
|
|
||
| ## Privilege drop | ||
|
|
||
| The container's default process starts as **root** — required for the CA | ||
| install step above, which only root can perform. The entrypoint | ||
| (`entrypoint.sh`) always ends by dropping to `agent` (uid 1000) via | ||
| `setpriv` before `exec`-ing the real command: | ||
|
|
||
| ```bash | ||
| exec setpriv --reuid=agent --regid=agent --init-groups -- "$@" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. setpriv assumed present, never explicitly installed (only bash/ca-certificates/git/openssh-client are). It ships in util-linux (Essential in Debian trixie-slim), so present today — but container startup hard-depends on it. Add explicit util-linux to the runtime apt-get for a base-bump-proof, legible dependency. |
||
| ``` | ||
|
|
||
| Nothing meant to run as the sandbox's agent user ever executes as root — | ||
| verify with `docker top <container>`, not `docker exec ... whoami` (a fresh | ||
| `docker exec` process defaults to root regardless of what PID 1 dropped to). | ||
|
|
||
| ## Run | ||
|
|
||
| ```bash | ||
| docker run --rm band-python-kit bash -c '$BAND_SDK_PYTHON -c "import band; print(band.__version__)"' | ||
| ``` | ||
|
|
||
| With a proxy CA (mirrors what a real sandbox session sets): | ||
|
|
||
| ```bash | ||
| docker run --rm -e PROXY_CA_CERT_B64="$(base64 -i proxy-ca.pem)" band-python-kit \ | ||
| bash -c '$BAND_SDK_PYTHON -c "import httpx; print(httpx.get(\"https://your-injected-domain\").status_code)"' | ||
| ``` | ||
|
|
||
| ## Out of scope here | ||
|
|
||
| - **Customer venv creation** — created at sandbox runtime from the | ||
| workspace's own dependency declaration, not part of this image. | ||
| - **`spec.yaml`** (`kind: sandbox`, `caps.network.allow`, credentials) — a | ||
| separate deliverable; this image is what it points at. | ||
| - **`sbx kit validate` / `sbx run --kit`** — requires the `spec.yaml` above | ||
| and a real Docker Sandboxes-capable machine; not runnable in CI (no | ||
| nested virtualization on GitHub-hosted runners). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| # Deliberately thin: reading workspace config, spec.yaml-driven customer venv | ||
| # sync, and agent launch belong to the sandbox launcher, not this image. This | ||
| # stub only wires CA trust and validates the SDK venv before dropping to the | ||
| # non-root runtime user. | ||
|
|
||
| # The proxy CA is session-generated per sandbox, so it can only be installed | ||
| # at container start, not baked into the image. Runs while still root. | ||
| if [[ -n "${PROXY_CA_CERT_B64:-}" ]]; then | ||
| base64 -d <<<"${PROXY_CA_CERT_B64}" > /usr/local/share/ca-certificates/sandbox-proxy-ca.crt | ||
| update-ca-certificates >/dev/null | ||
|
Comment on lines
+12
to
+13
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CA decode has no validation. Invalid base64 already fails loud under set -e; the silent path is a decode that succeeds but isn't PEM → update-ca-certificates skips it → trust silently not wired. Cheap guard: openssl x509 -noout -in and exit 1 on failure. Silent-untrusted is nasty to debug in a sandbox. |
||
| fi | ||
|
|
||
| if [[ ! -x "${BAND_SDK_PYTHON:-}" ]]; then | ||
| echo "[band-python-kit] ERROR: SDK interpreter not found at ${BAND_SDK_PYTHON:-<unset>}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| exec setpriv --reuid=agent --regid=agent --init-groups -- "$@" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sed strip of [tool.uv.sources] is currently a no-op — no such section exists in the committed root pyproject.toml. Legitimate as a guard against a dev's local override leaking into the build, but reads as load-bearing when it isn't. Reword the comment to say "defensive guard; no-op against committed state," or drop it. Failure mode is safe (local source + stale lock → --locked fails loud).