feat: band-python-kit base image, isolated SDK venv, CA wiring [INT-977]#443
Open
AlexanderZ-Band wants to merge 14 commits into
Open
feat: band-python-kit base image, isolated SDK venv, CA wiring [INT-977]#443AlexanderZ-Band wants to merge 14 commits into
AlexanderZ-Band wants to merge 14 commits into
Conversation
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 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 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 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
force-pushed
the
feat/band-python-kit-base-image-uv-isolated-sdk-venv-ca-INT-977
branch
from
July 16, 2026 07:55
df4b852 to
6654609
Compare
| 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)) |
…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.
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>
Collaborator
Author
|
Added |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
python:3.12image for arm64 and x86_64 underdocker/band_python_kit/, with the Python and uv images exposed as build arguments.$BAND_SDK_PYTHONexposes the absolute interpreter without putting the venv onPATH, keeping customer dependencies separate.SDK_EXTRA, avoiding known cross-extra dependency conflicts.PROXY_CA_CERT_B64into the system trust store, exportsSSL_CERT_FILEandREQUESTS_CA_BUNDLE, then drops from root to the non-rootagentuser before launching the requested command.gitandopenssh-clientsupport required byband.docker.repo_init.DOCKER_TESTS_ENABLED(andE2E_TESTS_ENABLEDfor the live test).docker/band_python_kit/README.mdand surfaces it from the repository's main README.Notable reliability fixes
/usr/local/bin/band-entrypoint.sh, independent of configurableBAND_SDK_HOME.Verification
SDK_EXTRAbuild and dependency isolation from a customer venv pinned tohttpx==0.13.3BAND_SDK_HOMEoverride builds and starts successfullyband-acpandband-triggerconsole scripts use the final venv interpreterlinux/amd64andlinux/arm64httpx; public TLS remains validagent) with and without proxy CA installationOut of scope
spec.yaml,sbx kit validate, andsbx run --kitare separate launcher and kit-spec deliverables.ghcr.io/thenvoi/band-python-kitand catalog integration are separate release deliverables.