feat: Band sandbox kit — spec.yaml (kit-spec v2) + entrypoint launcher [INT-978]#445
Open
AlexanderZ-Band wants to merge 15 commits into
Conversation
…, exec [INT-978] band.docker.launcher boots a locked customer uv project as a Band agent inside a Docker Sandbox: strict band.yaml + documented env overrides (pydantic-settings), traversal/symlink-fenced paths, an opt-in guard-railed workspace credential file, `uv sync --locked` with the image's pinned uv into a sandbox-owned venv (UV_PYTHON pinned, downloads disabled), optional band.docker.repo_init, then os.execve into the customer entrypoint so signals reach customer code directly. One module per concern so the package reads as customer-facing reference code. Every failure is a phase-named LaunchError that never carries secret values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kit-spec v2 (zero validation warnings on sbx v0.34.0): headless launch via a commands.startup entry chained through the base entrypoint (CA refresh, uid-1000 drop) into the launcher, and the measured-minimal egress allowlist for Band, PyPI, and the supported LLM backends. The example/ workspace is a complete locked customer project (annotated band.yaml, graceful-shutdown echo agent, committed uv.lock). Drift tests pin the spec's launch shape, allowlist, and example/launcher contract coherence. Proven live: fresh `sbx create --kit` boots the example onto the dev platform and completes a mentioned marker/echo-reply round trip, touching only files.pythonhosted.org and the Band host. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-978] Rewritten for SDK users: quickstart (build, template load, create), workspace/config reference, credential custody guardrails, launch flow and troubleshooting, network policy, and the image reference — including the operational gotchas (kit add silently skips network config; verify the privilege drop via the process table, not docker exec). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fail closed: inside a git repo, any unexpected git error now fails the credentials tracked/gitignored checks instead of silently skipping them; the not-a-repo skip is classified explicitly via rev-parse. - Correct HOME=/home/agent once at the process boundary in main() so git subprocesses (credentials check, repo_init clone) and uv all see the agent home, not root's inherited one. - Reject symlinked parent directories on the credentials path, not just a symlinked leaf — and make the symlink tests exercise the actual guard (the old test passed via a tmpdir-name coincidence). - Attribute credential-file read errors (e.g. unreadable host-uid-owned file) to the credentials phase instead of a generic OSError exit. - Pin UV_PYTHON to the base interpreter (sys.base_prefix), not the SDK venv python, with downloads disabled. - Validate static repo config (absolute path, URL scheme) fail-fast in the config phase instead of erroring after sync in repo-init. - Empty BAND_SDK_HOME no longer collapses the SDK-home fence to the cwd. - Assert the kit allowlist by exact equality so any egress widening fails the test, not just reintroducing the known-excluded hosts. - Table-drive runtime-path resolution; align ResolvedPaths field names with ResolvedLaunch; split logging setup into stderr + additive file handler. - Type the example adapter's on_message override; restore the dropped README notes (invoke SDK via $BAND_SDK_PYTHON, websockets CA rationale, sbx kit add silently skips network config). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… repo section [INT-978] Two speculative generalizations removed after scope review: - The BAND_KIT_*_PATH override matrix (8 vars) — band.yaml owns every path; only BAND_KIT_CONFIG_PATH remains as the bootstrap seam. Env overrides stay for identity, endpoints, and credentials, where the ticket wants them. - The repo: section / repo_init integration — in the sandbox model an additional repository is better mounted as an extra workspace (sbx create PATH [PATH...]) than cloned inside the VM; deferred to a fast-follow, and the dead-end context-bundle wiring goes with it. 848 -> 777 launcher lines, no safety property removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collaborator
Author
|
Scope trim after review ( |
…INT-978] The opt-in secrets handling now reads as what it is: a SecretsFile value object (locate() guarantees workspace containment), a CredentialName StrEnum as the documented-names allowlist, and a module-level GUARDS tuple of named check functions that IS the security policy in enforcement order — never_traverses_a_symlink, is_a_file, has_owner_only_permissions, is_gitignored_and_never_tracked, defines_only_documented_names. Same behavior and error messages; the checklist is now auditable at a glance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…INT-978] resolve_launch, the git guard, the sync, and load_file_credentials each mixed several concerns in one long body; every validation and side effect is now its own named function and the top-level functions read as phase lists. Behavior, phases, and error messages are unchanged. Log formatting moves from the legacy %-style string to a single launcher_formatter() factory: str.format-style fields (validated at construction since 3.8) with explicit ISO 8601 timestamps, shared by the stderr and file handlers so they can never drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-978] filelock is a core declared dependency, so deferring its import inside the sync (and the repo_init helper the pattern was copied from) bought nothing. Tests hoist their stdlib/filelock imports too; the lock contention test now patches the launcher module's own FileLock binding, the same seam style it already uses for subprocess. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ycle [INT-978] The model is produced by run and consumed by sync; housing it in run forced sync into a TYPE_CHECKING workaround. launch.py now holds the one data model the phases hand to each other, and every import of it is an ordinary top-level import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…raph_tools hashlib, re, and pydantic.Field are always available, so importing them inside functions bought nothing and hid the modules' real dependencies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-978] Each doc now owns one level and links instead of repeating: the kit README covers image/spec/launch, the new example README covers authoring a workspace (identity, credentials from the new secrets.env.example template, swapping the echo adapter), and the new launcher README maps the module-per-concern package. A drift test pins the template to exactly the documented credential names, and the launcher README's code fence runs in CI via markdown-docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… state [INT-978] Review hardening, each with a regression test that fails on the old code: - band.yaml's schemaVersion is pinned to the implemented version via Literal["1"]; a file declaring other semantics no longer launches. - Endpoint URLs are structurally validated (scheme AND host via urlsplit), so a hostless https://\wss:// fails the config phase instead of the first connect after the dependency sync. - The git credentials guard distinguishes git's own not-a-repository verdict (LC_ALL=C, recognized by message text — exit 128 is shared with every fatal git error) from corrupt/unreadable metadata, which now fails the launch instead of silently skipping the checks. - Outside a repository the ignore rule is still enforced, via check-ignore against a scratch --git-dir, so a later git init cannot expose the plaintext file; only the (meaningless) tracked-state check is skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s [INT-978] ResolvedLaunch now carries the full canonical credential mapping (process environment first, opt-in file filling gaps) instead of only the file's gap-fillers, and build_child_environment applies it. Validation accepts environment names case-insensitively and library callers may construct LauncherEnv programmatically; inheriting the raw environment alone could leave the child without the documented uppercase names the SDK reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t [INT-978] An opt-in repo: section in band.yaml clones the customer project into the fenced project path before dependency sync, completing the ticket's "reuse band.docker.repo_init" scope line: - The clone destination is derived, never configured: always the resolved project path (a required workspace subdirectory — the root holds band.yaml), so workspace config cannot direct clone writes elsewhere. - Path validation is two-stage: containment fencing always at resolve time; existence checks run post-bootstrap when a repo section is set. - State and context live under the runtime state path, never repo_init's /workspace defaults; failures surface as phase-labelled [repo] errors, with agent_id (never a credential) as the logged agent key. - repo.url is validated structurally in [config]: host and repository path must parse, blank URLs cannot fall into repo_init's local-only mode, and embedded userinfo credentials are rejected outright — band.yaml is committed and repo_init echoes the URL in logs and git error text (ssh://git@… stays accepted: a login user, not a secret). - Endpoint URL validation now requires a parsed hostname and forces the lazy port parse, rejecting host-less and malformed-port URLs. Existing checkouts are validated and reused on restart, never re-cloned (pinned by a real-git test). 33 new launcher tests; docs and the annotated example cover the new section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Implements INT-978: the declarative kit + launcher that boots a customer's locked Python project as a Band agent inside a Docker Sandbox, stacked on the INT-977 base image (#443). Full decision record and validation evidence: the implementation plan attached to the Linear ticket.
What's here
band.docker.launcher— SDK-owned launcher package, one module per concern (config,paths,credentials,bootstrap,sync,launch,run): strictband.yaml+ documented env overrides, traversal/symlink-fenced paths, opt-in guard-railed workspace credential file (T0 custody until INT-980/981), optional repository bootstrap reusingband.docker.repo_init(repo:inband.yaml; clone destination is always the fenced project path),uv sync --lockedvia the image's pinned$BAND_SDK_UVinto a sandbox-owned venv, thenos.execveinto the customer entrypoint. Phase-named errors, no secrets in diagnostics. 77 unit tests.docker/band_python_kit/spec.yaml— kit-spec v2,sbx kit validateclean (zero deprecation warnings, sbx v0.34.0). Headless launch viacommands.startupchained through the base entrypoint; measured-minimal egress allowlist. 7 drift tests pin the launch shape, allowlist, and example/launcher coherence.example/— complete customer workspace template (annotatedband.yaml, graceful-shutdown echo agent, committed PyPIuv.lock,secrets.env.example), with its own README; launcher README documents the module.Adaptations vs the ticket (deliberate, evidence-backed)
BAND_AGENT_ID/URLs "parameterized" inspec.yaml, but its own context fact add ruff, and pyrefly to run when merging to dev #3 (no--kit-arg/--env) makes a static spec unparameterizable — identity/endpoints come from the workspace file with env overrides, exactly as fact add ruff, and pyrefly to run when merging to dev #3 prescribes. Non-prod Band hosts are granted per sandbox viasbx policy allow, never baked in.commands.startup, notsandbox.entrypoint. Verified on sbx v0.34.0: a kit'sentrypoint.runexecutes only on interactive attach, as root, on a PTY;entrypoint.argsalone yields exit 126. The startup command (chained through the base entrypoint for CA + uid-1000 drop) starts the agent onsbx createalone and re-runs on every sandbox restart — stronger than the acceptance flow.repo_initreuse ships as an opt-inrepo:section (initially deferred per the attached plan, then pulled forward to complete the ticket's scope line). A kit workspace is a bind mount, so cloning is optional rather than the default: withrepo:set, the launcher clones into the fenced project path (a required workspace subdirectory) before the dependency sync; existing checkouts are validated and reused. State/context live under the runtime state path, never repo_init's/workspacedefaults.Proof (live)
Fresh
sbx create --kitagainst the dev platform: startup dispatcher → CA install → uid-1000 drop → credentials from the opt-in file → locked sync into the sandbox venv → customermain.pyunder the customer interpreter → mentioned marker sent via the baseline toolkit → echo reply received →sbx policy logshows exactly two hosts touched (files.pythonhosted.org,platform.dev.band.ai). Note: proof ran against dev (per the environment-targeting decision: dev locally / prod in CI; the flow is environment-agnostic by configuration) rather than the ticket's literal "staging".Test plan
ruff check/ruff format/pyrefly checkcleansbx kit validateVALID, zero warnings (v0.34.0)🤖 Generated with Claude Code