Skip to content

feat: band-python-kit base image, isolated SDK venv, CA wiring [INT-977]#443

Open
AlexanderZ-Band wants to merge 14 commits into
devfrom
feat/band-python-kit-base-image-uv-isolated-sdk-venv-ca-INT-977
Open

feat: band-python-kit base image, isolated SDK venv, CA wiring [INT-977]#443
AlexanderZ-Band wants to merge 14 commits into
devfrom
feat/band-python-kit-base-image-uv-isolated-sdk-venv-ca-INT-977

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a multi-stage, digest-pinned python:3.12 image for arm64 and x86_64 under docker/band_python_kit/, with the Python and uv images exposed as build arguments.
  • Builds the Band SDK from the repository lockfile into an isolated, read-only venv at its final runtime path. $BAND_SDK_PYTHON exposes the absolute interpreter without putting the venv on PATH, keeping customer dependencies separate.
  • Supports core-only builds by default and one optional framework extra through SDK_EXTRA, avoiding known cross-extra dependency conflicts.
  • Installs the per-sandbox PROXY_CA_CERT_B64 into the system trust store, exports SSL_CERT_FILE and REQUESTS_CA_BUNDLE, then drops from root to the non-root agent user before launching the requested command.
  • Includes runtime git and openssh-client support required by band.docker.repo_init.
  • Adds reusable, opt-in Docker test infrastructure plus isolation and live-platform scenarios, gated by DOCKER_TESTS_ENABLED (and E2E_TESTS_ENABLED for the live test).
  • Adds the Linux arm64 and Windows AMD64 uv lock environments without changing resolved package versions.
  • Documents the image in docker/band_python_kit/README.md and surfaces it from the repository's main README.

Notable reliability fixes

  • Builds the SDK venv directly at its final path so installed console-script shebangs remain valid.
  • Keeps the OCI entrypoint at /usr/local/bin/band-entrypoint.sh, independent of configurable BAND_SDK_HOME.
  • Gives both Docker scenarios the full cold-build timeout budget from the shared Docker toolkit.
  • The live fixture relies on the SDK's startup room reconciliation for messages sent during initial WebSocket subscription; this behavior was verified with an injected pre-connect delay.

Verification

  • Default core-only Docker build and container startup
  • SDK_EXTRA build and dependency isolation from a customer venv pinned to httpx==0.13.3
  • BAND_SDK_HOME override builds and starts successfully
  • band-acp and band-trigger console scripts use the final venv interpreter
  • Multi-architecture build for linux/amd64 and linux/arm64
  • Synthetic proxy CA trusted automatically by httpx; public TLS remains valid
  • Entrypoint drops to uid 1000 (agent) with and without proxy CA installation
  • Containerized core SDK agent replies over the live Band platform
  • Full local unit suite: 3,798 passed
  • Main README markdown-doc test: 15 passed
  • Required GitHub CI checks pass

Out of scope

  • Customer venv creation, spec.yaml, sbx kit validate, and sbx run --kit are separate launcher and kit-spec deliverables.
  • Publishing ghcr.io/thenvoi/band-python-kit and catalog integration are separate release deliverables.
  • Docker Sandbox tests require nested virtualization and remain developer-local; standard GitHub-hosted runners cannot provide the required environment.

@linear-code

linear-code Bot commented Jul 16, 2026

Copy link
Copy Markdown

INT-977

Multi-stage, digest-pinned, multi-arch (arm64+x86_64) Dockerfile at
docker/band_python_kit/: immutable SDK venv baked at build time outside
the customer's writable workspace, launched only via a fixed absolute
interpreter path so it never collides with the customer's own venv.

Entrypoint decodes the sandbox's per-session PROXY_CA_CERT_B64 into the
system trust store and drops from root to a non-root user via setpriv
before exec. SSL_CERT_FILE/REQUESTS_CA_BUNDLE point at the same system
bundle so certifi-first clients (httpx) see it too, verified against a
synthetic proxy CA end-to-end.

Core deps only by default; an optional SDK_EXTRA build arg bakes in
exactly one framework extra, since some extras (crewai vs.
parlant/pydantic_ai) can't coexist in one venv.

Includes a usage README covering build options, image layout, the CA
trust model, and the privilege-drop mechanism.
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/e2e.yml
Comment on lines +80 to +155
name: resolve lanes
runs-on: ubuntu-latest
outputs:
lanes: ${{ steps.gen.outputs.lanes }}
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up E2E environment
uses: ./.github/actions/setup-e2e

# The registry import is framework-free (band.adapters is lazy) and pytest-free,
# so the default extra is enough to resolve the lane partition.
- name: Install dependencies
run: uv sync --extra dev

- name: Emit lane matrix from the registry
id: gen
env:
# The dispatch `lane` input; defaults to 'all' (the full matrix).
SELECTED_LANE: ${{ github.event.inputs.lane || 'all' }}
# The dispatch `os` input; defaults to 'all' (Linux + Windows).
SELECTED_OS: ${{ github.event.inputs.os || 'all' }}
run: |
uv run python - <<'PY' >> "$GITHUB_OUTPUT"
import json
import os
from tests.e2e.baseline.toolkit.adapters import (
assert_registry_covers_discovered,
)
from tests.e2e.baseline.toolkit.ci_lanes import (
assert_every_adapter_has_a_ci_home,
assert_workflow_lane_gates_known,
ci_lanes,
)
# Fail before any test runs if the registry or lane partition has drifted.
assert_registry_covers_discovered()
assert_every_adapter_has_a_ci_home()
# Fail if a `matrix.lane ==` gate below names a lane the registry no longer
# emits (its step would silently never run).
assert_workflow_lane_gates_known()
lanes = list(ci_lanes())
# The registry stays authoritative: a chosen lane must be one it emits, so
# a stale dropdown option fails loudly here instead of running nothing.
selected = os.environ.get("SELECTED_LANE") or "all"
known = {lane.id for lane in lanes}
if selected != "all" and selected not in known:
raise SystemExit(
f"Requested lane {selected!r} is not one the registry emits "
f"(known: {sorted(known)}). Pick 'all' or a known lane."
)
# OS is a workflow concern (runner image), not a registry fact: every lane
# runs on each selected OS. `runner` is the runs-on image; `os` is the short
# id surfaced in the job name.
runners = {"ubuntu": "ubuntu-latest", "windows": "windows-latest"}
selected_os = os.environ.get("SELECTED_OS") or "all"
if selected_os != "all" and selected_os not in runners:
raise SystemExit(
f"Requested os {selected_os!r} is not known "
f"(known: {sorted(runners)}). Pick 'all', 'ubuntu', or 'windows'."
)
oses = list(runners) if selected_os == "all" else [selected_os]
# A lane that can only run on Linux (its backend has no Windows story —
# ``LINUX_ONLY_LANES``, surfaced as ``lane.linux_only``) contributes no
# windows cell, so a full-matrix dispatch never emits a cell that can't run.
include = [
{"lane": lane.id, "extra": lane.extra, "os": osid, "runner": runners[osid]}
for lane in lanes
if selected == "all" or lane.id == selected
for osid in oses
if not (osid == "windows" and lane.linux_only)
]
print("lanes=" + json.dumps({"include": include}))
PY

e2e:
Comment thread .github/workflows/e2e.yml
Comment on lines +156 to +291
name: e2e (${{ matrix.lane }} / ${{ matrix.os }})
needs: lanes
runs-on: ${{ matrix.runner }}
# Per-(lane, OS) concurrency: a new dispatch cancels only the matching
# lane+OS still running, never the whole matrix — so one lane can be
# re-run in isolation. (See the note above the jobs block.)
concurrency:
group: e2e-baseline-${{ matrix.lane }}-${{ matrix.os }}
cancel-in-progress: true
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.lanes.outputs.lanes) }}
# Use bash on every OS so the setup scripts and uv commands run identically
# (Git Bash ships on the windows-latest runner).
defaults:
run:
shell: bash
env:
E2E_TESTS_ENABLED: "true"
# Force UTF-8 for stdout/stderr. On the windows-latest runner Python defaults
# to the cp1252 console codec, so a framework that logs emoji (e.g. CrewAI's
# event bus: 🤖/🔧/✅) raises UnicodeEncodeError mid-turn and fails the test.
# No-op on Linux (already UTF-8).
PYTHONUTF8: "1"
PYTHONIOENCODING: "utf-8"
# Turn budget (E2E_TIMEOUT) is not set here: it defaults to 120s in the e2e
# settings, and slow frameworks add headroom per-test via
# @pytest.mark.timeout(extra=...). See tests/e2e/baseline/conftest.py.
# The registry scopes which adapters run in this lane.
BAND_E2E_LANE: ${{ matrix.lane }}
# The dev-crewai venv lacks the other frameworks; tolerate missing framework
# configs there instead of failing fast (mirrors ci.yml's crewai job).
BAND_ALLOW_MISSING_FRAMEWORKS: ${{ matrix.extra == 'dev-crewai' && '1' || '0' }}
# Secrets carry an E2E_ prefix in GitHub (so they're clearly scoped to this
# workflow), but the code reads the unprefixed names — the mapping happens
# here: the env var keeps its code-expected name, the secret is prefixed.
# Band platform (the live target + the driver/observer user key).
BAND_REST_URL: ${{ secrets.E2E_BAND_REST_URL }}
BAND_WS_URL: ${{ secrets.E2E_BAND_WS_URL }}
BAND_API_KEY_USER: ${{ secrets.E2E_BAND_API_KEY_USER }}
# Optional second user key, for baseline smokes exercising two-user interaction.
BAND_API_KEY_USER_2: ${{ secrets.E2E_BAND_API_KEY_USER_2 }}
# Model providers (a lane simply ignores a key it has no adapter for).
ANTHROPIC_API_KEY: ${{ secrets.E2E_ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.E2E_OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.E2E_GOOGLE_API_KEY }}
# A PAT from a GitHub account with Copilot entitlement — NOT the ambient
# secrets.GITHUB_TOKEN (a repo-scoped installation token with no such
# entitlement). The copilot_sdk adapter (in the `core` lane) needs it;
# exposed to every lane's job env so `core` picks it up.
GITHUB_TOKEN: ${{ secrets.E2E_GITHUB_TOKEN }}
# Per-lane scorecard: each lane writes its own adapter×test slice (pass/fail for
# its cells, skip for out-of-lane cells, N/A + reason for exclusions). The final
# `scorecard` job merges them. See tests/e2e/baseline/scorecard.py.
BAND_E2E_SCORECARD_JSON: artifacts/scorecard-${{ matrix.lane }}-${{ matrix.os }}.json

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up E2E environment
uses: ./.github/actions/setup-e2e

# --- Backend setup: each step is gated to the lane whose server/CLI it stands
# up (the `backends` lane co-runs codex + opencode; letta is its own lane). ---

- name: Set up Node (codex, opencode)
if: matrix.lane == 'backends'
uses: actions/setup-node@v4
with:
node-version: '20'

# Each setup script installs its backend and exports the env it discovered
# (CODEX_CWD / OPENCODE_BASE_URL / LETTA_BASE_URL) via $GITHUB_ENV. See
# .github/scripts/ for the inline detail.
- name: Install + authenticate the Codex CLI (codex)
if: matrix.lane == 'backends'
# Invoke via `bash` explicitly: on the windows-latest runner a directly
# executed .sh depends on the exec bit surviving checkout and Git Bash
# resolving the shebang; `bash <script>` sidesteps both. No-op on Linux.
run: bash .github/scripts/setup-codex.sh

# copilot_acp drives `copilot --acp`; auth is the job-env GITHUB_TOKEN (no login).
- name: Install the GitHub Copilot CLI (copilot_acp)
if: matrix.lane == 'backends'
run: .github/scripts/setup-copilot.sh

- name: Install + start the OpenCode server (opencode)
if: matrix.lane == 'backends'
env:
# The OpenCode Zen provider key the server reads via {env:...} substitution.
OPENCODE_ZEN_API_KEY: ${{ secrets.E2E_OPENCODE_ZEN_API_KEY }}
run: bash .github/scripts/setup-opencode.sh

# The Letta server runs in docker with a host-gateway alias so it can call
# back into the adapter's self-hosted MCP server (LocalMCPServer inside the
# pytest process) via host.docker.internal. OPENAI_API_KEY (mapped at job
# level) doubles as the Letta server's own LLM provider key.
- name: Start the Letta server (letta)
if: matrix.lane == 'letta'
run: .github/scripts/setup-letta.sh

# -----------------------------------------------------------------------------

- name: Install dependencies (${{ matrix.extra }})
run: uv sync --extra ${{ matrix.extra }}

# Flaky reply-latency tests opt into reruns per-test via @pytest.mark.flaky;
# the run itself stays plain so only those tests are retried, not the lane.
- name: Run baseline E2E (lane ${{ matrix.lane }})
run: uv run pytest tests/e2e/baseline/ -v -s --no-cov

# The backends lane also exercises the editor-facing ACP path (codex-acp).
# It uses the same ~/.codex login and a local MCP server — no Band platform.
- name: Run codex-acp e2e (codex)
if: matrix.lane == 'backends'
run: uv run pytest tests/integrations/acp/test_e2e_codex_acp.py -v --no-cov

- name: Stop the Letta server (letta)
if: always() && matrix.lane == 'letta'
run: docker rm -f letta-server || true

# Emitted even when tests fail (session end still runs), so a red lane still
# contributes its pass/fail rows to the merged scorecard.
- name: Upload lane scorecard
if: always()
uses: actions/upload-artifact@v4
with:
name: scorecard-${{ matrix.lane }}-${{ matrix.os }}
path: artifacts/scorecard-*.json
if-no-files-found: ignore

# Fold the per-lane scorecards into one adapter×test grid (pass / fail / skip / N-A).
# `if: always()` so a failed lane still yields a scorecard; this job reports the
# matrix, it does not gate on it (gating policy is tracked separately).
scorecard:
Comment thread .github/workflows/e2e.yml
Comment on lines +292 to +335
name: scorecard (merge lanes)
needs: e2e
if: always()
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up E2E environment
uses: ./.github/actions/setup-e2e

- name: Install dependencies
run: uv sync --extra dev

- name: Download lane scorecards
uses: actions/download-artifact@v4
with:
pattern: scorecard-*
path: artifacts
merge-multiple: true

- name: Merge into one scorecard
run: |
shopt -s nullglob
files=(artifacts/scorecard-*.json)
if [ ${#files[@]} -eq 0 ]; then
echo "no lane scorecards to merge (all lanes failed before session end)"
exit 0
fi
uv run python -m tests.e2e.baseline.scorecard merge "${files[@]}" \
--out artifacts/scorecard.json --markdown artifacts/scorecard.md

- name: Upload scorecard
if: always()
uses: actions/upload-artifact@v4
with:
name: scorecard
path: |
artifacts/scorecard.json
artifacts/scorecard.md
if-no-files-found: ignore
@AlexanderZ-Band
AlexanderZ-Band force-pushed the feat/band-python-kit-base-image-uv-isolated-sdk-venv-ca-INT-977 branch from df4b852 to 6654609 Compare July 16, 2026 07:55
socket.socket(socket.AF_INET, socket.SOCK_STREAM) as a,
socket.socket(socket.AF_INET, socket.SOCK_STREAM) as b,
):
a.bind(("", 0))
socket.socket(socket.AF_INET, socket.SOCK_STREAM) as b,
):
a.bind(("", 0))
b.bind(("", 0))
@AlexanderZ-Band
AlexanderZ-Band changed the base branch from main to dev July 16, 2026 07:56
…NT-977]

PYTHON_BASE_IMAGE and UV_IMAGE are now global build ARGs, defaulting to
the same fully-specific-tag+digest pins as before (python:3.12.13-slim-trixie,
uv 0.9.13). A build can override either via --build-arg without editing
the Dockerfile; the default keeps the pin's reproducibility guarantee for
anyone who doesn't override.

uv's image needs its own named stage (`FROM ${UV_IMAGE} AS uv`) since
COPY --from doesn't support ARG expansion directly.

Re-verified: default build, SDK_EXTRA build, multi-arch buildx build,
an explicit PYTHON_BASE_IMAGE override (confirmed sys.version actually
changes), and the full CA-wiring audit (PROXY_CA_CERT_B64 decode +
install, httpx verification, privilege drop to agent) all still pass.
…INT-977]

linux/aarch64: closes the gap this ticket's multi-arch Docker build
exercises (buildx --platform linux/arm64) but the resolver never
formally guaranteed. windows/AMD64: CI already runs windows-latest in
the test matrix (ci.yml) but the lock had no resolver guarantee for it
either.

Re-locked to generate the matching required-markers entries. Verified
no actual package version changed (diffed { name, version } pairs
before/after: identical) — the large uv.lock diff is uv's marker
simplifier restating the same crewai/parlant/pydantic_ai conflict
logic more concisely, not a dependency change. Full local test suite
(3798 passed) and the band-python-kit Docker build both still pass
against the updated lock.
band.docker.repo_init (core band-sdk, always in the baked venv) shells
out to git for clone/checkout/rev-parse and to ssh-keygen for SSH
host-key verification, but both were only installed in the builder
stage. Reproduced the failure: _git() raised FileNotFoundError with
git absent from the runtime image.

This image has no later Docker layer to add them in — INT-978's
launcher (which explicitly reuses band.docker.repo_init) runs directly
inside a container instantiated from this exact image, not a derived
build. Verified fixed (git --version succeeds via repo_init._git) and
re-ran the full regression sweep: non-root drop, immutable venv, fixed
interpreter path, entrypoint validation, SDK_EXTRA isolation, and the
end-to-end CA audit (PROXY_CA_CERT_B64 decode/install, httpx
verification, privilege drop) all still pass.
Builds the real image, creates a second venv inside a container to
simulate the customer venv INT-978's launcher will create at sandbox
runtime, installs a deliberately ancient/conflicting httpx pin into it,
and proves the baked SDK venv's own httpx and `band` import are
unaffected. This is the ticket's core proof obligation per its
Acceptance criteria.

Gated behind a new docker_build marker + DOCKER_TESTS_ENABLED env var,
mirroring how e2e tests are gated: CI runners do have a Docker daemon
(unlike the nested-virtualization sbx tests), so a plain
Docker-availability check wouldn't keep this off CI by itself. Skipped
by default, verified it actually runs and passes with the flag set,
and confirmed the standard test command (`pytest tests/
--ignore=tests/integration/ --ignore=tests/e2e/`) is unaffected (3798
passed, one more skip than before).
…-977]

Moves the subprocess/docker-CLI plumbing out of the conflicting-pin
test into tests/docker/toolkit/docker_cli.py, mirroring
tests/e2e/baseline/'s "toolkit holds the plumbing, the test holds the
scenario" split. Nothing in it is band-python-kit-specific, so a future
docker/ image test can reuse Image/Container as-is.

Image.build(...) and Container.run(...) are context managers on the
resource's own class rather than a create/remove function pair a
caller has to remember to match up — creation and guaranteed cleanup
live in one place. Container.run_python() shell-quotes its code
automatically (shlex.quote) so callers never hand-quote, while still
passing the interpreter through unquoted so $BAND_SDK_PYTHON expands.

toolkit/__init__.py stays empty; real code lives in the named
docker_cli submodule (matches the baseline toolkit's own convention).

Verified: default run still skips (no docker daemon touched), enabled
run builds/runs/passes, image+container are actually gone after
teardown, ruff+pyrefly clean, and the full standard suite (3798
passed) is unaffected.
Use the band_python_kit_container fixture parameter directly instead
of assigning it to a same-scope local named container.
…-977]

Proves the band-python-kit image's baked SDK works end-to-end against a
live Band platform: provisions a real agent + room via the baseline
toolkit, starts a minimal SimpleAdapter echo agent (core band-sdk only,
no framework) as the non-root "agent" user inside the actual built
image, sends a message as the user, and asserts a reply comes back
through the real WS/REST transport.

Locally targets whatever .env.test points at (dev, via the always-on
VPN); in CI it would target prod via secrets, matching every other live
E2E test's convention (dev locally / prod in CI) -- see the
e2e-dev-local-prod-ci memory note. Confirmed along the way that dev's
VPN-IP-allowlist does NOT block a local Docker container's real client
libraries (httpx, the SDK's own WebSocketClient) -- an earlier "container
can't reach dev" finding was a false alarm caused by testing with
urllib specifically, which the SDK never uses.

Gated behind BOTH docker_build and e2e markers (needs a Docker daemon
AND live credentials) -- deliberately outside tests/e2e/baseline/'s
adapter matrix, since there's no framework adapter to fan across; reuses
the baseline toolkit's ResourceManager/UserOps/reply_capture fixtures
directly instead. Needed the same asyncio(loop_scope="session") +
explicit timeout backstop tests/e2e/baseline/conftest.py auto-applies
to its own tree, added explicitly here since this test lives outside it.

Container/Image gain exec_background()/run_python_background() (fire-
and-forget via `docker exec -d`, for a process meant to keep running)
and a `user` parameter (a fresh `docker exec` defaults to root
regardless of what user the entrypoint dropped its own PID 1 to).

Verified: passes against dev end-to-end, cleans up fully (no leftover
image/container), skips correctly with either gate alone, ruff+pyrefly
clean, full standard suite (3798 passed) unaffected.
…NT-977]

Code review findings, both verified live before fixing:

[P1] Console-script shebangs (band-acp, band-trigger, ...) baked in the
builder's /build/.venv path, then the venv was copied to /opt/band/venv
without ever touching those shebangs -- reproduced: running the copied
script failed with "required file not found" (exit 127). Fixed at the
root: BAND_SDK_HOME is now a shared ARG between both stages, and
UV_PROJECT_ENVIRONMENT makes uv build the venv directly at that final
path in the builder stage, so the runtime COPY is path-for-path
identical and no relocation (or shebang mismatch) ever happens.
Verified: shebangs now read /opt/band/venv/bin/python, both console
scripts execute (--help exits 0), full regression sweep still passes.

[P2] The opt-in Docker build test had no timeout override, so the
repo-wide 30s pytest-timeout default (which bounds fixture setup, not
just the test body) could kill Image.build() on a cold-cache machine
long before its own internal 600s subprocess timeout ever got a chance.
Added @pytest.mark.timeout(660) (600s ceiling + margin).

Also (post-review, same area): moved the live E2E test's echo-agent
script from an inline string literal to a real file
(tests/docker/echo_agent.py) -- ruff-checked, and a refactor to
SimpleAdapter/Agent.create now shows up here instead of silently
drifting until the live test runs it -- and extracted the live test's
provision-agent/room + start-container-with-echo-agent plumbing into a
fixture (tests/docker/toolkit/live_agent.py +
conftest.py::live_containerized_agent), so the test body is scenario
only: get the agent+room, send a turn, assert.

Verified: both docker_build tests pass (pin-isolation and live, the
latter against dev end-to-end), default run still skips both, cleanup
confirmed, ruff+pyrefly clean, full standard suite (3798 passed)
unaffected.
Code review findings, both verified live before fixing:

[P2 CONFIRMED] --build-arg BAND_SDK_HOME=/srv/band correctly relocated
the venv and entrypoint.sh, but ENTRYPOINT's exec-form JSON array is one
of the few Dockerfile instructions Docker does not substitute ARG/ENV
into, so it stayed hardcoded at /opt/band/entrypoint.sh -- reproduced:
container failed outright ("stat /opt/band/entrypoint.sh: no such file
or directory"). Fixed by decoupling the entrypoint script's location
from the configurable SDK home: it now always copies to a fixed system
path (/usr/local/bin/band-entrypoint.sh, matching how uv/uvx are placed
there too), so no --build-arg override can ever break it. Kept
exec-form ENTRYPOINT rather than switching to shell-form to gain
variable expansion -- that would trade away correct SIGTERM forwarding
to PID 1 for a rarely-used customization path. Verified: both the
default build and a BAND_SDK_HOME override now start correctly and
drop privileges to `agent` as expected.

[P2 CONFIRMED] The live test's timeout only budgeted the live-turn
backstop (210s), not the session-scoped image-build fixture it shares
with the sibling pin test (which explicitly budgets for a 600s cold
build). If this file is selected alone on a machine with no cached
layers, a valid cold build could get killed well before Image.build()'s
own ceiling. Fixed by DRYing that ceiling into a named
BUILD_TIMEOUT_S constant in docker_cli.py (was a bare 600 literal) and
having both tests' timeouts derive from it, so they can't drift apart
again.

[P2 PARTIALLY RIGHT] The live-agent fixture yields right after
`docker exec -d` launches the echo process, without an explicit
readiness wait. Verified the underlying claim is real at the raw
protocol level (a bare Phoenix channel subscribe does not replay a
message sent before it joins -- 0 received in a direct test), but the
predicted consequence for this fixture doesn't reproduce: Agent.start()
does REST-based room reconciliation on startup, which picks up a
message sent to a room the agent is already a participant of regardless
of live-WS timing. Verified empirically with an injected 5s pre-connect
delay (~25x measured `import band` overhead) -- reply still arrived.
Documented the reasoning and its scope (relies on the bootstrap/first-
message path) in the fixture rather than adding synchronization
machinery for a risk that's empirically absent in the current scenario.

Verified: both docker_build tests pass (pin-isolation and live, against
dev end-to-end), default run still skips both, cleanup confirmed empty,
ruff+pyrefly clean, full standard suite (3798 passed) unaffected.
AlexanderZ-Band and others added 2 commits July 16, 2026 13:16
The base-image contract requires the digest-pinned uv at a fixed
image-owned path so the sandbox launcher can run locked customer-venv
sync without ever downloading uv at runtime — but the binary was only
copied into the builder stage. Copy it into the runtime stage under the
read-only SDK home, off PATH (same rationale as BAND_SDK_PYTHON), and
cover it with a docker-gated test that runs as the non-root agent user,
the launcher's actual uid after the entrypoint's privilege drop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AlexanderZ-Band

Copy link
Copy Markdown
Collaborator Author

Added 8ce00464: the runtime image now ships the digest-pinned uv at a fixed image-owned path ($BAND_SDK_UV = /opt/band/bin/uv, read-only, off PATH). Surfaced during INT-978 preflight — the sandbox launcher must run uv sync --locked for the customer venv without downloading uv at runtime, and the binary was previously only in the builder stage. Covered by a new docker-gated test that execs it as the non-root agent user (the launcher's actual uid after the entrypoint's privilege drop).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants