# Contributing to AstraBox AstraBox is a self-hosted platform for running installed Agent programs in isolated sandboxes. It provides a web console, APIs, saved Session history, live output, and approval controls for teams. Thanks for helping improve it. ## Development setup You need **Python 3.12+**, a working **Docker** daemon (required for the dev stack and the end-to-end suite), and a PostgreSQL server for the unit tests that use the store. The repository manages its exact Node.js version from `.nvmrc`; an existing matching NVM install is reused, otherwise `make` downloads the checksum-pinned official archive into `.astrabox/toolchains/node`. Create a virtualenv and install the package editable with the dev tools: ```bash python -m venv .venv .venv/bin/pip install -U pip .venv/bin/pip install -e '.[dev,sandbox-server]' ``` The `[dev]` extra pulls in `pytest`, its async/timeout/xdist support, `ruff`, and `mypy`; it is enough for the Python gate (`make test-py`), lint, and typecheck. `[sandbox-server]` adds the bundled OpenSandbox service used by the default dev and container launch paths. A deployment configured with an external lifecycle service connects to it instead. If a required dependency is missing, startup exits with an explicit error. Use the Makefile target to install everything required by the complete `make test` gate, including the frontend, channel gateway, API code generator, and both Playwright projects: ```bash make install ``` ### Run the local dev stack `scripts/dev.sh` (also `make dev`) boots the two processes you iterate against. It needs the server image (`make build-server-image` once): - **Backend** — uvicorn (`astrabox.api.app:create_app`) with `--reload` in the server container of the maintained Compose stack, published at **:8000**. `containers/compose.source.yaml` mounts this checkout over the installed package, and `astrabox.deploy.onebox` starts the bundled OpenSandbox lifecycle server beside it with access to `/var/run/docker.sock`. - **Frontend** — the Vite dev server (`frontend/`) at **:5173**, whose `/api` proxies to the backend on `:8000`. ```bash make dev # == scripts/dev.sh (Ctrl-C stops both) ``` Then open . The backend boots without credentials — the UI and session list work — but a real Agent response additionally needs the agent image (`make build-agent-image`) and your LLM key. The dev stack and the e2e gate both read your Anthropic-compatible endpoint and token from a gitignored repo-root `.env` (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN` **or** `ANTHROPIC_API_KEY`, optional `ANTHROPIC_MODEL`). [`e2e/README.md`](e2e/README.md) covers the browser happy-path run end to end. You can run the backend on its own with `ASTRABOX_DEV_BACKEND_ONLY=1 scripts/dev.sh`, or change the backend port with `ASTRABOX_DEV_BACKEND_PORT=9000 scripts/dev.sh`. ## Which loop your change needs Use local static checks while editing, then choose verification by the behavior changed. A live check needs a deployment you control: Docker, the agent image, and a model key. | Changed | Check with | |---|---| | Documentation or comments | Comment and link checks; for website changes, bilingual build and website browser checks | | Frontend or backend behavior | Static checks, then the affected real E2E; the repository gate for a coherent release candidate | | Backend code on an isolated development deployment | `make run-mounted` mounts this tree over a compatible image's package | | Sandbox control service, Agent image, or deployment configuration | Build the affected images and verify the changed path against a live deployment | Dependency or bundled frontend changes also require a new server image (`make build-server-image`), because `make run-mounted` swaps code, not the environment: it mounts this tree over the package the image installed. One thing is deliberate, and costs an afternoon when it surprises you: never push a changed `:latest` to a shared cluster. A Kubernetes node pulls `IfNotPresent`, so a new `:latest` leaves it serving the OLD image while your source says otherwise. `make push-images REGISTRY=` tags by tree state precisely so a node cannot do that, and prints the `ASTRABOX_AGENT_IMAGE` line that points a deployment at what you pushed. ## Running tests ### Unit gate (PostgreSQL, no secret) Use a Git checkout at the candidate commit, including its Git metadata: `make check-comments` reads its file list from `git ls-files`, so an extracted deployment tarball alone cannot run the complete gate. This is what CI runs on every push and PR. A plain `pytest` run deselects the live suite by default (`pyproject.toml` sets `addopts = -m 'not e2e'`), so it never touches Docker or an LLM key. The unit tests are the database races and security boundaries an end-to-end run cannot show. Tests that use the store need a PostgreSQL server: set `ASTRABOX_TEST_POSTGRES_URL` to a `postgresql://` URL for a role that may create databases. Each pytest process creates its own database with every table and drops it at the end (`tests/_database.py`): ```bash ASTRABOX_TEST_POSTGRES_URL=postgresql://user:password@127.0.0.1:5432/postgres \ make test-py # unit suite only (the `e2e` mark is deselected) ``` The repository gate combines the local targets below. CI runs the corresponding checks in separate jobs in [`.github/workflows/ci.yml`](.github/workflows/ci.yml): ```bash make test # complete repository gate ``` The target includes repository consistency checks, Python lint/typecheck/tests, the generated API client check, channel-gateway tests, both Playwright typechecks, and the console typecheck, production build, policy checks, and unit tests. The Makefile is the current list. During diagnosis, retain the first failure and prove a fix with the smallest relevant check before rerunning its E2E. Continue with tests that lack evidence; do not restart an unaffected suite after each edit. Run the full gate once for a coherent release candidate. Never remove or weaken an assertion to make a failing change pass. ### End-to-end gate (needs Docker + an LLM key) The live suite is marked `e2e` and deselected by default. It needs a running deployment and the appropriate model and credential fixtures. Run only the selected cases for the behavior being changed; a model reply alone does not prove file persistence, native resume, child tasks, or approvals. ```bash ASTRABOX_E2E_BASE_URL=http://127.0.0.1:8088 make e2e-live ``` The development happy-path target builds the images and runs a curl smoke plus the console browser path. It is not the complete multi-engine campaign: ```bash make build-agent-image # bake astrabox/sandbox-claude-code:latest (pins + smoke-tests the claude CLI) make e2e ``` [`tests/e2e/README.md`](tests/e2e/README.md) describes the Python lane's two phases and how to select exact nodes; [`tests/e2e-ui/README.md`](tests/e2e-ui/README.md) does the same for the browser suite and lists the deployment fixtures each group of specs reads. Core lifecycle and native-state recovery must work without a persistent workspace volume; file retention is tested separately with persistent storage. ### Frontend build and tests The console SPA is TypeScript. Run its typecheck and real production build through the repository's pinned Node.js toolchain: ```bash make build-web ``` `make test-web` adds the channel gateway, console policy and unit tests, and both Playwright suite typechecks. ## Repository map Where to look when you want to change something: | Path | What lives there | |---|---| | `astrabox/seams/` | Versioned plugin interfaces for sandbox, storage, model endpoint, secret store, scheduler, runtime-preparation, messaging, and identity providers. Third-party plugins implement these interfaces and register through the `astrabox.providers.*` Python entry-point groups. Preserve backward compatibility when extending them. | | `astrabox/providers/` | The built-in providers: `open_sandbox/` (the sandbox backend — creates containers through the OpenSandbox lifecycle API; conversation tenancy isolates each Session, while agent tenancy can share one sandbox), `model.py` (the `litellm` model endpoint provider; no bundled direct passthrough), `secret_store.py` (AES-GCM local vault store), `identity.py` (no-auth defaults) + `identity_sso.py` (`trusted_header`/`jwt` resolvers). | | `astrabox/deploy/` | Deployment startup: `onebox.py` starts AstraBox and supervises the bundled services selected by configuration; `sandbox_server.py` renders the bundled lifecycle server's config from `ASTRABOX_*`. | | `astrabox/api/` | The HTTP API: `routes/` contains resource routers for Sessions (with their inputs, answers and output stream), Agents, Assistants, vaults, admin operations, MCP, and related resources; `app.py` contains the FastAPI factory, lifespan, and SPA serving. Routes stay thin — logic belongs in services. | | `astrabox/web/` | ASGI middleware for browser/API authentication and trusted-host validation. | | `astrabox/core/service/orchestrator/` | The platform core: `session_runtime/` (the one reader per Session that records what the Agent runtime reports, the output feed, and sending, answering and interrupting), `session_lifecycle/` (Session creation, startup and recovery), `runtime_manager.py` + `runtime/` (sandbox lifecycle and common workspace routing), `engine/` (native Agent-program adapters and their runtime clients), and deployment/assistant services. | | `astrabox/persistence/` | Data access code: `sql/` holds the PostgreSQL engine, the table declarations (`sql/models/`) and the Alembic schema upgrade that runs at startup (`sql/migrate.py`); `repository/` holds one repository class per domain, each reading and writing its own tables. | | `astrabox/config/` | Typed settings + the env-var registry (`env_registry.py`) — every env var the code reads must be registered; `scripts/check_env_registry.py` enforces it. | | `astrabox/testing/` | Reusable compatibility tests for engine, runtime-client, sandbox, storage, channel, and scheduler plugins, plus E2E fault-injection helpers. Plugin authors run these tests against their implementations. Production modules do not import this package. | | `astrabox/common/` | Logger + small shared utils. | | `frontend/` | The React console SPA (Vite; `npm run build` runs `tsc --noEmit` first). | | `containers/` | The server, sandbox and LiteLLM images + compose. | | `tests/` | Unit tests for database races and security boundaries (file naming: `*_test.py`). `tests/e2e/` is the live lane (`-m e2e`). | | `e2e/` | The Playwright browser suite against the real console. | | `tests/e2e-contract/` | What every live suite shares: the frozen lane inventory, the per-engine profiles the specs parametrize on, and the one 180-second test budget each Playwright config reads. | ## Coding conventions - **Python**: target 3.12, `ruff` for lint, `mypy` for typechecking, line length 100. Configuration lives in [`pyproject.toml`](pyproject.toml) (`[tool.ruff]`, `[tool.mypy]`). Run `make lint` and `make typecheck` before opening a PR — both must stay green. Ruff grandfathers a handful of rule codes tree-wide, and mypy runs with a documented, grandfathered list of disabled error codes — both with rationale in `pyproject.toml`. The remaining enabled rules stay enforced, including in new code, so do not introduce new violations. - **Errors**: use one explicit supported path. Do not silently switch providers, weaken security, or discard data when a required operation fails. Return an error with enough context to diagnose it. - **Comments**: follow the rules in [Comments](#comments) below. `make check-comments` runs the mechanical half of them — over every tracked and untracked source file (`git ls-files`), which includes `.ts`, `.tsx`, `.css`, `.sh` and `.yaml`, not only Python. A TypeScript-only or test-only change is inside its scope; "this change is not Python" is not a reason to skip it, and a violation left behind stays invisible until some unrelated change runs the gate. - **TypeScript**: run `make build-web` for console changes and `make test-web` for the complete web gate; both use the Node.js version pinned by the repository. - **Tests**: name Python test files `*_test.py`. Test observable product behavior through real E2E, including native supplier evidence and user-visible results. Keep a unit test only where an end-to-end run cannot show the property: a database race (uniqueness, a compare-and-set, a lease or fence under concurrency) or a security or authorization boundary. Repository consistency rules belong in the `scripts/check_*.py` gates, not in tests. When moving an implementation-shaped contract to E2E, preserve its intent, obtain passing evidence, then remove the replaced contract. Reusable provider checks belong in `astrabox/testing/`, not copies per backend. - Keep changes focused; match the style of the surrounding code. ## Comments Write comments for contributors who know only this repository. A useful comment explains behaviour they cannot infer from the code and points to files, symbols or public sources they can inspect. `make check-comments` enforces the mechanical half against a strict-zero baseline (`scripts/comment_style_baseline.json`); `make check-comments-report` prints every current finding grouped by class. 1. **Docstrings carry API behaviour.** Module, class and function docstrings reach API documentation, editor tooltips and `help()`. Put what callers rely on there — behaviour, prerequisites, expected errors, limits. An inline comment is for a local reason: an ordering requirement, a guard whose purpose its name does not carry, a value taken from a measurement. 2. **Describe the code, not how it got this way.** State the current requirement and its reason; change history belongs in the commit. Banned in comments: `used to`, `no longer`, `previously`, `originally`, `anymore`, `the regression`, `BUG (fixed)`, `moved verbatim`. When a guard prevents a known failure, describe the unguarded operation and the guard — not the guarded code as though it were broken. 3. **Every name a comment uses must resolve for the reader.** A code symbol, a repository path or a public URL. Never a work-plan or phase code (`F3-5`, `Stage T4`), an issue number in a tracker they cannot open, or a component that is not in this repository. 4. **State the failure, not where it was seen.** Keep the failure and its mechanism; do not cite private infrastructure a reader cannot inspect. 5. **The subject is something in the code.** Name the component or caller that acts. First person hides it — except inside a quoted value or user message. 6. **Explain why, never restate what.** If the code needs a comment to be readable, rename something or split the function instead. 7. **Neutral register.** No editorialising (`obviously`), no exasperation (`unfortunately`), no shouting. Emphasis is for a severe, silent failure — an all-caps `MUST NOT` on a security or data-loss constraint earns its keep. 8. **Section banners are noun phrases**, labelling a region so the file can be scanned; they are not headlines. 9. **No TODO, FIXME, HACK or commented-out code.** Track unfinished work in an issue with an owner and acceptance criteria. This tree has zero of these. 10. **Test comments explain the scenario's intent** — what situation is being built, and what would be wrong without the assertion. ## Pull request flow 1. Branch off `main`. 2. Make your change with focused commits. 3. Record what you ran: the static checks, and the live cases that cover the behaviour you changed. Run the complete repository gate when the batch is ready for review. 4. Open a pull request against `main`. CI ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs the unit/lint/typecheck gate on your branch and **must pass** before merge. The live e2e workflow ([`.github/workflows/e2e.yml`](.github/workflows/e2e.yml)) is manually dispatched and gated on an LLM-key repo secret. 5. Describe what changed and why; link any related issue. ## License of contributions AstraBox is licensed under the **Apache License 2.0** (see [`LICENSE`](LICENSE)). By submitting a contribution you agree that it is licensed under the same terms and that you have the right to contribute it.