# AstraBox Community — developer entrypoints. # # These targets are the human-facing twins of what CI runs: # * `make test` == the .github/workflows/ci.yml unit/lint/typecheck job # (ruff + mypy + non-e2e pytest, + frontend # tsc, build & vitest) — no Docker daemon and no secret; # the pytest half needs ASTRABOX_TEST_POSTGRES_URL. # * `make e2e` == the .github/workflows/e2e.yml job (build the agent # image, then the curl smoke + the Playwright happy # path) — needs Docker + the operator LLM key (.env). # * `make dev` — boot the local backend+frontend stack (scripts/dev.sh). # * `make build-agent-image`— build the in-sandbox astrabox/sandbox-claude-code:latest image. # * `make k8s-testbed-up` — throwaway k3s + the OpenSandbox controller, so the # sandbox server's kubernetes runtime can be exercised # rather than assumed — needs passwordless sudo. # # Override the interpreter / image knobs on the command line, e.g. # make build-agent-image CLAUDE_CODE_VERSION=2.1.220 # make test PY=python3.12 SHELL := /usr/bin/env bash .DEFAULT_GOAL := help # ── tools / knobs ──────────────────────────────────────────────────────────── VENV ?= .venv PY ?= $(VENV)/bin/python NODE_TOOLCHAIN ?= python3 scripts/node-toolchain.py RUFF ?= $(PY) -m ruff MYPY ?= $(PY) -m mypy PYTEST ?= $(PY) -m pytest PKG ?= astrabox AGENT_IMAGE ?= astrabox/sandbox-claude-code:latest ASSISTANT_IMAGE ?= astrabox/sandbox-hermes:latest DSH_IMAGE ?= astrabox/sandbox-deepseek-harness:latest CODEX_IMAGE ?= astrabox/sandbox-codex:latest PI_IMAGE ?= astrabox/sandbox-pi:latest # An explicit execd input belongs to the caller; build only the implicit default. _EXECD_PREREQUISITE := $(if $(filter undefined,$(origin EXECD_IMAGE)),build-execd-image) EXECD_IMAGE ?= astrabox/execd:latest WORKSPACE_MOUNTER_IMAGE ?= astrabox/workspace-mounter:latest SERVER_IMAGE ?= astrabox/server:latest # `--build-context =DIR` flags for images with checksum-pinned source # stages. Empty for a standalone build, which downloads each pinned source in its # own stage; a build host with a verified cache passes it here instead # (scripts/pinned-build-sources.py). DOCKER_BUILD_CONTEXTS ?= ALL_IN_ONE_IMAGE ?= astrabox/all-in-one:latest # Kubernetes nodes using IfNotPresent need a new tag for each image build; # reusing `:latest` can leave a node serving cached content. The commit-derived # tag also makes the deployed source identifiable from `docker images`. # Source copies without `.git` use a timestamp so separate builds remain # distinguishable. IMAGE_TAG ?= $(shell git rev-parse --short=12 HEAD 2>/dev/null && { git diff --quiet 2>/dev/null || echo -dirty; } || date -u +nogit-%Y%m%d%H%M%S) REGISTRY ?= CLAUDE_CODE_VERSION ?= 2.1.308 SANDBOX_BASE_IMAGE ?= ghcr.io/agent-infra/sandbox:1.11.0 NPM_REGISTRY ?= https://registry.npmjs.org/ .PHONY: help install install-py install-web install-channel-gateway dev test test-py lint typecheck fmt-check check-e2e-collection \ check-comments check-comments-report check-i18n check-upstream check-api-client check-all-in-one check-dockerfile-modes check-sandbox-seam \ check-env-registry check-http-surface check-background-tasks check-release \ test-web test-channel-gateway test-layout utilisation build-web build-dist e2e e2e-smoke e2e-browser e2e-live \ build-agent-image build-assistant-image build-dsh-image build-codex-image build-pi-image build-execd-image build-workspace-mounter-image build-all-in-one-image clean \ k8s-testbed-up k8s-testbed-status k8s-testbed-down k8s-testbed-purge help: ## Show this help. @grep -hE '^[a-zA-Z0-9_-]+:.*?## ' $(MAKEFILE_LIST) \ | awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' # ── install ────────────────────────────────────────────────────────────────── install: install-py install-web install-channel-gateway ## Install Python, web, and channel-gateway dependencies. # [sandbox-server] is NOT optional for a developer checkout, even though it is an # optional extra of the package: every launch path (`make dev`, `make e2e-smoke`, # scripts/dev.sh, scripts/e2e_smoke.sh) runs `python -m astrabox.deploy.onebox`, # which starts the bundled OpenSandbox lifecycle server in-process and fails loud # without it. The CI unit lane deliberately installs `.[dev]` alone — it never # launches the stack — and that is what keeps `import astrabox` free of the extra. install-py: ## Create .venv (if missing) and install the package editable with [dev,sandbox-server]. @test -x "$(PY)" || python3 -m venv "$(VENV)" $(PY) -m pip install -U pip $(PY) -m pip install -e '.[dev,sandbox-server]' # scripts/api-codegen is its own npm package because openapi-typescript emits # through the TypeScript compiler API and the console is on typescript@7, the Go # port, which ships none — see scripts/check_api_client.py. install-web: ## npm ci for every web/test package (lockfile-exact). $(NODE_TOOLCHAIN) npm --prefix frontend ci $(NODE_TOOLCHAIN) npm --prefix tests/e2e-ui ci $(NODE_TOOLCHAIN) npm --prefix e2e ci $(NODE_TOOLCHAIN) npm --prefix scripts/api-codegen ci install-channel-gateway: ## npm ci for the bundled concrete channel adapters. $(NODE_TOOLCHAIN) npm --prefix channel-gateway ci --ignore-scripts # ── dev ────────────────────────────────────────────────────────────────────── dev: ## Boot the local dev stack: backend (:8000) + frontend (:5173). Ctrl-C stops. ./scripts/dev.sh # ── test (the CI unit/lint/typecheck job — no Docker daemon, no secret) ────── test: check-comments check-i18n check-errors check-env-registry check-http-surface check-background-tasks check-release check-credential-seam check-sandbox-seam check-all-in-one check-dockerfile-modes check-api-client lint typecheck test-py test-web ## Full unit gate: static checks + ruff + mypy + pytest + web build/vitest. test-py: ## Race and security unit tests (the live `e2e` mark is deselected). Needs ASTRABOX_TEST_POSTGRES_URL: a PostgreSQL role that may create databases. $(NODE_TOOLCHAIN) $(PYTEST) lint: ## ruff check (E402/F401/F811/F821/F841 grandfathered tree-wide — see pyproject). $(RUFF) check $(PKG) scripts tests typecheck: ## mypy over the package (lenient config — see [tool.mypy]). $(MYPY) $(PKG) fmt-check: ## Advisory ruff format check; not part of the repository gate. -$(RUFF) format --check $(PKG) screenshots: ## Recapture assets/screenshots/ from a live deployment. Needs ASTRABOX_E2E_BASE_URL, and ASTRABOX_E2E_SCREENSHOT_MODEL naming a model the deployment holds a credential for. $(NODE_TOOLCHAIN) npm --prefix tests/e2e-ui run e2e -- console-screenshots check-comments: ## Enforce the CONTRIBUTING.md comment rules against the reviewed zero baseline. $(PY) scripts/check_comment_style.py check-comments-report: ## Print every current comment-style violation, grouped by class. $(PY) scripts/check_comment_style.py --report check-i18n: ## Check console locale JSON, EN/ZH key parity, plurals, and placeholders. $(PY) scripts/check_i18n.py check-errors: ## Every raised error code has a registry row, against the shrinking baseline. $(PY) scripts/check_error_registry.py check-env-registry: ## Every env var read has a registry row and every row a read; docs/configuration.md is fresh. $(PY) scripts/check_env_registry.py check-http-surface: ## Routes are unique and deterministic, the OpenAPI snapshot is current, every _svc() call resolves. $(PY) scripts/check_http_surface.py check-background-tasks: ## Every created asyncio task is held or awaited. $(PY) scripts/check_background_tasks.py check-release: ## Default images are published by release.yml, Compose names the same registry, storage providers are discoverable. $(PY) scripts/check_release.py check-credential-seam: ## Credential machinery stays behind the egress seam, against the shrinking baseline. $(PY) scripts/check_credential_seam.py check-sandbox-seam: ## Platform code imports no sandbox backend's SDK, provider package or server launcher. $(PY) scripts/check_sandbox_seam.py check-all-in-one: ## Every Compose server setting is refused or passed through by the all-in-one image, whose edges match Compose's. $(PY) scripts/check_all_in_one.py check-dockerfile-modes: ## Every COPY/ADD from the build context sets its files' mode, so an image does not depend on the builder's umask. $(PY) scripts/check_dockerfile_modes.py check-api-client: ## frontend/src/api/schema.d.ts is byte-identical to what the committed OpenAPI snapshot generates. Needs `npm --prefix scripts/api-codegen ci` (make install-web). $(PY) scripts/check_api_client.py check-upstream: ## Every vendored ui/ and ai-elements/ file still matches its registry. Needs the network; NOT in `make test` for that reason — run it when re-vendoring, and when upstream moves. $(PY) scripts/check_upstream.py test-web: test-channel-gateway build-web typecheck-e2e ## Channel gateway + frontend typecheck (tsc --noEmit) + prod build + interaction-state, palette and failure-voice checks + vitest unit run + e2e spec typecheck. $(PY) scripts/check_interaction_states.py $(PY) scripts/check_palette.py $(PY) scripts/check_failure_voice.py $(NODE_TOOLCHAIN) npm --prefix frontend test test-channel-gateway: ## Node tests for provider schemas and the bundled adapter runtime. $(NODE_TOOLCHAIN) npm --prefix channel-gateway test typecheck-e2e: ## tsc --noEmit over both Playwright suites, plus the lane inventories. Playwright transpiles specs without typechecking them, so a wrong-arity API call is otherwise found on the testbed. $(NODE_TOOLCHAIN) npm --prefix tests/e2e-ui run typecheck $(NODE_TOOLCHAIN) npm --prefix e2e run typecheck $(PY) scripts/check_e2e_collection.py check-e2e-collection: ## Ask Playwright what each e2e lane holds and compare to the frozen contract. Needs the e2e-ui dependency tree (make install-web). $(PY) scripts/check_e2e_collection.py test-layout: ## Console layout gate across 4 viewports (stubbed /api — no backend, no Docker, no key). Needs chromium: `npm --prefix e2e run install:browser`. $(NODE_TOOLCHAIN) npm --prefix e2e run test:layout utilisation: ## Report how much of the window each page uses, at 1280/1440/1920/2560. Asserts nothing — read it when adding or widening a page. $(NODE_TOOLCHAIN) npm --prefix e2e run utilisation build-web: ## tsc --noEmit && vite build for the console. $(NODE_TOOLCHAIN) npm --prefix frontend run build # ── e2e (needs Docker + the operator LLM key in .env) ──────────────────────── e2e: build-server-image build-agent-image e2e-smoke e2e-browser ## Full live e2e: build images, curl smoke, Playwright happy-path. e2e-smoke: ## Headless curl-level live-turn proof (scripts/e2e_smoke.sh; asserts a real text-delta). ./scripts/e2e_smoke.sh e2e-browser: ## Browser happy-path through the real UI (e2e/ Playwright; boots its own stack). $(NODE_TOOLCHAIN) npm --prefix e2e ci $(NODE_TOOLCHAIN) npm --prefix e2e run install:browser $(NODE_TOOLCHAIN) npm --prefix e2e run test:happy # The live pytest suite against a deployment that is ALREADY running — set # ASTRABOX_E2E_BASE_URL to it. Five workers because these tests wait on a real # model and a real cluster, not on CPU. # # scripts/e2e_live.sh, not a bare pytest, because `maxfail` only stops pytest # handing out NEW tests: the workers already running keep going, and a live test # can sit until its fixed 180 s timeout. The script kills the run at the first failed # report and cleans up nothing, so the scene is there when you look. E2E_WORKERS ?= 5 e2e-live: ## Main PostgreSQL + Agent live pytest lane (5 workers; first-red; keeps its sandbox). E2E_WORKERS=$(E2E_WORKERS) ./scripts/e2e_live.sh # ── the Kubernetes testbed (scripts/k8s-testbed.sh) ────────────────────────── # Stands up a throwaway k3s + the OpenSandbox CONTROLLER, so the sandbox server's # kubernetes runtime can be exercised rather than assumed. The server itself is # NOT installed into the cluster — AstraBox runs it, exactly as on one host. # Needs passwordless sudo (it installs k3s). See docs/providers/opensandbox.md. k8s-testbed-up: ## Bring up k3s + the OpenSandbox controller + the sandbox namespace, then print the env to set. ./scripts/k8s-testbed.sh up k8s-testbed-status: ## What the testbed is actually running (cluster, controller, CRDs, live sandboxes). ./scripts/k8s-testbed.sh status k8s-testbed-down: ## Remove the controller, its CRDs and both namespaces. Refuses while sandbox resources still exist. ./scripts/k8s-testbed.sh down k8s-testbed-purge: ## k8s-testbed-down, then uninstall k3s itself. ./scripts/k8s-testbed.sh purge # ── images ─────────────────────────────────────────────────────────────────── build-execd-image: ## Build the pinned native filesystem transport with streaming uploads. docker build -t $(EXECD_IMAGE) $(DOCKER_BUILD_CONTEXTS) -f containers/execd/Dockerfile . build-agent-image build-dsh-image build-codex-image build-pi-image: $(_EXECD_PREREQUISITE) build-agent-image: ## Build the sandbox agent image; stop with an error if the pinned Claude CLI or runtime service is missing. docker build -t $(AGENT_IMAGE) \ --build-arg SANDBOX_EXECD_IMAGE=$(EXECD_IMAGE) \ --build-arg CLAUDE_CODE_VERSION=$(CLAUDE_CODE_VERSION) \ --build-arg SANDBOX_BASE_IMAGE=$(SANDBOX_BASE_IMAGE) \ --build-arg NPM_REGISTRY=$(NPM_REGISTRY) \ -f containers/sandbox-claude-code/Dockerfile \ . build-assistant-image: ## Build the in-sandbox Hermes image the assistant engine runs in. # The Hermes pin stays in containers/sandbox-hermes/Dockerfile rather than # being passed from here: a second copy of the version is a second thing to # forget. The base image IS passed, because the assistant and claude-code # boxes must share it. docker build -t $(ASSISTANT_IMAGE) $(DOCKER_BUILD_CONTEXTS) \ --build-arg SANDBOX_BASE_IMAGE=$(SANDBOX_BASE_IMAGE) \ -f containers/sandbox-hermes/Dockerfile \ . build-dsh-image: ## Build the in-sandbox DeepSeek Harness runtime image. docker build -t $(DSH_IMAGE) \ --build-arg SANDBOX_EXECD_IMAGE=$(EXECD_IMAGE) \ --build-arg SANDBOX_BASE_IMAGE=$(SANDBOX_BASE_IMAGE) \ -f containers/sandbox-deepseek-harness/Dockerfile \ . build-codex-image: ## Build the in-sandbox Codex app-server runtime image. docker build -t $(CODEX_IMAGE) \ --build-arg SANDBOX_EXECD_IMAGE=$(EXECD_IMAGE) \ --build-arg SANDBOX_BASE_IMAGE=$(SANDBOX_BASE_IMAGE) \ -f containers/sandbox-codex/Dockerfile \ . build-pi-image: ## Build the in-sandbox pi RPC runtime image. docker build -t $(PI_IMAGE) \ --build-arg SANDBOX_EXECD_IMAGE=$(EXECD_IMAGE) \ --build-arg SANDBOX_BASE_IMAGE=$(SANDBOX_BASE_IMAGE) \ --build-arg NPM_REGISTRY=$(NPM_REGISTRY) \ -f containers/sandbox-pi/Dockerfile \ . build-workspace-mounter-image: ## Build the host-side mergerfs workspace helper, outside agent sandboxes. docker build -t $(WORKSPACE_MOUNTER_IMAGE) $(DOCKER_BUILD_CONTEXTS) -f containers/workspace-mounter/Dockerfile . build-server-image: ## Build the deployment image (console + backend). Slow by design: it builds the frontend. docker build --build-arg NODE_VERSION=$$(cat .nvmrc) -t $(SERVER_IMAGE) -f containers/server/Dockerfile . build-all-in-one-image: build-server-image ## Build the single-container image FROM the server image just built: embedded PostgreSQL 17 and Valkey. docker build --build-arg SERVER_IMAGE=$(SERVER_IMAGE) -t $(ALL_IN_ONE_IMAGE) -f containers/all-in-one/Dockerfile . # Content-addressed publish for anything a CLUSTER pulls. Tagging by tree state # makes IfNotPresent safe: every changed tree receives a distinct tag, so a node # cannot serve a stale image for the requested tag. push-images: ## Tag every cluster-pulled image $(IMAGE_TAG) and push to $$REGISTRY (e.g. REGISTRY=10.0.0.5:5000). @test -n "$(REGISTRY)" || { echo "push-images: set REGISTRY=host:port"; exit 2; } docker tag $(EXECD_IMAGE) $(REGISTRY)/astrabox/execd:$(IMAGE_TAG) docker tag $(AGENT_IMAGE) $(REGISTRY)/astrabox/sandbox-claude-code:$(IMAGE_TAG) docker tag $(ASSISTANT_IMAGE) $(REGISTRY)/astrabox/sandbox-hermes:$(IMAGE_TAG) docker tag $(DSH_IMAGE) $(REGISTRY)/astrabox/sandbox-deepseek-harness:$(IMAGE_TAG) docker tag $(CODEX_IMAGE) $(REGISTRY)/astrabox/sandbox-codex:$(IMAGE_TAG) docker tag $(PI_IMAGE) $(REGISTRY)/astrabox/sandbox-pi:$(IMAGE_TAG) docker tag $(WORKSPACE_MOUNTER_IMAGE) $(REGISTRY)/astrabox/workspace-mounter:$(IMAGE_TAG) docker tag $(SERVER_IMAGE) $(REGISTRY)/astrabox/server:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/execd:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/sandbox-claude-code:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/sandbox-hermes:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/sandbox-deepseek-harness:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/sandbox-codex:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/sandbox-pi:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/workspace-mounter:$(IMAGE_TAG) docker push $(REGISTRY)/astrabox/server:$(IMAGE_TAG) @echo @echo "pushed $(IMAGE_TAG) — point the deployment at it:" @echo " ASTRABOX_AGENT_IMAGE=$(REGISTRY)/astrabox/sandbox-claude-code:$(IMAGE_TAG)" @echo " ASTRABOX_WORKSPACE_MOUNTER_IMAGE=$(REGISTRY)/astrabox/workspace-mounter:$(IMAGE_TAG)" @echo " assistant environment runtime_template_name=$(REGISTRY)/astrabox/sandbox-hermes:$(IMAGE_TAG)" @echo " dsh environment runtime_template_name=$(REGISTRY)/astrabox/sandbox-deepseek-harness:$(IMAGE_TAG)" @echo " codex environment runtime_template_name=$(REGISTRY)/astrabox/sandbox-codex:$(IMAGE_TAG)" @echo " pi environment runtime_template_name=$(REGISTRY)/astrabox/sandbox-pi:$(IMAGE_TAG)" # ── Source-mounted deployment loop ─────────────────────────────────────────── # scripts/dev.sh mounts this checkout over the installed package while keeping # the Compose database and sandbox-edge topology. Rebuild the server # image only when dependencies change; Python source changes reload in place. run-mounted: ## Run the source-mounted backend on :8088 using the maintained Compose topology. @test -f .env || { echo "run-mounted: needs .env (see .env.example)"; exit 2; } ASTRABOX_DEV_BACKEND_ONLY=1 \ ASTRABOX_DEV_BACKEND_PORT=8088 \ ASTRABOX_SERVER_IMAGE=$(SERVER_IMAGE) \ ./scripts/dev.sh build-dist: build-web ## Release wheel+sdist WITH the console packaged (astrabox/_frontend_dist). rm -rf astrabox/_frontend_dist dist cp -r frontend/dist astrabox/_frontend_dist $(PY) -m build @unzip -l dist/*.whl | grep -q "astrabox/_frontend_dist/index.html" \ || { echo "build-dist: wheel is MISSING the packaged console (astrabox/_frontend_dist)"; exit 1; } @unzip -l dist/*.whl | grep -q "astrabox/py.typed" \ || { echo "build-dist: wheel is MISSING the PEP 561 marker (astrabox/py.typed)"; exit 1; } @tar tzf dist/*.tar.gz | grep -q "_frontend_dist/index.html" \ || { echo "build-dist: sdist is MISSING the packaged console"; exit 1; } @echo "build-dist: console packaged OK ($$(unzip -l dist/*.whl | grep -c _frontend_dist) files)" # ── housekeeping ───────────────────────────────────────────────────────────── clean: ## Remove build/test caches + web dist dirs (does NOT touch .venv or state). rm -rf .pytest_cache .ruff_cache .mypy_cache **/__pycache__ \ frontend/dist astrabox/_frontend_dist dist \ e2e/test-results e2e/playwright-report